Skip to content

Custom Actor Implementations

An actor is a scene entity defined by a name, appearance, visibility, and transform (position, rotation, and scale). It can asynchronously change appearance, visibility, and transform over time. Examples of actors include characters, backgrounds, text printers, and choice handlers.

Actors are represented by the IActor interface and its derivatives:

  • ICharacterActor
  • IBackgroundActor
  • ITextPrinterActor
  • IChoiceHandlerActor

Each actor interface can have multiple implementations; e.g., character actors currently have seven built-in implementations: sprite, diced sprite, generic, layered, narrator, Spine, and Live2D.

Actor implementation can be selected in the configuration managers accessible via Naninovel -> Configuration context menu. You can change the default implementation used for all actors or set a specific implementation per actor. To change the default implementation, use the Default Metadata property; to set specific ones, use the Implementation drop-down in the actor's configuration.

cover
cover

The Implementation drop-down contains all the types that implement the specific actor interface. You can add your own custom implementations, and they'll also appear in the list. See the Naninovel/Runtime/Actor scripts for reference when creating your own actor implementation. Consider using the built-in abstract MonoBehaviourActor implementation to fulfill most base interface requirements when the actor is supposed to be spawned in the scene.

When creating custom actor implementations, make sure they have a compatible public constructor:

csharp
public ActorImplementationType (string id, ActorMetadata metadata) { }

— where id is the ID of the actor and metadata is either the actor's metadata (when an actor record exists in the resources) or a default metadata. When implementing a specific actor interface, it's possible to request corresponding specific metadata (e.g., CharacterMetadata for ICharacterActor implementations).

EXAMPLE

All the built-in actor implementations are authored on top of the same actor APIs, so you can use them as a reference when adding your own. Find the sources at the Runtime/Actor directory of the Naninovel package.

Actor Resources

Apply the ActorResources attribute to the implementation type to specify which assets can be used as resources for your custom actor and whether it's allowed to assign multiple resources in the editor menus. When multiple resources are not allowed (the default), you can load the single available resource by specifying just the actor ID, e.g.:

csharp
var resource = await resourceLoader.Load(actorId);

When multiple resources are allowed, specify the full path; e.g., given you've assigned a resource with the name CubeBackground:

cover

—to load the resource, use:

csharp
var resource = await resourceLoader.Load($"{actorId}/CubeBackground");

Custom Metadata

It's possible to add custom additional data to actor metadata (for both built-in and custom implementations).

To inject custom data, create a new C# class and inherit from CustomMetadata<TActor>, where TActor is the type of the actor implementation the data should be associated with. Below is an example of adding custom data to the characters of CustomCharacterImplementation:

csharp
using Naninovel;
using UnityEngine;

public class MyCharacterData : CustomMetadata<CustomCharacterImplementation>
{
    public int MyCustomInt;
    [Range(0f, 100f), Tooltip("Tooltip for my custom range.")]
    public float MyCustomRange = 50f;
    [ColorUsage(false, true)]
    public Color MyCustomColor = Color.white;
}

Serializable fields of the created custom data class will be automatically exposed in the Naninovel editor menus when an actor with the associated implementation is selected.

cover

To access the custom data at runtime, use GetCustomData<TData>() method of an ActorMetadata instance, where TData is the type of the custom data class, e.g.:

csharp
var charsConfig = Engine.GetConfiguration<CharactersConfiguration>();
var myCharMeta = charsConfig.GetMetadataOrDefault("CharId");
var myCharData = myCharMeta.GetCustomData<MyCharacterData>();
Debug.Log(myCharData.MyCustomInt);

Custom Metadata Editor

It's possible to customize the custom metadata editor via property drawers ↗. Below is an example of adding a property drawer that inserts an extra label above the edited field.

csharp
// Create an attribute to apply to the serialized fields;
// don't forget to inherit it from `PropertyAttribute`.
public class ExtraLabelAttribute : PropertyAttribute
{
    public readonly string LabelText;

    public ExtraLabelAttribute (string labelText)
    {
        LabelText = labelText;
    }
}

// Create the custom editor that will be used when drawing the affected fields.
// The script should be inside an `Editor` folder, as it uses the `UnityEditor` API.
[CustomPropertyDrawer(typeof(ExtraLabelAttribute))]
public class ExtraLabelPropertyDrawer : PropertyDrawer
{
    public override void OnGUI (Rect rect, SerializedProperty prop, GUIContent label)
    {
        var extraLabelAttribute = attribute as ExtraLabelAttribute;

        rect.height = EditorGUIUtility.singleLineHeight;
        EditorGUI.LabelField(rect, extraLabelAttribute.LabelText);

        rect.y += EditorGUIUtility.singleLineHeight +
                  EditorGUIUtility.standardVerticalSpacing;
        EditorGUI.PropertyField(rect, prop);
    }

