Створення вашої першої сутності
Сутності — це динамічні та інтерактивні об'єкти в грі, які не є частиною ландшафту (як-от блоки). Сутності можуть рухатися та взаємодіяти зі світом різними способами. Ось кілька прикладів:
Villager,PigіGoat— це все прикладиMob, найпоширенішого типу сутностей — живих істот.ZombieтаSkeleton— це прикладиMonster, різновидуEntity, який є ворожим доPlayer.MinecartіBoat— це прикладиVehicleEntity, який має спеціальну логіку обробки введення гравця.
Ця стаття проведе вас через процес створення власного мініґолема. Ця сутність матиме кумедні анімації. Вона буде PathfinderMob, який є класом, що використовується більшістю мобів із функцією пошуку шляху, як-от Zombie та Villager.
Підготовка вашої першої сутності
Першим кроком у створенні власної сутності є визначення її класу та реєстрація її в грі.
Ми створимо клас 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
Додавання цілей
Цілі — це система обробки того, що буде прагнути сутність, надаючи їй визначений набір поведінок. Цілі мають певний пріоритет: мети з нижчим значенням пріоритету мають перевагу над метами з вищим значенням пріоритету.
Щоб додати цілей до сутності, вам потрібно створити в класі своєї сутності метод 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
Рендер сутностей завжди обробляються на стороні клієнта. Сервер керує логікою та поведінкою сутності, а клієнт відповідає за показ моделі, текстури та анімацій сутності.
Рендер має декілька кроків, що стосується їх власних класів, але ми почнемо з класу EntityRenderState.
java
public class MiniGolemEntityRenderState extends LivingEntityRenderState {
}1
2
2
Дані, що зберігаються в стані рендера, використовуються для визначення того, як сутність представляється візуально, включаючи стани анімації, як-от поведінка руху чи бездіяльності.
Налаштування моделі
Клас MiniGolemEntityModel визначає, як ваша сутність виглядає, описуючи її форму та частини. Моделі зазвичай створюються у сторонніх інструментах, як-от BlockBench, а не вручну. Проте ця стаття проведе вас через приклад ручного створення, щоб показати, як це працює.
WARNING
BlockBench підтримує кілька мапінгів (як-от мапінги Mojang, Yarn та інші). Переконайтеся, що ви обрали правильні мапінги, які відповідають вашому середовищу розробки — ця стаття використовує мапінги Mojang.
Невідповідні мапінги можуть спричинити помилки під час інтеграції коду, згенерованого BlockBench.
java
public class MiniGolemEntityModel extends EntityModel<MiniGolemEntityRenderState> {
private final ModelPart head;
private final ModelPart leftLeg;
private final ModelPart rightLeg;
//:::dancing_animation
public MiniGolemEntityModel(ModelPart root) {
//:::dancing_animation
super(root);
head = root.getChild(PartNames.HEAD);
leftLeg = root.getChild(PartNames.LEFT_LEG);
rightLeg = root.getChild(PartNames.RIGHT_LEG);
}
//:::dancing_animation1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Клас 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 модель мініґолема, створюючи його тіло, голову та ноги як куби, встановлює їх позицію та мапінги текстури та повертає LayerDefinition для рендера.
Кожна частина додається разом з точкою зміщення, яка є початком координат для всіх перетворень, що застосовуються до цієї частини. Усі інші координати в частині моделі вимірюються відносно цієї точки зміщення.
WARNING
Вищі значення Y у моделі відповідають низу сутності. Це навпаки у порівнянні з ігровими координатами.
Тепер нам потрібно буде створити клас ModEntityModelLayers у клієнтському пакеті. Ця сутність має лише один шар текстури, але інші сутності можуть використовувати декілька – подумайте про вторинний шар скіну на сутностях, таких як 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() {
EntityModelLayerRegistry.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
Створення рендера
Рендер сутності дає змогу бачити її в грі. Ми створимо новий клас, MiniGolemEntityRenderer, який повідомить Minecraft, яку текстуру, модель і стан рендера сутності використовувати для цієї сутності.
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
//:::dancing_animation
@Override
public void setupAnim(MiniGolemEntityRenderState state) {
super.setupAnim(state);
head.xRot = state.xRot * Mth.RAD_TO_DEG;
head.yRot = state.yRot * Mth.RAD_TO_DEG;
float limbSwingAmplitude = state.walkAnimationSpeed;
float limbSwingAnimationProgress = state.walkAnimationPos;
leftLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f + Mth.PI) * 1.4f * limbSwingAmplitude;
rightLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f) * 1.4f * limbSwingAmplitude;
}
//:::dancing_animation
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Для початку застосуйте поворот і нахил до частини моделі голови.
Потім ми застосовуємо анімацію ходьби до частин моделі ніг. Ми використовуємо функцію cos, щоб створити загальний ефект хитання ногою, а потім трансформуємо косинусну хвилю, щоб отримати правильну швидкість і амплітуду хитання.
- Константа
0,2у формулі контролює частоту косинусної хвилі (наскільки швидко гойдаються ноги). Вищі значення призводять до вищої частоти. - Константа
1,4у формулі контролює амплітуду косинусної хвилі (наскільки далеко гойдаються ноги). Вищі значення призводять до вищої амплітуди. - Змінна
limbSwingAmplitudeтакож впливає на амплітуду так само, як константа1.4. Ця змінна змінюється залежно від швидкості сутності, так що ноги розгойдуються більше, коли сутність рухається швидше, і коливаються менше або зовсім не коливаються, коли сутність рухається повільніше або не рухається. - Константа
Mth.PIдля лівої ноги перетворює косинусну хвилю на половину фази так, що ліва нога коливається в протилежному напрямку до правої ноги.
Ви можете нанести їх на графік, щоб побачити, як вони виглядають:


