# Custom Actor Implementations

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 are characters, backgrounds, text printers and choice handlers.

Actors are represented by IActor interface and its derivatives:

  • ICharacterActor
  • IBackgroundActor
  • ITextPrinterActor
  • IChoiceHandlerActor

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

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

Default Actor Implementation Actor Implementation

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

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

public ActorImplementationType (string id, ActorMetadata metadata) { }

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

NOTICE

When adding custom implementation types under a non-predefined assembly (via assembly definitions), add the assembly name to the Type Assemblies list found in the engine configuration menu. Otherwise, the engine won't be able to locate your custom types.

EXAMPLE

For a complete example on adding custom actor implementation see Live2D extension project on GitHub. Specifically, the custom "Live2DCharacter" actor is implemented in Runtime/Live2DCharacter.cs script.

# Actor Resources

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

var resource = await resourceLoader.LoadAsync(actorId);

When multiple resources are allowed, specify full path; eg, given you've assigned a resource with "CubeBackground" name:

— to load the resource, use:

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

# Custom Metadata

It's possible to add custom additional data to the actors metadata (of both built-in and custom implementations).

To inject custom data, create a new C# class and inherit it from CustomMetadata<TActor> type, 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" implementation:

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 actor with the associated implementation is selected.

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

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 on adding a property drawer, which will insert an extra label above the edited field.

// 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 used when drawing the affected fields.
// The script should be inside an `Editor` folder, as it uses `UnityEditor` namespace.
[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 the 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, our custom character data will now draw as following:

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 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.

NOTICE

This applies for 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.

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

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

// Our extended state, that serializes the last choice time.
public class MyChoiceHandlerState : ChoiceHandlerState
{
    // This field is serializable and persist 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 UniTask ChangeVisibilityAsync (bool visible, float duration, 
        EasingType easingType = default, CancellationToken cancelToken = default)
    {
        Debug.Log($"Last choice time: {LastChoiceTime}");
        return base.ChangeVisibilityAsync(visible, duration, easingType, cancelToken);
    }
}

// Overriding the 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 UniTask<IChoiceHandlerActor> AddActorAsync (string actorId, 
        ChoiceHandlerState state)
    {
        return base.AddActorAsync(actorId, state as MyChoiceHandlerState);
    }

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

Our 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 the supported serializable data types see Unity's serialization guide.

Last Updated: October 4, 2020