本篇教程的视频
本篇教程的源代码
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); }
|
这是获取服务端和客户端之间的网络通信包,这里我们就用Forge的NetworkHooks创建一个实体生成的数据包
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偏移量,这个值你可以自己调整
测试
那么最后我们就可以进入游戏进行测试了