Создание вашей первой сущности 26.1.2
Узнайте, как зарегистрировать простую сущность, задать ей цели (goals), настроить рендеринг, модель и анимацию.
Сущности (Entities) — это динамические, интерактивные объекты в игре, которые не являются частью ландшафта (в отличие от блоков). Сущности могут перемещаться и взаимодействовать с миром различными способами. Вот несколько примеров:
- Деревенский житель (
Villager), Свинья (Pig) и Коза (Goat) — это примеры Мобов (Mob). Это самый распространенный тип сущностей — живые существа. - Зомби (
Zombie) и Скелет (Skeleton) — это примеры Монстров (Monster). Это разновидность сущностей, враждебных к Игроку (Player). - Вагонетка (
Minecart) и Лодка (Boat) — это примеры Транспорта (VehicleEntity). Они имеют специальную логику для обработки управления игроком.
В данном руководстве мы будем создавать сущность Мини-голем. Эта сущность будет иметь забавные анимации. Она будет наследоваться от класса PathfinderMob. Этот класс используется для большинства мобов с поиском пути, таких как Зомби и Деревенский житель.
Подготовка вашей первой сущности
Первый шаг при создании сущности — это определение её класса и регистрация в игре.
Мы создадим класс MiniGolemEntity и начнем с настройки атрибутов. Атрибуты определяют различные параметры, включая максимальное здоровье, скорость передвижения и радиус приманивания сущности.
java
public class MiniGolemEntity extends PathfinderMob {
public MiniGolemEntity(Level world) {
this(ModEntityTypes.MINI_GOLEM, world);
}
public MiniGolemEntity(EntityType<? extends MiniGolemEntity> entityType, Level world) {
super(entityType, world);
}
public static AttributeSupplier.Builder createCubeAttributes() {
return PathfinderMob.createMobAttributes()
.add(Attributes.MAX_HEALTH, 5)
.add(Attributes.TEMPT_RANGE, 10)
.add(Attributes.MOVEMENT_SPEED, 0.3);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Для регистрации сущности рекомендуется создать отдельный класс ModEntityTypes. В нем вы будете регистрировать абсолютно все типы сущностей, задавать их размеры и регистрировать их атрибуты.
java
public class ModEntityTypes {
public static final EntityType<MiniGolemEntity> MINI_GOLEM = register(
"mini_golem",
EntityType.Builder.<MiniGolemEntity>of(MiniGolemEntity::new, MobCategory.MISC)
.sized(0.75f, 1.75f)
);
private static <T extends Entity> EntityType<T> register(String name, EntityType.Builder<T> builder) {
ResourceKey<EntityType<?>> key = ResourceKey.create(Registries.ENTITY_TYPE, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, name));
return Registry.register(BuiltInRegistries.ENTITY_TYPE, key, builder.build(key));
}
public static void registerModEntityTypes() {
ExampleMod.LOGGER.info("Registering EntityTypes for " + ExampleMod.MOD_ID);
}
public static void registerAttributes() {
FabricDefaultAttributeRegistry.register(MINI_GOLEM, MiniGolemEntity.createCubeAttributes());
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Добавление целей
Цели (Goals) — это система, которая управляет задачами и намерениями сущности, обеспечивая её определенным набором поведения. Цели имеют приоритет: задачи с меньшим значением приоритета выполняются в первую очередь по сравнению с задачами, у которых это значение выше.
Чтобы добавить цели вашей сущности, необходимо создать метод registerGoals в классе сущности.
java
@Override
protected void registerGoals() {
this.goalSelector.addGoal(0, new TemptGoal(this, 1, Ingredient.of(Items.WHEAT), false));
this.goalSelector.addGoal(1, new RandomStrollGoal(this, 1));
this.goalSelector.addGoal(2, new LookAtPlayerGoal(this, Cow.class, 4));
this.goalSelector.addGoal(3, new RandomLookAroundGoal(this));
}1
2
3
4
5
6
7
2
3
4
5
6
7
INFO
TemptGoal— приманивание: сущность идет за игроком, который держит в руках определенный предмет.RandomStrollGoal— случайное блуждание: сущность ходит и бродит по миру.LookAtPlayerGoal— взгляд на игрока: несмотря на название, эта цель принимает любую сущность. В данном случае она используется для взгляда на Корову (Cow).RandomLookAroundGoal— случайный осмотр: сущность осматривается в случайных направлениях.
Создание рендеринга
Рендеринг — это процесс преобразования игровых данных (т.к. блоки, сущности и окружение) в визуальные образы, отображаемые на экране игрока. Этот процесс определяет, как объекты освещаются, затеняются и покрываются текстурами.
INFO
Рендеринг сущностей всегда обрабатывается на стороне клиента (client side). Сервер управляет логикой и поведением сущности, в то время как клиент отвечает за отображение её модели, текстуры и анимаций.
Процесс рендеринга состоит из нескольких шагов и включает в себя разные классы, но мы начнем с класса EntityRenderState.
java
public class MiniGolemEntityRenderState extends LivingEntityRenderState {
}1
2
2
Данные, хранящиеся в состоянии рендеринга (render state), используются для определения визуального облика сущности. Сюда входят состояния анимации, такие как поведение при движении или в режиме ожидания (idle).
Настройка модели
Класс MiniGolemEntityModel определяет внешний вид вашей сущности, описывая её форму и части тела. Обычно модели создаются в сторонних программах, таких как Blockbench, а не пишутся вручную. Здесь же мы разберем пример ручного создания, чтобы показать вам, как это работает.
WARNING
Blockbench поддерживает несколько видов маппингов (например, Mojang Mappings, Yarn и другие). Убедитесь, что вы выбрали маппинг, соответствующий вашей среде разработки — в данном туториале используются Mojang Mappings.
Несовпадение маппингов может привести к ошибкам при интеграции кода, сгенерированного в Blockbench.
java
public class MiniGolemEntityModel extends EntityModel<MiniGolemEntityRenderState> {
private final ModelPart head;
private final ModelPart leftLeg;
private final ModelPart rightLeg;
public MiniGolemEntityModel(ModelPart root) {
super(root);
this.head = root.getChild(PartNames.HEAD);
this.leftLeg = root.getChild(PartNames.LEFT_LEG);
this.rightLeg = root.getChild(PartNames.RIGHT_LEG);
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Класс MiniGolemEntityModel задаёт визуальную модель для нашей сущности. Он наследуется от EntityModel и определяет названия частей тела сущности (тело, голова, левая нога и правая нога).
java
public static LayerDefinition getTexturedModelData() {
MeshDefinition modelData = new MeshDefinition();
PartDefinition root = modelData.getRoot();
root.addOrReplaceChild(
PartNames.BODY,
CubeListBuilder.create().addBox(
/* x: */ -6,
/* y: */ -6,
/* z: */ -6,
/* width: */ 12,
/* height: */ 12,
/* depth: */ 12
),
PartPose.offset(0, 8, 0)
);
root.addOrReplaceChild(
PartNames.HEAD,
CubeListBuilder.create().texOffs(36, 0).addBox(-3, -6, -3, 6, 6, 6),
PartPose.offset(0, 2, 0)
);
root.addOrReplaceChild(
PartNames.LEFT_LEG,
CubeListBuilder.create().texOffs(48, 12).addBox(-2, 0, -2, 4, 10, 4),
PartPose.offset(-2.5f, 14, 0)
);
root.addOrReplaceChild(
PartNames.RIGHT_LEG,
CubeListBuilder.create().texOffs(48, 12).addBox(-2, 0, -2, 4, 10, 4),
PartPose.offset(2.5f, 14, 0)
);
return LayerDefinition.create(modelData, 64, 32);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Этот метод формирует 3D-модель Мини-голема: он создает тело, голову и ноги в виде параллелепипедов (cuboids), задает их координаты, настраивает привязку к текстуре (texture mappings) и возвращает объект LayerDefinition для последующего рендеринга.
Каждая часть тела добавляется со своим смещением (offset point), которое служит точкой начала координат (центром) для всех трансформаций этой части. Все остальные координаты внутри элемента модели рассчитываются относительно этой точки смещения.
WARNING
Логика координат здесь работает иначе: чем больше значение Y, тем ниже объект находится относительно сущности. Это инверсия обычной игровой системы координат.
Теперь нам нужно создать класс ModEntityModelLayers в клиентском пакете (client). Эта сущность имеет всего один текстурный слой, но другие сущности могут использовать несколько — вспомните дополнительный слой кожи у Игрока (Player) или глаза Паука (Spider).
java
public class ModEntityModelLayers {
public static final ModelLayerLocation MINI_GOLEM = createMain("mini_golem");
private static ModelLayerLocation createMain(String name) {
return new ModelLayerLocation(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, name), "main");
}
public static void registerModelLayers() {
ModelLayerRegistry.registerModelLayer(ModEntityModelLayers.MINI_GOLEM, MiniGolemEntityModel::getTexturedModelData);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Затем этот класс необходимо инициализировать в клиентском инициализаторе мода.
java
public class ExampleModCustomEntityClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ModEntityModelLayers.registerModelLayers();
}
}1
2
3
4
5
6
2
3
4
5
6
Настройка текстуры
TIP
Убедитесь, что размеры текстуры совпадают со значениями из метода LayerDefinition.create(modelData, 64, 32). В данном случае ширина должна быть 64 пикселя, а высота — 32. Если вам нужна текстура другого размера, не забудьте изменить параметры в LayerDefinition.create, чтобы они совпадали.
Каждая часть модели / куб ожидает увидеть развертку на текстуре в определенном месте. По умолчанию ожидается координата 0, 0 (верхний левый угол), но это можно изменить, вызвав функцию texOffs в CubeListBuilder.
В качестве примера вы можете использовать эту текстуру для пути assets/example-mod/textures/entity/mini_golem.png
Создание класса отрисовки
Класс отрисовки сущности (renderer) позволяет увидеть вашу сущность в игре. Мы создадим новый класс MiniGolemEntityRenderer, который укажет Minecraft, какую текстуру, модель и состояние рендеринга (render state) использовать для этой сущности.
java
public class MiniGolemEntityRenderer extends MobRenderer<MiniGolemEntity, MiniGolemEntityRenderState, MiniGolemEntityModel> {
private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "textures/entity/mini_golem.png");
public MiniGolemEntityRenderer(EntityRendererProvider.Context context) {
super(context, new MiniGolemEntityModel(context.bakeLayer(ModEntityModelLayers.MINI_GOLEM)), 0.375f); // 0.375 shadow radius
}
@Override
public MiniGolemEntityRenderState createRenderState() {
return new MiniGolemEntityRenderState();
}
@Override
public Identifier getTextureLocation(MiniGolemEntityRenderState state) {
return TEXTURE;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Здесь же задается радиус тени — для этой сущности он будет равен 0.375f.
Затем его необходимо зарегистрировать в клиентском инициализаторе мода.
java
EntityRenderers.register(ModEntityTypes.MINI_GOLEM, MiniGolemEntityRenderer::new);1
Добавление анимации ходьбы
Чтобы задать сущности анимацию ходьбы, нужно добавить в класс MiniGolemEntityModel следующий код:
java
@Override
public void setupAnim(MiniGolemEntityRenderState state) {
super.setupAnim(state);
this.head.xRot = state.xRot * Mth.RAD_TO_DEG;
this.head.yRot = state.yRot * Mth.RAD_TO_DEG;
float limbSwingAmplitude = state.walkAnimationSpeed;
float limbSwingAnimationProgress = state.walkAnimationPos;
this.leftLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f + Mth.PI) * 1.4f * limbSwingAmplitude;
this.rightLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f) * 1.4f * limbSwingAmplitude;
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Для начала примените углы рыскания (yaw) и тангажа (pitch) к части модели, отвечающей за голову.
Затем мы применяем анимацию ходьбы к частям модели, отвечающим за ноги. Мы используем функцию cos (косинус) для создания базового эффекта качания ног, а затем преобразуем косинусоидальную волну, чтобы получить правильную скорость и амплитуду раскачивания.
- Константа
0.2в формуле управляет частотой косинусоидальной волны (насколько быстро двигаются ноги). Более высокие значения увеличивают частоту. - Константа
1.4в формуле управляет амплитудой косинусоидальной волны (насколько далёк размах ног). Более высокие значения увеличивают амплитуду. - Переменная
limbSwingAmplitude(амплитуда движения конечностей) влияет на амплитуду точно так же, как и константа1.4. Эта переменная меняется в зависимости от скорости сущности: ноги качаются сильнее, когда сущность движется быстрее, и слабее (или вообще не двигаются), когда сущность замедляется или стоит на месте. - Константа
Mth.PIдля левой ноги сдвигает косинусоидальную волну на полупериод, чтобы левая нога двигалась в противофазе относительно правой.
Вы можете построить их на графике, чтобы посмотреть, как они выглядят:


Синяя кривая отвечает за левую ногу, а коричневая — за правую. Горизонтальная ось X представляет собой время, а ось Y указывает на угол наклона конечностей (ног).
Вы можете свободно поэкспериментировать с константами на Desmos, чтобы увидеть, как именно они влияют на кривую.
Если зайти в игру, то теперь у вас есть всё необходимое, чтобы призвать эту сущность с помощью команды /summon example-mod:mini_golem.

Добавление данных в сущность
Для хранения данных сущности обычно достаточно просто добавить поле в класс сущности.
Иногда необходимо синхронизировать данные с серверной сущности с данными с клиентской сущности. Дополнительную информацию об архитектуре клиент-сервер см. на странице «Сетевое взаимодействие». Для этого можно использовать синхронизированные данные, определив для них EntityDataAccessor.
В нашем случае мы хотим, чтобы наша сущность время от времени танцевала. Поэтому нам нужно создать состояние танца, которое синхронизируется между клиентами, чтобы позже его можно было анимировать. Однако время перезарядки танца не обязательно синхронизировать с клиентом, так как анимация запускается сервером.
java
private static final EntityDataAccessor<Boolean> DANCING = SynchedEntityData.defineId(MiniGolemEntity.class, EntityDataSerializers.BOOLEAN);
private int dancingTimeLeft;
@Override
protected void defineSynchedData(SynchedEntityData.Builder builder) {
super.defineSynchedData(builder);
builder.define(DANCING, false);
}
public boolean isDancing() {
return entityData.get(DANCING);
}
private void setDancing(boolean dancing) {
entityData.set(DANCING, dancing);
}
@Override
public void tick() {
super.tick();
if (!level().isClientSide()) {
if (this.isDancing()) {
if (this.dancingTimeLeft-- <= 0) {
this.setDancing(false);
}
} else {
if (this.random.nextInt(1000) == 0) {
this.setDancing(true);
this.dancingTimeLeft = 100 + this.random.nextInt(100);
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Как видите, мы добавили метод tick для управления состоянием танца.
Сохранение данных в NBT
Для постоянных данных, которые должны сохраняться после закрытия игры, мы переопределим методы addAdditionalSaveData и readAdditionalSaveData в классе MiniGolemEntity. Мы можем использовать их для хранения количества времени, оставшегося до конца анимации танца.
java
@Override
protected void addAdditionalSaveData(ValueOutput valueOutput) {
super.addAdditionalSaveData(valueOutput);
valueOutput.putInt("dancing_time_left", this.dancingTimeLeft);
}
@Override
protected void readAdditionalSaveData(ValueInput valueInput) {
super.readAdditionalSaveData(valueInput);
this.dancingTimeLeft = valueInput.getInt("dancing_time_left").orElse(0);
this.setDancing(this.dancingTimeLeft > 0);
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Теперь при каждой загрузке сущности она будет восстанавливать состояние, в котором ее оставили.
Добавление анимации
Первый шаг для добавления анимации к сущности — это добавление состояния анимации (animation state) в класс сущности. Мы создадим состояние анимации, которое будет использоваться для того, чтобы заставить сущность танцевать.
java
public final AnimationState dancingAnimationState = new AnimationState();
@Override
public void onSyncedDataUpdated(EntityDataAccessor<?> data) {
super.onSyncedDataUpdated(data);
if (data == DANCING) {
this.dancingAnimationState.animateWhen(this.isDancing(), this.tickCount);
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Мы переопределили метод onSyncedDataUpdated. Он вызывается всякий раз, когда синхронизированные данные обновляются как на сервере, так и на клиенте. Условие if проверяет, являются ли обновленные синхронизированные данные именно данными танца.
Теперь мы перейдем к самой анимации. Мы создадим класс MiniGolemAnimations и добавим AnimationDefinition, чтобы определить, как именно анимация будет применяться к сущности.
java
public class MiniGolemAnimations {
public static final AnimationDefinition DANCING = AnimationDefinition.Builder.withLength(1)
.looping()
.addAnimation(PartNames.HEAD, new AnimationChannel(
AnimationChannel.Targets.ROTATION,
new Keyframe(0, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.2f, KeyframeAnimations.degreeVec(0, 0, 45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.4f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.6f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.8f, KeyframeAnimations.degreeVec(0, 0, -45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(1, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR)
))
.addAnimation(PartNames.LEFT_LEG, new AnimationChannel(
AnimationChannel.Targets.ROTATION,
new Keyframe(0, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.2f, KeyframeAnimations.degreeVec(0, 0, 45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.4f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR)
))
.addAnimation(PartNames.RIGHT_LEG, new AnimationChannel(
AnimationChannel.Targets.ROTATION,
new Keyframe(0.5f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.7f, KeyframeAnimations.degreeVec(0, 0, -45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.9f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR)
))
.build();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Тут много важного, поэтому давайте разберем главное:
withLength(1)задает длительность анимации в 1 секунду.looping()делает анимацию циклически повторяющейся.- Далее следует серия вызовов
addAnimation, которые добавляют отдельные анимации для конкретных частей модели. В данном случае у нас есть разные анимации для головы, левой ноги и правой ноги.- Каждая анимация нацелена на определенное свойство этой части модели — в нашем случае мы каждый раз меняем поворот (rotation) части модели.
- Анимация состоит из списка ключевых кадров (keyframes). Когда время анимации (количество прошедших секунд) совпадает с одним из этих ключевых кадров, значение выбранного свойства становится равным значению, указанному для этого кадра (в нашем случае это угол поворота).
- Когда время находится между ключевыми кадрами, значение будет интерполироваться (плавно переходить) между двумя соседними кадрами.
- Мы использовали линейную интерполяцию (linear interpolation) — это самый простой тип, который изменяет значение (в нашем случае поворот части модели) с постоянной скоростью от одного ключевого кадра к другому. В коде ванильной игры (Vanilla) также доступна интерполяция сплайнами Катмулла-Рома (Catmull-Rom spline interpolation), которая обеспечивает более плавный переход между кадрами.
- Вы также можете создавать собственные типы интерполяции.
И наконец, давайте привяжем анимацию к модели:
java
private final KeyframeAnimation dancing;
public MiniGolemEntityModel(ModelPart root) {
// ...
this.dancing = MiniGolemAnimations.DANCING.bake(root);
}
@Override
public void setupAnim(MiniGolemEntityRenderState state) {
super.setupAnim(state);
if (state.dancingAnimationState.isStarted()) {
this.dancing.apply(state.dancingAnimationState, state.ageInTicks);
} else {
// ... the leg swing animation code from before
this.head.xRot = state.xRot * Mth.RAD_TO_DEG;
this.head.yRot = state.yRot * Mth.RAD_TO_DEG;
float limbSwingAmplitude = state.walkAnimationSpeed;
float limbSwingAnimationProgress = state.walkAnimationPos;
this.leftLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f + Mth.PI) * 1.4f * limbSwingAmplitude;
this.rightLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f) * 1.4f * limbSwingAmplitude;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Когда анимация воспроизводится, мы применяем её, в противном случае мы используем старый код анимации ног.
Добавление яйца призыва
Чтобы добавить яйцо спавна для сущности Мини-голема, обратитесь к полной статье «Создание яйца спавна».






