This guide will explain and demonstrate the components of creating a custom actor from (mostly) scratch using the Tsuru custom code platform.
- A development environment set up with Tsuru and Tachyon
- Knowledge of C++
Class and Profile Registry
Begin by creating a new file in
source/actors/ with the name of your actor in full lowercase and ending with the
.cpp extension. Add this file to the list in
modules/customactors.yaml, this YAML file lists some of the source files to be compiled.
Next, add a new entry at the bottom of the enum in
ProfileID enum contains a list of profiles which are data associated with every actor and are required for them to be spawned. Return to your C++ source file.
Every actor in the game is a class which derives from one of the many actor base classes provided by the game for you to use. They each have their own functionality and use case, however they all have the same base interface for us to use. Some basic ones are:
StageActor-> Basic level actor without physics
PhysicsActor-> Basic level actor with physics (gravity, tile detection, etc)
MultiStateActor-> Basic level actor with multiple states
These are general purpose bases for actors which are intended to be used in stages (levels). There is also higher level classes with more functionality such as
Boss which have a lot more complexity and are for more specific cases.
Create a class and inherit the base class which you believe is the best fit for your actor. The base of all actor classes is
Actor, which contains an interface with virtual functions for actions done at certain periods during the actor's lifecycle:
onCreate-> Called once when the actor is being spawned. Initialize your model and any other values here.
onExecute-> Called once for every frame that actors are supposed to be interactive (as opposed to frozen, such as during the player entering a pipe or the level being paused). Main behavior of the actor goes here.
onDraw-> Called once for every frame regardless of any events such as pipe enters or the level being paused. Render your model here.
onDelete-> Called once when the actor is being despawned. Deallocate and free any allocated memory or handles here.
Each of these also has a
after* counterpart in addition to the main
on* functions, which are called before and after the main ones respectively.
In addition to this interface, the
Actor class provides access to the personal heap provided to the actor (which all allocations by the actor are supposed to be done on), the settings data passed from the stage file (spritedata), and some other miscellaneous data.
Every actor's constructor must take in a
const ActorBuildInfo* parameter, which provides data regarding the actor's spawning. This is usually filled in from the level data, and must be passed to the base class you are inheriting. A blank virtual destructor should also be declared, to override the base one and clean up allocated data correctly.
To register the class as an actor and link it to a profile ID, call the
REGISTER_PROFILE macro below the class declaration, passing the name of the class, and the profile ID you wish to link it to. Additional parameters may also be given to this macro but are not required. Finally, to be able to place your sprite in a level, you must add an array entry for it at the bottom of
source/profile/profile.cpp, and also a sprite tag in the
spritedata.xml of your Miyamoto patch.
From here you may override any of the virtual functions provided by your base class to implement functionality into the actor. Any of the game's APIs are available to use such as hitboxes, sounds, effects, and more. These will be detailed below.
The first thing you may want to do to verify that your actor works is to log something. Logging is also useful for debugging, as there is no publicly available source-level debugger when developing code for Wii U.
Logging functionality is provided by the
include/log.h header, through the
Colored text is also available by printing one of the values in the
LogColor enum, which will cause all following data to be printed in that color until the macro is completed or a
LogColor::Reset is printed.
- WARNING: Log colors at plain text level look like simply random blobs of gibberish, to actually visualize them you must view your logs through a terminal which supports ANSI color codes. Despite this, logs as plain text with log colors are still perfectly readable should you just disregard the raw color codes printed, so nothing is lost, just something to keep in mind.
Additionally, you may format numbers as hexadecimal by passing the
fmt::hex value, which will print the next number in hexadecimal.
If you try to print something and receive a linker error about a
tprint symbol when compiling, it means you attempted to print a value of a type which is not supported to be logged directly, likely a struct or array of some sort.
Most actors in the game that the player interacts with contain code for rendering a model to display the current state of the actor. The majority of these models are located in the
./content/Common/actor folder of the game's filesystem, stored in BFRES format within an SZS (compressed SARC) archive.
To register a model to be loaded for your actor to use, call the
PROFILE_RESOURCES macro below the
REGISTER_PROFILE call and pass your actor's profile ID,
Profile::LoadResourcesAt::Course, and then a comma-separated list of string arguments of the names of the SZS archives containing your desired models, up to a maximum of 64.
Next, to add a model to the actor itself, include the header at
include/game/graphics/model/modelnw.h and add a
ModelWrapper* member to your class. Then, initialize it in the
onCreate function of the actor by assigning to it the return value of a
ModelWrapper::create call, passing the names of the desired model archive and the specific model within the archive. Optionally, you may also provide how many animations are to be allocated from the archive, by passing a number for each respective animation type parameter.
To update the model's transformations, create an
Mtx34 variable in
onExecute, and call
makeRTIdx on it, passing the rotation and position that you desire, usually just the ones from your actor. Next, call
setScale on the model, passing the mtx and scale to the functions respectively.
updateModel, and optionally
updateAnimations if animations are to be used on this model. To draw the model, simply call the
draw method of the model in
onDraw. To play animations on the model, simply call the respective function for it and pass the name of the animation to be played. You may also access or modify parameters related to how the animations are played through the first index of the model's respective animation array's
Hitboxes and Collision
There are two distinct collision systems in the game. One is for hitboxes and is used mainly for cases such as detecting when a player has entered an enemy's damage zone, in order to deal the damage. The other one is for physics, such as solid collision in walls and floors. The former system will be referred to as red collsion, with the latter being referred to as blue collision, since these are the colors displayed in Tsuru's collision viewer.
This system is interacted with through the
HitboxCollider class, which can be a rectangle, circle, or trapezoid shape, but cannot be rotated. Collision is detected when two
HitboxCollider's come into contact, and two bitfields in each are used to opt-in to which types of collisions are to be checked for. In addition to the shape and collision bitfields, there is also a callback function pointer which is called once an appropriate collision is detected, and it is passed two
HitboxCollider* parameters, one for the current hitbox and one for the colliding hitbox. Since the callback function provided to the hitbox is a static/free function,
HitboxCollider also contains a pointer to the parent actor, which may be used to test if it is colliding with a specific actor, such as by querying the profile ID or the RTTI (
RuntimeTypeInfo) data. These three pieces of data are stored in a struct (
HitboxCollider::Info) which is passed to the
HitboxCollider during initialization using the
init method on it. Once a
HitboxCollider has been initialized, it now needs to be added to the global list of
HitboxColliders, which is containted in the
HitboxColliderMgr singleton, and may be added using the
safeAddToCreateList helper method or the
addHitboxColliders method in
StageActor. The latter method is preferred, however, it is important to note that it only adds the hitboxes provided by the base class, so if you add any additional hitboxes, you must override the function to add the hitboxes to
HitboxColliderMgr manually using the
safeAddToCreateList function. Lastly, the
StageActor class provides a single
HitboxCollider instance already, so there is no need to add your own to your class, unless you require more than one in which case you may add additional ones to the class to meet your needs.
The blue collision system is comprised of three parts: tiles, sensors, and colliders. Tiles are placed in the level, and compose the terrain in which actors are placed on. Sensors are lines which are able to detect tiles and colliders, and are useful for cases such as stopping movement or turning when a wall or edge has been met. Colliders are shapes which sensors interact with, and can be used as dynamic terrain/ground such as platforms, and they also have extra functionality compared to
HitboxColliders such as rotation, slipperiness, and having liquid physics within the bounds.
Sensors are handled by the
ActorPhysicsMgr instance bundled with
PhysicsActor, and there can be a maximum of 4, with the side sensors being duplicates of the same one. To add sensors to your actor, create a
PhysicsMgr::Sensor instance for the sensors you would like to add (below, above, side) and fill in the data. As sensors are lines, there is only three values to provide. Two points are connected, so you pass the radius of each point, and then an offset for the distance of the entire sensor from the center of the actor. Call the
init method on the physics manager instance, and pass your sensors. You may pass
nullptr for sensors that you don't want to include. Make sure to call
processCollisions on the physics manager as well during execution to detect for collision once the sensors touch a solid surface. From here you may use the
Left methods to for example, turn a walking actor once a wall is reached.
Blue colliders are far more flexible than
HitboxColliders, but also come at the cost of increased complexity. For example, blue colliders can be any shape, such as polygons and polylines. They consist of an array of points which are connected with lines, and these lines apply the collision. Both the
PolylineCollider classes inherit from
ColliderBase, and you may use them directly however there are some specialized classes such as
CircularCollider which can be used instead to make rectangular and circular colliders respectively more easily.
To add a rectangular or circular collider, include the respective header from
here and add a
CircularCollider member to your actor's class. Create a
PolygonCollider::Info object and fill it in with a description of your desired collider. You may pass zeroes for the second and third parameters. Call
init on the collider and pass a pointer to the info object.
To add a collider with a custom shape, use the
PolylineCollider class. Polyline colliders are mainly used for semisolid platforms as the lines are only solid on one side. Create an array of
Vec2f points as your vertices, making sure that they are relative to the position of the actor, and create
PolylineCollider::Info object. Pass it the desired offset, two zeroes, the points array, and your desired rotation. Next, call
init on the collider and make sure to give it the number of vertices in your array.
Regardless of which shape your collider is, once it is initialized you must add it to the
ColliderMgr global list. Simply use the
add method and pass a pointer to your collider. Before doing this however, you can modify the collision type of it using the set methods for type, solidityType, and surfaceType.
Now that your collider is fully loaded and ready, you must call
execute on it during execution, and it will behave as expected.