本篇教程的视频

本篇教程的源代码

GitHub地址:

创建可坐实体

在前面一期教程中,我们已经制作了一个沙发,但现在玩家还没办法坐在上面

那么本期教程我们就来实现这个功能

在游戏中,要想实现一个方块可以让玩家坐下,那么就得去创建一个实体,因为只有实体里面是有可坐方法(ride)的

,还有矿车这种,它们都是实体

而我们的思路是,当我们右键方块与之交互时,我们就在这个方块的位置上创建一个实体,只是这个实体不能动,而且我们也看不见这个实体

另外,本期教程参考的是Mr.Crayfish的家具模组

SeatEntity

首先我们要创建一个SeatEntity类,让它继承Entity

1
2
3
4
5
6
7
8
9
10
11
12
public class SeatEntity extends Entity {
public SeatEntity(Level pLevel) {
super(, pLevel);
this.noPhysics = true;
}

private SeatEntity(Level level, BlockPos pos, double yOffset, Direction direction) {
this(level);
this.setPos(pos.getX() + 0.5, pos.getY() + yOffset, pos.getZ() + 0.5);
this.setRot(direction.toYRot(), 0.0f);
}
}

这里我们再写一个私有的构造函数,并设置一些属性,这个实际上是设置当玩家坐上去之后的位置和朝向

另外还有3个要我们重写的方法,但我们这个实体除了让玩家可以坐下之外,就没有别的作用了,所以这里的3个方法都空着就好了

它们的作用我写了注释,未来我们真正讲到实体时还会用到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void defineSynchedData() {
// 数据同步
}

@Override
protected void readAdditionalSaveData(CompoundTag pCompound) {
// 数据持久化,读取
}

@Override
protected void addAdditionalSaveData(CompoundTag pCompound) {
// 数据持久化,保存
}

tick

接下来我们要重写tick方法

1
2
3
4
5
6
7
8
9
10
@Override
public void tick() {
super.tick();
if (!this.level().isClientSide()) {
if (this.getPassengers().isEmpty() || this.level().isEmptyBlock(this.blockPosition())) {
this.remove(RemovalReason.DISCARDED);
this.level().updateNeighbourForOutputSignal(blockPosition(), this.level().getBlockState(blockPosition()).getBlock());
}
}
}

这个方法我们来检测当前位置上是否有玩家,或者说方块有没有被破坏

如果没有玩家或者方块也没了,那么就移除实体,因为实体是对游戏性能有影响的,一旦放了太多但并没有用上的实体就会导致我们游戏卡顿

我们现在只希望当玩家坐下时,这个实体存在,而脱离时这个实体就被移除了,以此来节约性能开销

getPassengersRidingOffset

1
2
3
4
@Override
public double getPassengersRidingOffset() {
return 0.0;
}

这个是获取高度偏移,原版会根据不同生物实体的大小来改变玩家骑乘的高度,
比如说,骑的时候它们的高度当然不一样

当然我们这里就返回0.0就可以了,具体的我们要根据座椅的实际高度赋值

canRide

1
2
3
4
@Override
protected boolean canRide(Entity pVehicle) {
return true;
}

显然易见,这个方法就是返回玩家是否可以骑,这里返回true

getAddEntityPacket

1
2
3
4
@Override
public Packet<ClientGamePacketListener> getAddEntityPacket() {
return NetworkHooks.getEntitySpawningPacket(this);
}

这是获取服务端和客户端之间的网络通信包,这里我们就用ForgeNetworkHooks创建一个实体生成的数据包

getDismountLocationForPassenger

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Vec3 getDismountLocationForPassenger(LivingEntity entity) {
Direction original = this.getDirection();
Direction[] offsets = {original, original.getClockWise(), original.getCounterClockWise(), original.getOpposite()};
for(Direction dir : offsets) {
Vec3 safeVec = DismountHelper.findSafeDismountLocation(entity.getType(), this.level(), this.blockPosition().relative(dir), false);
if(safeVec != null) {
return safeVec.add(0, 0.25, 0);
}
}
return super.getDismountLocationForPassenger(entity);
}

这个方法是当玩家脱离座椅之后,返回一个能够让玩家落脚的位置,防止玩家卡住

方位的话就找四个面,然后判断能不能让玩家重新生成

如果可以就再给一个Y轴上的偏移量,防止玩家卡住

create

这个方法是我们自己定义的,不是重写的

这个方法也是用来创建这个可坐实体的方法,它的返回值类型为InteractionResult,至于里面的参数,根据的是方块类中的use方法来写的,
待会你就知道为什么了

1
2
3
4
5
6
7
8
9
10
11
public static InteractionResult create(Level level, BlockPos pos, double yOffset, Player player, Direction direction) {
if(!level.isClientSide()) {
List<SeatEntity> seats = level.getEntitiesOfClass(SeatEntity.class, new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1.0, pos.getY() + 1.0, pos.getZ() + 1.0));
if(seats.isEmpty()) {
SeatEntity seat = new SeatEntity(level, pos, yOffset, direction);
level.addFreshEntity(seat);
player.startRiding(seat, false);
}
}
return InteractionResult.SUCCESS;
}

首先我们要判断是否是服务端,同样的,逻辑处理在服务端完成

List<SeatEntity> seats用于获取SeatEntity实体列表,这个获取范围也就只有一个方块的范围