    public override float GetPropertyHeight (SerializedProperty prop, GUIContent label)
    {
        return EditorGUIUtility.singleLineHeight * 2 +
               EditorGUIUtility.standardVerticalSpacing;
    }
}

// Now you can use the attribute to apply extra label to the serialized fields.
public class MyCharacterData : CustomMetadata<CustomCharacterImplementation>
{
    [ExtraLabel("Text from my custom property drawer")]
    public string MyCustomProperty;
}

Given the above implementation, the custom character data will now draw as follows:

cover

TIP

It's also possible to override built-in configuration editors as a whole; see custom configuration guide for more information and examples.

Custom State

To override or extend the state type for your custom actor, you'll have to also override the actor's manager, as the state is serialized and applied to the managed actors there.

NOTE

This applies to custom actor implementations of one of the built-in IActor interface derivatives (characters, backgrounds, text printers, and choice handlers); if you've inherited your custom actor directly from IActor, there's no need to override the built-in managers to use a custom state — just create your own.

If you're looking to add a custom state for other systems (e.g., UIs, game objects, or components for various game mechanics outside of Naninovel), see the state management guide.

Below is an example of extending choice handler state by adding a LastChoiceTime field, which stores the time of the last added choice. The time is printed to the console when the custom choice handler is shown.

csharp
// Our extended state that serializes the last choice time.
public class MyChoiceHandlerState : ChoiceHandlerState
{
    // This field is serializable and persists through game save-loads.
    public string LastChoiceTime;

    // This method is invoked when saving the game; get the required data
    // from the actor and store it with serializable fields.
    public override void OverwriteFromActor (IChoiceHandlerActor actor)
    {
        base.OverwriteFromActor(actor);
        if (actor is MyCustomChoiceHandler myCustomChoiceHandler)
            LastChoiceTime = myCustomChoiceHandler.LastChoiceTime;
    }

    // This method is invoked when loading the game;
    // get the serialized data back and apply it to the actor.
    public override void ApplyToActor (IChoiceHandlerActor actor)
    {
        base.ApplyToActor(actor);
        if (actor is MyCustomChoiceHandler myCustomChoiceHandler)
            myCustomChoiceHandler.LastChoiceTime = LastChoiceTime;
    }
}

// Our custom choice handler implementation that uses the last choice time.
public class MyCustomChoiceHandler : UIChoiceHandler
{
    public string LastChoiceTime { get; set; }

    public MyCustomChoiceHandler (string id, ChoiceHandlerMetadata metadata)
        : base(id, metadata) { }

    public override void AddChoice (ChoiceState choice)
    {
        base.AddChoice(choice);
        LastChoiceTime = DateTime.Now.ToShortTimeString();
    }

    public override Awaitable ChangeVisibility (bool visible, float duration,
        EasingType easingType = default, AsyncToken token = default)
    {
        Debug.Log($"Last choice time: {LastChoiceTime}");
        return base.ChangeVisibility(visible, duration, easingType, token);
    }
}

// Overriding built-in choice handler manager to make it use our extended state.
// The important step is to specify `MyChoiceHandlerState` in the generic types;
// other modifications are just to fulfill the interface requirements.
[InitializeAtRuntime(@override: typeof(ChoiceHandlerManager))]
public class MyChoiceHandlerManager : ActorManager<IChoiceHandlerActor,
    MyChoiceHandlerState, ChoiceHandlerMetadata,
    ChoiceHandlersConfiguration>, IChoiceHandlerManager
{
    public MyChoiceHandlerManager (ChoiceHandlersConfiguration config)
        : base(config) { }

    public Awaitable<IChoiceHandlerActor> AddActor (string actorId,
        ChoiceHandlerState state)
    {
        return base.AddActor(actorId, state as MyChoiceHandlerState);
    }

    ChoiceHandlerState IActorManager<IChoiceHandlerActor,
        ChoiceHandlerState, ChoiceHandlerMetadata,
        ChoiceHandlersConfiguration>.GetActorState (string actorId)
    {
        return base.GetActorState(actorId);
    }
}

The custom choice handler will now keep the last added choice time and log it in the console, even if the last choice was added in a previous game session loaded from a save slot. You can store any amount of custom data in addition to the built-in actor state this way. For supported serializable data types, see Unity's serialization guide ↗.