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:
ICharacterActorIBackgroundActorITextPrinterActorIChoiceHandlerActor
Each actor interface can have multiple implementations; eg character actors currently have eleven built-in implementations: placeholder, sprite, diced sprite, universal, layered, generic, video, Live2D, Spine, narrator and transient.
An actor implementation can be selected in the configuration managers accessible via the Naninovel -> Configuration editor 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 dropdown in the actor's configuration.
The Implementation dropdown 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:
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 the default metadata. When implementing a specific actor interface, it's possible to request corresponding specific metadata (eg, 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 in 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, eg:
var resource = await resourceLoader.Load(actorId);When multiple resources are allowed, specify the full path; eg given you've assigned a resource with the name CubeBackground:
— to load the resource, use:
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:
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.
To access the custom data at runtime, use the GetCustomData<TData>() method of an 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 of adding a property drawer that inserts 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 be used when drawing the affected fields.
// Keep the script in 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 the attribute can apply an 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:
TIP
It's also possible to override built-in configuration editors as a whole; see the 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 manager with the custom state type.
If 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 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.
// 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 async Awaitable ApplyToActor (IChoiceHandlerActor actor)
{
await 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 meta,
EmbeddedAppearanceLoader<GameObject> loader) : base(id, meta, loader) { }
public override void AddChoice (Choice choice)
{
base.AddChoice(choice);
LastChoiceTime = DateTime.Now.ToShortTimeString();
}
public override Awaitable ChangeVisibility (bool visible, Tween tween,
AsyncToken token = default)
{
Debug.Log($"Last choice time: {LastChoiceTime}");
return base.ChangeVisibility(visible, tween, token);
}
}
// Override the built-in choice handler manager to use our extended state.
// The key 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.