feat: nPC可以正常移动

This commit is contained in:
叁玖领域 2026-04-12 14:48:01 +08:00
parent c80bd9dc75
commit b50fc8d5e4
6 changed files with 84 additions and 57 deletions

View File

@ -258,6 +258,11 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
return animStep;
}
@Override
public void setSpeed(float speed) {
super.setSpeed(speed);
this.setZza(speed);
}
protected void updateControlFlags() {
boolean flag = !(this.getControllingPassenger() instanceof Mob);

View File

@ -21,6 +21,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.control.Control;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import java.util.Optional;
@ -39,17 +40,17 @@ public class NPCLookControl implements Control {
}
public void setLookAt(Vec3 lookVector) {
public void setLookAt(@NotNull Vec3 lookVector) {
this.setLookAt(lookVector.x, lookVector.y, lookVector.z);
}
public void setLookAt(Entity entity) {
public void setLookAt(@NotNull Entity entity) {
this.setLookAt(entity.getX(), getWantedY(entity), entity.getZ());
}
public void setLookAt(Entity entity, float deltaYaw, float deltaPitch) {
public void setLookAt(@NotNull Entity entity, float deltaYaw, float deltaPitch) {
this.setLookAt(entity.getX(), getWantedY(entity), entity.getZ(), deltaYaw, deltaPitch);
}
@ -76,11 +77,11 @@ public class NPCLookControl implements Control {
if (this.lookAtCooldown > 0) {
--this.lookAtCooldown;
this.getYRotD().ifPresent((p_287447_) -> {
this.npc.yHeadRot = this.rotateTowards(this.npc.yHeadRot, p_287447_, this.yMaxRotSpeed);
this.getYRotD().ifPresent((aFloat) -> {
this.npc.yHeadRot = this.rotateTowards(this.npc.yHeadRot, aFloat, this.yMaxRotSpeed);
});
this.getXRotD().ifPresent((p_289400_) -> {
this.npc.setXRot(this.rotateTowards(this.npc.getXRot(), p_289400_, this.xMaxRotAngle));
this.getXRotD().ifPresent((aFloat) -> {
this.npc.setXRot(this.rotateTowards(this.npc.getXRot(), aFloat, this.xMaxRotAngle));
});
} else {
this.npc.yHeadRot = this.rotateTowards(this.npc.yHeadRot, this.npc.yBodyRot, 10.0F);

View File

@ -23,6 +23,7 @@ import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Predicate;
public class NPCFindTargetGoal extends Goal {
@ -35,8 +36,8 @@ public class NPCFindTargetGoal extends Goal {
// 目标过滤条件
private Predicate<LivingEntity> targetFilter;
// 优先级排序器
private Comparator<LivingEntity> targetSorter;
// 优先级排序器数值越小优先级越高
private Comparator<LivingEntity> priorityComparator;
public NPCFindTargetGoal(NPCServerPlayer npc, double searchRadius, int cooldown) {
this.npc = npc;
@ -50,44 +51,50 @@ public class NPCFindTargetGoal extends Goal {
entity.isAlive() &&
entity instanceof Player player && !player.isCreative();
// 默认排序最近优先
this.targetSorter = Comparator.comparingDouble(npc::distanceToSqr);
// 默认优先级距离最近优先级最高距离越小优先级越高
this.priorityComparator = Comparator.comparingDouble(npc::distanceToSqr);
}
// 允许自定义过滤和排序
// 允许自定义过滤条件
public NPCFindTargetGoal withFilter(Predicate<LivingEntity> filter) {
this.targetFilter = filter;
return this;
}
public NPCFindTargetGoal withSorter(Comparator<LivingEntity> sorter) {
this.targetSorter = sorter;
// 允许自定义优先级排序器例如血量最低威胁值最高等
public NPCFindTargetGoal withPriorityComparator(Comparator<LivingEntity> comparator) {
this.priorityComparator = comparator;
return this;
}
@Override
public boolean canUse() {
if (npc.getTarget() != null) return false;
if (cooldownTicks > 0) return false;
// 冷却中不寻找
if (cooldownTicks-- > 0) return false;
// 寻找有效目标
this.target = findNearestTarget();
// 寻找当前优先级最高的目标
this.target = findHighestPriorityTarget();
// 找到目标后立即返回 true准备设置目标
return this.target != null;
}
/**
* 一旦找到目标启动后立即停止不持续占用该 Goal
* 这样 NPC 每次只会执行一次寻找最高优先级目标的动作
* 然后由其他 Goal如攻击跟随来持续处理该目标
*/
@Override
public boolean canContinueToUse() {
// 继续条件目标存在且存活且没有更紧急的目标
return target != null &&
target.isAlive() &&
npc.getTarget() == target &&
!hasHigherPriorityTarget();
return false; // 找到目标后立即结束不持续运行
}
@Override
public void start() {
if (target != null) {
// 设置 NPC 的攻击目标
npc.setTarget(target);
// 重置冷却
cooldownTicks = cooldown;
}
}
@ -95,56 +102,62 @@ public class NPCFindTargetGoal extends Goal {
@Override
public void stop() {
// 不清除 target让其他 Goal 决定是否保留
cooldownTicks = cooldown;
// 只是本 Goal 停止运行
}
@Override
public void tick() {
// 由于 canContinueToUse 返回 falsetick 实际上不会被持续调用
// 但为了保险依然保留冷却递减逻辑
if (cooldownTicks > 0) {
cooldownTicks--;
}
// 定期验证目标是否仍然有效
if (npc.tickCount % 20 == 0 && target != null && !isValidTarget(target)) {
npc.setTarget(null);
target = null;
}
}
private LivingEntity findNearestTarget() {
return npc.level().getEntitiesOfClass(
LivingEntity.class,
npc.getBoundingBox().inflate(searchRadius),
this::isValidTarget
).stream()
.min(targetSorter)
/**
* 查找当前范围内优先级最高的有效目标
* 使用 priorityComparator 排序取第一个优先级最高
*/
private LivingEntity findHighestPriorityTarget() {
List<? extends LivingEntity> candidates = npc.level().getEntitiesOfClass(
LivingEntity.class,
npc.getBoundingBox().inflate(searchRadius),
this::isValidTarget
);
if (candidates.isEmpty()) {
return null;
}
// 根据优先级排序器找出优先级最高的目标
// 注意Comparator 定义的是优先级高的排序规则例如距离近在前血量低在前
return candidates.stream()
.min(priorityComparator)
.orElse(null);
}
/**
* 判断实体是否为有效目标
*/
private boolean isValidTarget(LivingEntity entity) {
// 基础检查
if (entity == npc) return false;
if (!entity.isAlive()) return false;
if (entity.isRemoved()) return false;
// 视线检查性能消耗较大
// 视线检查可选性能消耗较大
if (requiresLineOfSight() && !npc.hasLineOfSight(entity)) {
return false;
}
// 应用过滤器
// 应用自定义过滤器
return targetFilter.test(entity);
}
private boolean hasHigherPriorityTarget() {
// 检查是否有更高优先级
return npc.getLastHurtByMob() != null &&
npc.getLastHurtByMob().isAlive() &&
npc.distanceToSqr(npc.getLastHurtByMob()) < searchRadius * searchRadius;
}
/**
* 是否需要进行视线检查
*/
private boolean requiresLineOfSight() {
return true; // 配置
return true; // 可根据需要配置
}
}
}

View File

@ -38,22 +38,28 @@ public class NPCFollowGoal extends Goal {
@Override
public boolean canUse() {
this.target = npc.getTarget();
return target != null && npc.distanceToSqr(target) > stopDistance;
return target != null && target.isAlive() && npc.distanceToSqr(target) > stopDistance;
}
@Override
public boolean canContinueToUse() {
return target != null && npc.distanceToSqr(target) > followDistance;
return target != null && target.isAlive() && npc.distanceToSqr(target) > followDistance;
}
@Override
public void start() {
npc.getNavigation().moveTo(target, speedModifier);
}
@Override
public void stop() {
npc.setTarget(null);
}
@Override
public void tick() {
npc.getLookControl().setLookAt(target, 30.0f, 30.0f);
if (npc.distanceToSqr(target) > stopDistance) {
npc.getNavigation().moveTo(target, speedModifier);
npc.getNavigation().moveTo(target, speedModifier*5);
} else {
npc.getNavigation().stop();
}

View File

@ -24,6 +24,7 @@ import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.pathfinder.*;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCPathFinder;
import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCWalkNodeEvaluator;
@ -84,7 +85,7 @@ public class NPCGroupGroundPathNavigation extends NPCPathNavigation {
/**
* Returns a path to the given entity or null
*/
public Path createPath(Entity entity, int accuracy) {
public Path createPath(@NotNull Entity entity, int accuracy) {
return this.createPath(entity.blockPosition(), accuracy);
}

View File

@ -29,6 +29,7 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.pathfinder.*;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCNodeEvaluator;
@ -126,7 +127,7 @@ public abstract class NPCPathNavigation {
* Returns a path to one of the elements of the stream or null
*/
@Nullable
public Path createPath(Stream<BlockPos> targets, int accuracy) {
public Path createPath(@NotNull Stream<BlockPos> targets, int accuracy) {
return this.createPath(targets.collect(Collectors.toSet()), 8, false, accuracy);
}
@ -152,7 +153,7 @@ public abstract class NPCPathNavigation {
* Returns a path to the given entity or null
*/
@Nullable
public Path createPath(Entity entity, int accuracy) {
public Path createPath(@NotNull Entity entity, int accuracy) {
return this.createPath(ImmutableSet.of(entity.blockPosition()), 16, true, accuracy);
}
@ -165,7 +166,7 @@ public abstract class NPCPathNavigation {
}
@Nullable
protected Path createPath(Set<BlockPos> targets, int regionOffset, boolean offsetUpward, int accuracy, float followRange) {
protected Path createPath(@NotNull Set<BlockPos> targets, int regionOffset, boolean offsetUpward, int accuracy, float followRange) {
if (targets.isEmpty()) {
return null;
} else if (this.npc.getY() < (double)this.level.getMinBuildHeight()) {
@ -222,7 +223,7 @@ public abstract class NPCPathNavigation {
return false;
} else {
this.trimPath();
if (this.path.getNodeCount() <= 0) {
if (this.path != null && this.path.getNodeCount() <= 0) {
return false;
} else {
this.speedModifier = speed;