Синя крива — для лівої ноги, а коричнева — для правої. Горизонтальна вісь X представляє час, а вісь Y вказує кут кінцівок ніг.
Не соромтеся пограти з константами на Desmos, щоб побачити, як вони впливають на криву.
Заглянувши в гру, тепер у вас є все, що вам потрібно, щоб створити сутність за допомогою /summon example-mod:mini_golem!

Додавання даних до сутності
Щоб зберегти дані в сутності, звичайним способом є просто додати поле в клас сутності.
Іноді вам потрібно синхронізувати дані сутності між стороною сервера та клієнта. Перегляньте сторінку мережі, щоб дізнатися більше про архітектуру клієнт-сервер. Для цього ми можемо використати синхронізовані дані [sic], визначивши для нього 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 (isDancing()) {
if (dancingTimeLeft-- <= 0) {
setDancing(false);
}
} else {
if (this.random.nextInt(1000) == 0) {
setDancing(true);
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
Як бачите, ми додали метод такту для контролю стану танцю.
Зберігання даних у NBT
Для постійних даних, які можна зберегти після закриття гри, ми замінимо методи addAdditionalSaveData і readAdditionalSaveData в MiniGolemEntity. Ми можемо використовувати це, щоб зберегти кількість часу, що залишився в танцювальній анімації.
java
@Override
protected void addAdditionalSaveData(ValueOutput valueOutput) {
super.addAdditionalSaveData(valueOutput);
valueOutput.putInt("dancing_time_left", dancingTimeLeft);
}
@Override
protected void readAdditionalSaveData(ValueInput valueInput) {
super.readAdditionalSaveData(valueInput);
dancingTimeLeft = valueInput.getInt("dancing_time_left").orElse(0);
setDancing(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
Тепер, коли сутність завантажується, вона відновлюватиме стан, у якому була залишена.
Додавання анімації
Першим кроком до додавання анімації до сутності є додавання стану анімації в клас сутності. Ми створимо стан анімації, який використовуватиметься, щоб змусити сутність танцювати.
java
public final AnimationState dancingAnimationState = new AnimationState();
@Override
public void onSyncedDataUpdated(EntityDataAccessor<?> data) {
super.onSyncedDataUpdated(data);
if (data == DANCING) {
dancingAnimationState.animateWhen(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, що додає окремі анімації, спрямовані на окремі частини моделі. Тут ми маємо різні анімації, націлені на голову, ліву та праву ногу.- Кожна анімація націлена на певну властивість цієї частини моделі, у нашому випадку ми змінюємо обертання частини моделі в кожному випадку.
- Анімація складається зі списку ключових кадрів. Коли час (кількість секунд, що минули) анімації дорівнює одному з цих ключових кадрів, тоді значення цільової властивості дорівнюватиме значенню, яке ми вказали для цього ключового кадру (у нашому випадку обертання).
- Коли час знаходиться між нашими ключовими кадрами, тоді значення буде інтерпольовано (змішано) між двома сусідніми ключовими кадрами.
- Ми використали лінійну інтерполяцію, яка є найпростішою та змінює значення (у нашому випадку обертання частини моделі) з постійною швидкістю від одного ключового кадру до іншого. Стандартна гра також забезпечує сплайн-інтерполяцію Catmull-Rom, яка забезпечує більш плавний перехід між ключовими кадрами.
- Творці модів також можуть створювати власні типи інтерполяції.
Нарешті, приєднаймо анімацію до моделі:
java
private final KeyframeAnimation dancing;
public MiniGolemEntityModel(ModelPart root) {
// ...
this.dancing = MiniGolemAnimations.DANCING.bake(root);
//:::model1
}
@Override
public void setupAnim(MiniGolemEntityRenderState state) {
super.setupAnim(state);
//:::model_animation
if (state.dancingAnimationState.isStarted()) {
this.dancing.apply(state.dancingAnimationState, state.ageInTicks);
} else {
// ... the leg swing animation code from before
}
//:::model_animation
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Під час відтворення анімації ми застосовуємо анімацію, інакше використовуємо старий код анімації ніг.
Додавання яйця виклику
Щоб додати яйце виклику для сутності мініґолема, дивіться повну статтю про створення яйця виклику.




