Getting Started With Your First Mod with NeoForge

PART 01#
Getting Started With Your First Mod#
By the end of this part you'll have a NeoForge mod that compiles, launches inside Minecraft, and loads GeckoLib, the animation engine we'll use in Part 2. We won't add content yet; the goal is a clean, running project you can build on.
What you need before you start#
JDK 25: NeoForge for Minecraft 26.1 requires the Java 25 Development Kit. Grab a build from Eclipse Temurin or Microsoft's OpenJDK, and make sure it's a 64-bit JVM (Minecraft does not support 32-bit).
An IDE: IntelliJ IDEA (Community is free) or Eclipse. IntelliJ is the most common choice for Minecraft modding.
A copy of Minecraft and about 30 minutes.
Why these versions?
Minecraft, NeoForge, and GeckoLib versions are tightly coupled; a mismatched trio simply won't resolve. The set in the header (MC 26.1.2 / NeoForge 26.1.2.80 / GeckoLib 5.5.2 / JDK 25) is a known-good combination. Always pick a GeckoLib build that names your exact Minecraft version.
Step 1: Generate the project#
The fastest way to start is the official NeoForge Mod Generator. It stamps out a ready-to-build project (the "MDK", or Mod Developer Kit) with your names filled in.
Open the NeoForge Mod Generator.
Enter a mod name (
First Mod), a mod ID (firstmod, lowercase, no spaces), and a package (com.example.firstmod).Pick Minecraft
26.1.2and the ModDevGradle plugin (the simpler of the two build setups).Download the ZIP and unpack it, then open the folder in your IDE. It will run an initial Gradle sync; let it finish.
JDK, not JRE
If Gradle complains about the Java version, point your IDE's Gradle JVM at the JDK 25 you installed (IntelliJ: Settings › Build Tools › Gradle › Gradle JVM). A regular Java runtime (JRE) or a newer JDK than Gradle supports will fail the sync.
Step 2: Tour the files that matter#
The generated project has a lot of files, but only three touch what your mod is. Here's the trimmed layout:
firstmod/
├─ gradle.properties # mod id, name, versions
├─ build.gradle # dependencies & run configs
└─ src/main/
├─ java/com/example/firstmod/
│ └─ FirstMod.java # the main mod class (@Mod)
└─ resources/
├─ META-INF/
│ └─ neoforge.mods.toml # mod metadata & dependencies
└─ assets/firstmod/ # textures, models, lang, ...
gradle.properties is where the version numbers and your mod's identity live. Confirm these lines:
gradle.propertiesproperties
minecraft_version=26.1.2
neo_version=26.1.2.80
mod_id=firstmod
mod_name=First Mod
mod_group_id=com.example.firstmodneoforge.mods.toml is your mod's ID card: its display name, description, and which mods it depends on. The generator fills most of it in; we'll add one dependency in a moment.
FirstMod.java is the entry point. The @Mod annotation ties the class to your mod ID, and NeoForge hands its constructor the event bus you register things on:
FirstMod.javajava
package com.example.firstmod;
// imports omitted for brevity
@Mod(FirstMod.MODID)
public final class FirstMod {
public static final String MODID = "firstmod";
public FirstMod(IEventBus modBus, ModContainer container) {
// We'll register our mob here in Part 2.
}
}Read more
The official docs explain each file in depth: Getting Started and Mod Files.
Step 3: Add GeckoLib#
GeckoLib is the animation engine that plays keyframed animations on your mob. It's a separate, required dependency: you declare it, you don't bundle it. Two edits.
First, add GeckoLib's Maven repository and the dependency in build.gradle:
build.gradlegradle
repositories {
// GeckoLib 5 (group com.geckolib) lives on Cloudsmith
maven { url = 'https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/' }
mavenCentral()
}
dependencies {
implementation "com.geckolib:geckolib-neoforge-${minecraft_version}:5.5.2"
}Group changed in GeckoLib 5
GeckoLib 5 moved its Maven group to com.geckolib (version 4 used software.bernie.geckolib), and the Java import packages are now com.geckolib.* to match. Old tutorials will show the previous names; use the new ones.
Second, tell NeoForge your mod needs GeckoLib at runtime. Add this block to neoforge.mods.toml:
neoforge.mods.tomltoml
[[dependencies.firstmod]]
modId="geckolib"
type="required"
versionRange="[5.5.2,)"
ordering="AFTER"
side="BOTH"Re-sync Gradle so it downloads GeckoLib.
Step 4: Run it#
From your IDE's Gradle panel run the runClient task, or from a terminal:
terminalbash
./gradlew runClientMinecraft launches. Open Mods from the title screen and you should see First Mod and GeckoLib in the list. That's a working mod. Now let's put something in it.
PART 02
Adding Your First Mob#
Now the fun part. We'll build a gazelle: model and animate it in Blockbench, wire up a GeckoLib entity in Java, register it, and spawn it in the world. The same recipe works for any mob.
Step 1: Model and animate in Blockbench#
Blockbench is a free 3D model editor. Install it, then add the GeckoLib format:
In Blockbench, open File › Plugins, search GeckoLib Animation Utils, and install it.
Create a new model with File › New › GeckoLib Animated Model.
Build your gazelle out of cubes, grouped into bones (a bone named
head, one perleg, etc.). Bones are what animations rotate.Paint or generate a texture, then switch to the Animate tab and create two looping animations named exactly
idleandwalk. The names matter; we reference them from Java.
Export three files (all named after your mob's ID, gazelle):
Geometry to
gazelle.geo.json(the model)Animations to
gazelle.animation.jsonTexture to
gazelle.png
Read more
GeckoLib's own guide walks through modeling, bones, and exporting: the GeckoLib Wiki (choose the GeckoLib 5 section).
Step 2: Drop the assets in the right folders#
GeckoLib finds these files by convention, based on the entity's registered name. Put them here exactly:
src/main/resources/assets/firstmod/
├─ geckolib/
│ ├─ models/entity/gazelle.geo.json
│ └─ animations/entity/gazelle.animation.json
└─ textures/entity/gazelle.png
Names must line up
The file names, the animation names (idle/walk), and the registry name you'll pick in Step 4 all have to agree. A typo here is the #1 reason a mob spawns invisible or T-poses.
Step 3: Write the entity class#
The entity is a normal Minecraft Animal that also implements GeoEntity. GeckoLib needs two things from you: an animation cache, and a set of controllers that decide which animation plays each tick. Here's a complete, minimal gazelle:
entity/Gazelle.javajava
package com.example.firstmod.entity;
import com.geckolib.animatable.GeoEntity;
import com.geckolib.animatable.instance.AnimatableInstanceCache;
import com.geckolib.animatable.manager.AnimatableManager;
import com.geckolib.animation.AnimationController;
import com.geckolib.animation.RawAnimation;
import com.geckolib.util.GeckoLibUtil;
// vanilla imports (Animal, Attributes, goals, ...) omitted for brevity
public class Gazelle extends Animal implements GeoEntity {
// Animation names must match gazelle.animation.json
private static final RawAnimation IDLE = RawAnimation.begin().thenLoop("idle");
private static final RawAnimation WALK = RawAnimation.begin().thenLoop("walk");
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
public Gazelle(EntityType<? extends Gazelle> type, Level level) {
super(type, level);
}
// Health, speed, etc. Registered in ModEntities (next step).
public static AttributeSupplier.Builder createAttributes() {
return Animal.createAnimalAttributes()
.add(Attributes.MAX_HEALTH, 8.0D)
.add(Attributes.MOVEMENT_SPEED, 0.30D);
}
// Simple vanilla-style behaviour: float, flee, wander, look around.
@Override
protected void registerGoals() {
this.goalSelector.addGoal(0, new FloatGoal(this));
this.goalSelector.addGoal(1, new PanicGoal(this, 1.5D));
this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D));
this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 6.0F));
this.goalSelector.addGoal(4, new RandomLookAroundGoal(this));
}
// One controller: play WALK when moving, otherwise IDLE.
@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>("movement", 5, test ->
test.isMoving() ? test.setAndContinue(WALK) : test.setAndContinue(IDLE)));
}
@Override
public AnimatableInstanceCache getAnimatableInstanceCache() {
return this.cache;
}
@Override
public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob partner) {
return ModEntities.GAZELLE.get().create(level, EntitySpawnReason.BREEDING);
}
}What a controller does
Each tick GeckoLib calls your controller's handler. test.isMoving() tells you if the mob is walking; setAndContinue(...) smoothly transitions into that animation. That single line is the whole idle/walk loop.
Step 4: Register the entity type#
Registration tells Minecraft the entity exists. Use a DeferredRegister, give it a name (gazelle, the name everything else keys off), a hitbox size, and its attributes.
registry/ModEntities.javajava
package com.example.firstmod.registry;
// imports omitted for brevity
public final class ModEntities {
public static final DeferredRegister.Entities ENTITIES =
DeferredRegister.createEntities(FirstMod.MODID);
public static final DeferredHolder<EntityType<?>, EntityType<Gazelle>> GAZELLE =
ENTITIES.registerEntityType("gazelle", Gazelle::new,
MobCategory.CREATURE, builder -> builder.sized(0.6F, 1.3F));
// Called from the mod bus. LivingEntities crash without attributes.
public static void registerAttributes(EntityAttributeCreationEvent event) {
event.put(GAZELLE.get(), Gazelle.createAttributes().build());
}
}Now wire both into the main class constructor:
FirstMod.javajava
public FirstMod(IEventBus modBus, ModContainer container) {
ModEntities.ENTITIES.register(modBus);
modBus.addListener(EntityAttributeCreationEvent.class, ModEntities::registerAttributes);
}Read more
NeoForge docs: Registries and Entities.
Step 5: Register the renderer (client side)#
The entity now exists on the server, but nothing draws it. GeckoLib's GeoEntityRenderer auto-loads your model, animation, and texture from the gazelle name; you just subclass it and register it. Rendering is client-only, so this goes in a separate dist = Dist.CLIENT class.
client/GazelleRenderer.javajava
package com.example.firstmod.client;
import com.geckolib.renderer.GeoEntityRenderer;
import com.geckolib.renderer.base.GeoRenderState;
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
// ...
public class GazelleRenderer<R extends LivingEntityRenderState & GeoRenderState>
extends GeoEntityRenderer<Gazelle, R> {
public GazelleRenderer(EntityRendererProvider.Context context) {
super(context, ModEntities.GAZELLE.get()); // auto-resolves geo/anim/texture
}
}client/FirstModClient.javajava
@Mod(value = FirstMod.MODID, dist = Dist.CLIENT)
public final class FirstModClient {
public FirstModClient(IEventBus modBus, ModContainer container) {
modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, e ->
e.registerEntityRenderer(ModEntities.GAZELLE.get(),
context -> new GazelleRenderer<>(context)));
}
}Step 6: Give it a name and a spawn egg#
Two small touches so you can actually get one in-world and see a proper name. First, register a spawn egg item and add it to the vanilla Spawn Eggs tab:
registry/ModItems.javajava
public static final DeferredRegister.Items ITEMS =
DeferredRegister.createItems(FirstMod.MODID);
public static final DeferredItem<SpawnEggItem> GAZELLE_SPAWN_EGG =
ITEMS.registerItem("gazelle_spawn_egg",
props -> new SpawnEggItem(props.spawnEgg(ModEntities.GAZELLE.get())));Register ITEMS on the mod bus (next to ENTITIES.register(modBus)), then surface the egg in the creative menu:
FirstMod.java (constructor)java
ModItems.ITEMS.register(modBus);
modBus.addListener(BuildCreativeModeTabContentsEvent.class, e -> {
if (e.getTabKey() == CreativeModeTabs.SPAWN_EGGS)
e.accept(ModItems.GAZELLE_SPAWN_EGG);
});Finally, give the mob and egg readable names in a language file:
assets/firstmod/lang/en_us.jsonjson
{
"entity.firstmod.gazelle": "Gazelle",
"item.firstmod.gazelle_spawn_egg": "Gazelle Spawn Egg"
}Step 7: Run and test#
Launch the client again:
terminalbash
./gradlew runClientCreate a Creative world.
Find the Gazelle Spawn Egg in the Spawn Eggs tab and place a gazelle, or just run
/summon firstmod:gazelle.Watch it: standing still it plays
idle; walk it around (push it, or let it wander) and it switches towalk.
It worked if...
You see your textured model, it's named "Gazelle", and the animation changes between standing and moving. That's a fully custom, animated mob. Congratulations.
Troubleshooting
Invisible or T-posing? A file name or animation name doesn't match gazelle/idle/walk. Crash on spawn? You forgot to register attributes (Step 4). Name shows as entity.firstmod.gazelle? The lang file is missing or misnamed.
Where to go next#
You now have the full loop: scaffold, model, animate, register, render, test. From here, one small addition at a time:
Natural spawning: register a spawn placement and a biome modifier so gazelles appear in the wild.
More animations: add
run,attack, or a one-shoteatusing a second, triggered controller.Breeding & babies: add a
BreedGoalandTemptGoal, and render juveniles smaller.Sounds: register ambient, hurt, and death sounds.
Bookmark these
The three references you'll use most:

0 comments
No comments yet. Be the first to say something.