Custom Actors

From Zenith
Jump to navigation Jump to search


This guide will explain and demonstrate the components of creating a custom actor from (mostly) scratch using the Tsuru custom code platform.

Requirements

  • A development environment set up with Tsuru and Tachyon
  • Knowledge of C++

Steps

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 include/game/profile/profileid.h. The 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 Enemy and 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[1] 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[1] 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 before* and 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.

Logging

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 PRINT macro. Each identifier to be printed may be passed as a parameter (comma-separated list) and they will be concatenated and printed to the console.

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.

Models

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 setMtx and setScale on the model, passing the mtx and scale to the functions respectively.

Finally, call 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 frameCtrl member.

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.

Red Collision

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.

Blue Collision

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

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 isOnGround or isCollidedRight/Left methods to for example, turn a walking actor once a wall is reached.

Colliders

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 PolygonCollider and PolylineCollider classes inherit from ColliderBase, and you may use them directly however there are some specialized classes such as RectCollider and 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 RectCollider or 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 PolygonCollider or 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.

  1. 1.0 1.1 60 times a second, since NSMBU is a 60 FPS locked game