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

View File

@ -38,22 +38,28 @@ public class NPCFollowGoal extends Goal {
@Override @Override
public boolean canUse() { public boolean canUse() {
this.target = npc.getTarget(); this.target = npc.getTarget();
return target != null && npc.distanceToSqr(target) > stopDistance; return target != null && target.isAlive() && npc.distanceToSqr(target) > stopDistance;
} }
@Override @Override
public boolean canContinueToUse() { public boolean canContinueToUse() {
return target != null && npc.distanceToSqr(target) > followDistance; return target != null && target.isAlive() && npc.distanceToSqr(target) > followDistance;
} }
@Override @Override
public void start() { public void start() {
npc.getNavigation().moveTo(target, speedModifier); npc.getNavigation().moveTo(target, speedModifier);
} }
@Override
public void stop() {
npc.setTarget(null);
}
@Override @Override
public void tick() { public void tick() {
npc.getLookControl().setLookAt(target, 30.0f, 30.0f); npc.getLookControl().setLookAt(target, 30.0f, 30.0f);
if (npc.distanceToSqr(target) > stopDistance) { if (npc.distanceToSqr(target) > stopDistance) {
npc.getNavigation().moveTo(target, speedModifier); npc.getNavigation().moveTo(target, speedModifier*5);
} else { } else {
npc.getNavigation().stop(); 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.block.state.BlockState;
import net.minecraft.world.level.pathfinder.*; import net.minecraft.world.level.pathfinder.*;
import net.minecraft.world.phys.Vec3; 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.NPCServerPlayer;
import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCPathFinder; import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCPathFinder;
import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCWalkNodeEvaluator; 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 * 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); 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.level.pathfinder.*;
import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCNodeEvaluator; 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 * Returns a path to one of the elements of the stream or null
*/ */
@Nullable @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); 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 * Returns a path to the given entity or null
*/ */
@Nullable @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); return this.createPath(ImmutableSet.of(entity.blockPosition()), 16, true, accuracy);
} }
@ -165,7 +166,7 @@ public abstract class NPCPathNavigation {
} }
@Nullable @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()) { if (targets.isEmpty()) {
return null; return null;
} else if (this.npc.getY() < (double)this.level.getMinBuildHeight()) { } else if (this.npc.getY() < (double)this.level.getMinBuildHeight()) {
@ -222,7 +223,7 @@ public abstract class NPCPathNavigation {
return false; return false;
} else { } else {
this.trimPath(); this.trimPath();
if (this.path.getNodeCount() <= 0) { if (this.path != null && this.path.getNodeCount() <= 0) {
return false; return false;
} else { } else {
this.speedModifier = speed; this.speedModifier = speed;