如果这个列表是空的,那么就创建一个SeatEntity实体,然后调用world.spawnEntity(seat)方法来生成

最后调用player.startRiding方法让玩家骑上这个实体

另外,这里实例化的SeatEntity调用的是我们创建的私有构造方法

addPassenger

1
2
3
4
5
@Override
protected void addPassenger(Entity entity) {
super.addPassenger(entity);
entity.setYRot(this.getYRot());
}

这个方法是在玩家骑上实体时调用,同时我们这里还要重新设置玩家朝向

positionRider

1
2
3
4
5
@Override
public void positionRider(Entity entity, Entity.MoveFunction function) {
super.positionRider(entity, function);
this.clampYaw(entity);
}

这个方法用于更新玩家位置clampYaw是我们自定义的方法,用于限制玩家的朝向

clampYaw

1
2
3
4
5
6
7
8
private void clampYaw(Entity passenger) {
passenger.setYBodyRot(this.getYRot());
float wrappedYaw = Mth.wrapDegrees(passenger.getYRot() - this.getYRot());
float clampedYaw = Mth.clamp(wrappedYaw, -120.0F, 120.0F);
passenger.yRotO += clampedYaw - wrappedYaw;
passenger.setYRot(passenger.getYRot() + clampedYaw - wrappedYaw);
passenger.setYHeadRot(passenger.getYRot());
}

这里我们限制玩家头的转动范围,因为正常来说,在你不转身的情况下,我们的头是不可能360°转动的,得限制一下

这个限制呢就是一些计算公式,这里就不解释了

实体渲染器类

记住一件事,不论是生物实体、方块实体还是纯粹的实体,都需要写它们的渲染器类

因为你要告诉程序,这个实体该如何渲染,是一般的呢还是特殊的

SeatEntityRenderer

这里我们创建一个SeatEntityRenderer类,继承自EntityRenderer,泛型是SeatEntity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SeatEntityRenderer extends EntityRenderer<SeatEntity> {
protected SeatEntityRenderer(EntityRendererProvider.Context pContext) {
super(pContext);
}

@Override
public ResourceLocation getTextureLocation(SeatEntity pEntity) {
return null;
}

@Override
protected void renderNameTag(SeatEntity pEntity, Component pDisplayName, PoseStack pPoseStack, MultiBufferSource pBuffer, int pPackedLight) {

}
}

getTextureLocation方法用于获取实体的贴图,这里我们返回null,因为我们没有贴图(本来就看不见这个实体,也不用贴图)

同时我们再重写renderNameTag方法,这个方法用于渲染实体的标签,我们这里也不需要,所以直接空着

这个实体呢就像一个幽灵一样,你看不见它,但它就在那里,我们可以依靠这个实体来实现可坐

注册实体

ModEntities

接下来我们创建ModEntities,用来注册实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ModEntities {
public static final DeferredRegister<EntityType<?>> ENTITY_TYPES =
DeferredRegister.create(ForgeRegistries.ENTITY_TYPES, TutorialMod.MOD_ID);

public static final RegistryObject<EntityType<SeatEntity>> SEAT = ENTITY_TYPES.register("seat",
() -> EntityType.Builder.<SeatEntity>of(
(pEntityType, pLevel) -> new SeatEntity(pLevel), MobCategory.MISC)
.sized(0.0f, 0.0f)
.setCustomClientFactory(((spawnEntity, level) -> new SeatEntity(level)))
.build("seat"));

public static void register(IEventBus eventBus) {
ENTITY_TYPES.register(eventBus);
}
}

Forge这里,我们还是用延迟注册器来注册

注册语句中,我们需要通过EntityType.Builder.of方法构造我们的实体

MobCategory是实体所属类别,当然这个座椅比较特殊,所以就放在杂项里了

sized是碰撞箱,或者叫包围盒的大小,同样因为座椅特殊,而且我们这个实体需要依附方块存在,而方块本身也有碰撞箱,如果实体还有碰撞箱那么就会冲突

setCustomClientFactory方法是客户端收到实体生成数据包时,构造实体的方法

另外还有一个register这个用于初始化的方法

不要忘记主类调用

1
ModEntities.register(modEventBus);

前面SeatEntity构造函数中的方法我们就可以写了

1
2
3
4
public SeatEntity(Level pLevel) {
super(ModEntities.SEAT.get(), pLevel);
this.noPhysics = true;
}

渲染器类注册

写好渲染器和注册好方块实体之后,我们还要到客户端类中注册这个实体渲染器

1
2
3
4
@SubscribeEvent
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(ModEntities.SEAT.get(), SeatEntityRenderer::new);
}

在主类的ClientModEvents中写上上面那段话即可

可坐实体创建

那么最后就是要创建这个可坐实体了

我们到之前写的SofaBlock中,重写onUse方法

1
2
3
4
5
6
7
@Override
public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHit) {
if (!pLevel.isClientSide()) {
return SeatEntity.create(pLevel, pPos, 0.25, pPlayer, pState.getValue(FACING));
}
return InteractionResult.SUCCESS;
}

use方法是玩家右键这个方块时调用,它的返回值是InteractionResult,所以我们写的create方法也是用的InteractionResult

这里面的0.25,就是给的Y偏移量,这个值你可以自己调整

测试

那么最后我们就可以进入游戏进行测试了