Custom Mapper Types
The different element types displayed in the block mapper (and for entities,
GenericDataHolders etc.) are called mapper types.
Using the CustomMapperTypes API it is possible to add new mapper types to the game.
To add a mapper type two elements are required: The type itself (extending MCustom,
analogous to MKey, MToggle, etc.) and a CustomSelector that handles displaying the
type in the block mapper.
After registering them, custom mapper types can be added to any SaveableDataHolder
(Blocks, Entities, GenericDataHolder) using the AddCustom method.
Adding a mapper type is a somewhat complex topic and creating the interface properly can require a lot of work depending on the interface. It is not recommended for beginners or when it only provides marginal benefits over using a combination of the default mapper types.
There is a step-by-step guide to creating a mapper type available as well.
Creating a MapperType
The base class for all custom mapper types is MCustom<T>. The type parameter indicates
what kind of value is edited by the mapper type, this can be a primitive type (like string
or int) or a more complex type.
A mapper type contains three values: The default value, the current value and a "load value". The load value is basically used for the undo system and is normally handled automatically.
There are three things required in a class extending MCustom<T>: A constructor,
SerializeValue, and DeSerializeValue.
Required overrides
The constructor usually has a signature similar to the the default mapper types:
string displayName, string key, T defaultValue. Whether or not this structure is followed
exactly, it is required to call the base constructor with the same signature.
The XData SerializeValue(T value) method is used to specify how a value of type T should
be serialized into an XData object which is what is used for saving.
The key of the returned XData should be the SerializationKey property, not the Key
property!
Similarly, T DeSerializeValue(XData data) is used to deserialize an XData that was
serialized by the SerializeValue method.
The easiest way of converting to and from an XData object is to convert whatever type is
being edited to a primitive type supported by the XData system (if it is not already) and
then convert to XString, XInteger, etc. (Note that XStringArray, etc. are also supported
which may make serialization easier for more complex types.)
Both of these methods may be called from the base class constructor, before the constructor of the subclass is called. Make sure these methods are written such that they can handle this.
Overriding these methods is sufficient for value types (i.e. the value is changed by assigning a new one, not by changing fields of the value object). If the underlying type is not a value type, it is likely necessary to override some of the optional override below as well.
Optional overrides
The Serialize(), SerializeDefault(), SerializeLoadValue(), and DeSerialize(XData)
methods are automatically implemented using the SerializeValue and DeSerializeValue
methods. They can be overridden manually if necessary, but the default implementations should
normally suffice.
The MCustom base class provides a default Changed event, which is always invoked from
InvokeChanged. If desired, a subclass can add another event with a more appropriate signature.
This should then also be called from InvokeChanged, but the base method has to be called
from the override!
ResetDefaults should reset the value back to the default specified in the constructor.
The default implementation does this by assigning defaultValue to value which is likely
not correct for non-value types.
ResetValue and ApplyValue set value to loadValue or loadValue to value respectively.
ApplyValue should also call InvokeChanged with the new value. The same reservations as
for ResetDefaults apply.
If the underlying type is not a value type, the constructor should also do some additional
work: The default constructor will set value = loadValue = defaultValue which is only
correct for value types. If a reference type is used, the subclass needs to correctly create
copies of the default value after the base constructor has run.
The isDefaultValue property has a default implementation comparing Value and defaultValue
using the .Equals() method. If this is not appropriate or there is a more suitable comparison
available, this can be overridden.
Creating the Selector
Each mapper type has an associated Selector class handling the interface in the block
mapper. Custom selectors are created by extending the CustomSelector<T, TMapper> class,
where T is the same type parameter as in MCustom<T> and TMapper is the mapper type
(derived from MCustom) that this selector is associated to.
To define a selector, override CreateInterface and UpdateInterface.
Creating an interface
Writing code to create the interface can be somewhat bothersome because it requires correctly
positioning and creating UI elements from code. The block mapper uses a UI system based
on complete GameObjects instead of either the legacy Unity GUI system or the newer
Canvas-based system.
The CustomSelector class provides some helpers to assist creating the UI by using the
Elements property which has methods like MakeText or MakeBox to create elements
and AddButton or ScaleOnMouse to add behaviour to elements that is consistent with the
other game UI.
All UI objects should be put beneath the Content game object in the hierarchy. This is
done automatically when using the helper methods from Elements.
Also in the interest of consistency, the Materials property provides access to materials
used elsewhere in the block mapper which make it possible to create UI that fits in to the
block mapper.
When creating an interface, also make sure to scale the Background element correctly.
It determines the complete size of the selector interface, so it must have the correct size
to ensure other elements don't overlap. The background scale should ideally be set as the
first step in the interface creation, otherwise elements could appear offset.
The coordinate system in the interface is based directly on how Unity handles the elements: This means that (0, 0) is the middle point of the interface and the extent of the coordinate system is determined by the background scale.
Note that the positions given to the Elements helper methods also specify the middle point
of elements, so that passing a position of (0, 0) will always center them in the selector.
(This also applies to text, although the anchor properties of the text element can of course
be changed on the returned object if desired.)
The UpdateInterface method
UpdateInterface is automatically called when the value of the underlying mapper type changes
and should update the UI appropriately, for example by updating displayed texts.
Changing the mapper type's value
The CustomMapperType property can be used to access the underlying mapper type.
If the underlying type that is being edited is a value type (i.e. the value object
should be swapped out instead of changing properties of it), it can be set using the
Value property of the mapper type. Setting this will also automatically invoke the Changed
event of the mapper type.
If the underlying type is edited by changing its properties, call the InvokeChanged method
manually whenever the object is edited.
In either case, the OnEdit method of the CustomSelector must also be called every time
the value is edited.
Registering the custom mapper type
Custom mapper types must be registered so the game knows about them. This is done by calling
the CustomMapperTypes.AddMapperType<T, TMapper, TSelector>() method, passing the appropriate
types that were created in the earlier steps. This should be called during execution of
OnLoad.