-
Notifications
You must be signed in to change notification settings - Fork 8
Using EntityAnno
Important
This page assumes you have setup a Java mod with EntityAnno integration using my template, and possess some minimal degree of knowledge of Java modding. If you don't, refer to these articles:
If you so choose to use EntityAnno outside of my template, adjust the information from this Wiki according to your needs. You would most likely already have some degree of knowledge anyway if you managed to get EntityAnno working in your own project.
To make this walkthrough more approachable, this Wiki will explain things by giving examples. In particular, the main objective for us today is to create a simple custom unit entity, with legs, that will exit the game once it dies. Yes, it's silly, but it'll certainly be memorable enough to be useful.
Note
This section talks about how entity source files are generated, what the process is, and what the input files and parameters are. Skip to this section if you just want to go ahead and get to coding straight away.
Let's take a quick look at the mindustry.entities.comp package. You'll find that every single class names are suffixed with *Comp. These are our blueprint component classes—they're only used during compile-time, but are effectively gone at runtime. They might as well not exist (and they in fact don't) once the project is bundled into a final .jar file. They do, however, generate interfaces with this naming pattern:
*Comp blueprint class
|
*c component interface
|
|---|---|
class UnitComp |
interface Unitc |
class MechComp |
interface Mechc |
class LegsComp |
interface Legsc |
class BuildingComp |
interface Buildingc |
The blueprints, which will be gone at runtime, are used to generate component interfaces which do exist at runtime. These are the same interfaces used by the Composition by Inheritance pattern described previously. The interface files only contain body-less functions that exist in the blueprints.
These interfaces are also the ones used by the @EntityDef annotation, which when e.g. used like
//mech ... @EntityDef({Unitc.class, Mechc.class}) ...
will take Unitc and Mechc and generate MechUnit. Specifically, MechUnit is a concrete (non-abstract) class that implements Unitc and Mechc interfaces. However, that is not all; the sources from both MechComp and UnitComp blueprints are merged as well. For example, the void approach(Vec2) method, which in UnitComp is defined as
//UnitComp.java public void approach(Vec2 vector){ vel.approachDelta(vector, type.accel * speed()); }
and in MechComp is defined as
//MechComp.java @Override public void approach(Vec2 vector){ //mark walking state when moving in a controlled manner if(!vector.isZero(0.001f)){ walked = true; } }
will be merged in the generated MechUnit source like so
//MechUnit.java, generated
@Override
public void approach(Vec2 vector){
mech: {
if(!vector.isZero(0.001f)){
walked = true;
}
}
unit: {
vel.approachDelta(vector, type.accel * speed());
}
// potentially some more components that we didn't explicitly use, called "component dependencies"
// more on this later
}And that's exactly how entities with unit + mech components are defined.
Note
The usage of the @Override annotation isn't critically important, but it does have its uses here (aside from idiomatic Java OOP), which is basically saying that the actual method is defined by some component dependency. More on this in the next section.
Important
This subsection assumes you use my template, and assumes these placeholder names in the gradle.properties files:
| Property | Value |
|---|---|
modFetch |
mod.fetched |
modGenSrc |
mod.entities.comp |
modGen |
mod.gen |
Referring back to the preamble,
[...] the main objective for us today is to create a simple custom unit entity, with legs, that will exit the game once it dies.
the unit and legs components are already defined in Vanilla, by the Unitc and Legsc interfaces respectively. The exit component is the one that needs to be made from scratch. Do just that—create a file src/mod/entities/comp/ExitComp.java with the contents
package mod.entities.comp;
import arc.*;
import ent.anno.Annotations.*;
import mindustry.gen.*;
@EntityComponent
abstract class ExitComp implements Healthc{
@Override
public void killed(){
Core.app.exit();
}
}and try building the project. If everything went smoothly, your project should compile successfully and there should now be a Exitc interface in the package mod.gen. You can confirm this by navigating to the Exitc file through your IDE, or manually by navigating to the build/generated/sources/annotationProcessor/java/main/mod/gen directory.
There are some important things to note, namely
- the
@EntityComponent, which tells EntityAnno that this class is a blueprint and generateExitc; -
implements Healthc, which tells EntityAnno thatHealthCompis a component dependency; and -
@Override public void killed(), which adds our own behavior hooked to thekilled()method.
The component dependencies part is especially important. This tells the composition pattern that "has exit component" implies "has health component." In inheritance terms, implements Exitc will imply implements Healthc. This is important because oftentimes components only want to add to existing behaviors instead of defining a new behavior; in our case, we want to add a new logic—exiting Mindustry—into an existing "when killed" behavior defined by Healthc.
The @Override annotation makes this distinction clearer, although its meaning changes from the usual "override superclass method" into "merge new logic with existing method from another component."
Note
EntityAnno uses @EntityComponent for blueprints, as opposed to Vanilla's @Component.
Now, let's leverage @EntityDef to generate ExitMechUnit from unit + mech + exit components. You can do this in a lot of ways—the only things that matter are
- a
public static @EntityDef({Unitc.class, Mechc.class, Unitc.class}) UnitTypefield; - setting that field to a new
UnitTypeinstance at content-loading phase; - calling
EntityRegistry.registerUnits().
For example, you can apply these changes (denoted by //new: comments) to your mod class from my template like
package mod;
import ent.anno.Annotations.*; //new: import EntityAnno annotations
import mindutsry.gen.*; //new: import Unitc, Mechc
import mindustry.mod.*;
import mindustry.type.*; //new: import UnitType
import mod.gen.*;
public class YourMod extends Mod{
@EntityDef({Unitc.class, Mechc.class, Exitc.class}) //new: generate ExitMechUnit
public static UnitType exitingUnit; //new: assign to this unit type
@Override
public void loadContent(){
// Call this before loading any content!
EntityRegistry.register();
exitingUnit = new UnitType("exiting-unit"); //new: instantiate the unit type
// Call this *after* loading `UnitType`s!
EntityRegistry.registerUnits();
}
}Important
What EntityRegistry.registerUnits() does in this case is basically equivalent to YourMod.exitingUnit.constructor = ExitMechUnit::create, therefore it is very important to call that method after all units are newly created, otherwise your mod will crash due to null-pointer exceptions.
This is also why there is no constructor-setting code in Vanilla; because the code that does it is generated.
Now, try compiling and installing the mod. My template makes this easy by simply running gradlew install, which will compile the mod and automatically install the mod's .jar file into Mindustry's mods folder. Using a utility mod of your choice, or using the console directly, try spawning the unit and kill it. It should make the game close.
If you've gotten this far, congratulations, you now have all the tools necessary to create custom unit behaviors. However, this is only the surface of what EntityAnno offers to you. The next page is a very technical, in-depth usage guide on every single annotations that EntityAnno defines for you.