-
Notifications
You must be signed in to change notification settings - Fork 3
Improving Mono integration
- Remove AS bindings from engine
- Remove any engine-unrelated fields from mono-exposed types: Params/Stats, VarX, bindfield blobs
- Managed runtime becomes owner of the native types: it decides whether to instantiate one, and whether to dispose of one
- Closing the mono bindings in dll, the script writers won't be able to customize it
Let's take current Critter as example - it's fat class containing few related concepts:
- Core data needed by an engine to process its position, movement, map/location position
- Customizable data needed by scripts to process gameplay logic
The two above overlaps often, but still, we can safely tear apart those concepts, where of course the latter goes to the managed world, because it's about customizing gameplay features. This leaves our Critter with only core data, and we make it unextensible (aka sealed). Do we loose something?
Not really.
First, let's determine why current Critter implementation is so heavily customizable:
- it may represent an NPC
- it may represent a monster
- it may represent a player
- ...
Is this good? We may be even tempted to have BaseCritter and derive Npc, Monster, Player classes from it, but let's not do this, let's redefine the above (it's just example):
- NPC is Critter with stats + dialog
- Monster is Critter with stats + loot
- Player is Critter with stats, skills and inventory
- Caravan is list of NPCs + list of players + some car + some inventory
- ...
The above could represent types needed to define gameplay in some hypothetical scenario, the Npc, Monster, Player, Caravan types are defined and managed entirely on scripting side, whereas Critter is native and closed type encapsulating core critter functionality exposed by engine (events, map positioning, visibility...), and is instantiated only by request of the managed side, and is disposed only by request of managed side.
When the world is instantiated (be it from save or not) world, we instantiate those gameplay types, which in turn query engine for creation of the native types, and not the other way around (like it's now - engine instantiates engine types which in turn call some scripts that instantiate some other structures).
Note that this focuses on the gameplay, where traditional SDK focuses on extending and customizing the base set of types, which often was leading to tricky code (critter representing caravan member would have some Stat defined describing this membership etc etc).
How does this change what's currently implemented in mono bindings?
- The native types are instantiated and disposed from managed world, this removes IsNotValid problem
- The functionality is closed and extensible only when engine gots extended, this decreases the need to maintain the mono bindings
- The current Critter/Item/Map engine types require only to remove the customizing part, core stuff stays
The only thing we need to add is serialization, but this only helps in the future as we were struggling with serializing/deserializing gameplay concepts anyway
Hence the scripts (let's call the code exected by managed runtime this way) will be now responsible for managing native types, the workflow of writing them is gonna change. The below code snippets represent how one could approach some gameplay problem:
class NukaColaMachine
{
private int AmountLeft { get; set; }
private int CoinsInside { get; set; }
private Item Machine { get; set; } // this is our native type used here, via composition and not inheritance
public NukaColaMachine(int amount, int coins, int machineId = 0)
{
this.AmountLeft = amount;
this.CoinsInside = coins;
this.Machine = machineId == 0
? Global.CreateItem(...);
: Global.GetItem(machineId);
// now, the sweet part
this.Machine.Use += (o, e) =>
{
if(this.AmountLeft > 0) // we've got access to 'this' in event handler!
{
// now, the bitter part, we've got only Critter reference of the 'user'
// so we need to use some special functionality to operate on game type
// that knows this critter, it's a way of sending a message
// "hey, execute this code if you're managed by Player gameplay class"
e.Cr.HandleGameType<Player>(this, p =>
{
if(p.Coins > 0)
{
p.Coins--;
this.AmountLeft--;
this.CoinsInside++;
p.AddItem(PID_NUKACOLA);
}
});
}
};
}
public NukaColaMachine Load(ISaveLoader loader) // hypothetical way of loading
{
return new NukaColaMachine(loader.GetInt(), loader.GetInt(), loader.GetInt());
}
public void Save(ISaveSaver saver)
{
saver.StoreInt(AmountLeft);
saver.StoreInt(CoinsInside);
saver.StoreInt(Machine.Id);
}
}
class Player
{
Critter cr;
public Player(...)
{
// ...
cr = GetCritter(...);
cr.GameTypeHandler += (sener, e) => // e is tuple, Item1 is type, Item2 is callback
{
if (e.Item1 == typeof(Player))
e.Item2 (this);
};
}
}
And our Critter binding will have to be enhanced with following event:
class Critter
{
// ....
public event EventHandler<Tuple<Type, Action<object>>> GameTypeHandler;
public void HandleGameType<T>(object sender, Action<T> callback) where T: class
{
if(this.GameTypeHandler != null)
this.GameTypeHandler(sender, Tuple.Create<Type,Action<object>> (typeof(T), o => callback(o as T)));
}
}
TODO:
public class GameType : IDisposable
{
List<IDisposable> eventHandlers = new List<IDisposable>();
public GameType ()
{
}
public void HandleEvent(IDisposable handler)
{
eventHandlers.Add (handler);
}
public void HandleGameType(IGameTypeHandling engineObject, object gameObj)
{
eventHandlers.Add (engineObject.HandleGameType ((o,e) =>
{
if (e.Type == gameObj.GetType())
e.Callback (gameObj);
}));
}
public void Dispose()
{
eventHandlers.ForEach (h => h.Dispose ());
}
}
public interface IGameTypeHandling
{
IDisposable HandleGameType (EventHandler<GameTypeEventArgs> h);
}
/// <summary>
/// Wraps the callback to make possible to dispose an attached event handler
/// </summary>
public class DisposableEventHandler : IDisposable
{
Action dispose;
bool disposed = false;
public DisposableEventHandler(Action dispose)
{
this.dispose = dispose;
}
public void Dispose()
{
dispose ();
disposed = true;
}
~DisposableEventHandler()
{
if (!disposed)
dispose ();
}
}
// Critter
event EventHandler<GameTypeEventArgs> GameTypeEvent;
public IDisposable HandleGameType(EventHandler<GameTypeEventArgs> handler)
{
GameTypeEvent += handler;
return new DisposableEventHandler (() => GameTypeEvent -= handler);
}
public void HandleGameType<T>(object sender, Action<T> callback) where T: class
{
if(this.GameTypeEvent != null)
this.GameTypeEvent(sender, new GameTypeEventArgs (typeof(T), o => callback(o as T)));
}