VitalRouter.MRuby v1
Maintenance for the VitalRouter.MRuby v1 series will not continue. Please consider migrating to v2.
VitalRouter.MRuby v1 is only support for Unity.
To install it, please add the following URL from the Unity Package Manager.
To use VitalRouter.MRuby v1, you must install the VitalRouter v1 series. See https://github.com/hadashiA/VitalRouter/tree/v1
https://github.com/hadashiA/VitalRouter.git?path=/src/VitalRouter.Unity/Assets/VitalRouter.MRuby#1.6.4
To execute mruby scripts, first create an MRubyContext
.
var context = MRubyContext.Create();
context.Router = Router.Default; // ... 1
context.CommandPreset = new MyCommandPreset()); // ... 2
- Set the
Router
for VitalRouter. Commands published from mruby are passed to the Router specified here. (Default: Router.Default) - The
CommandPreset
is a marker that represents the list of commands you want to publish from mruby. You create it as follows:
[MRubyCommand("move", typeof(CharacterMoveCommand))] // < Your custom command name and type list here
[MRubyCommand("speak", typeof(CharacterSpeakCommand))]
partial class MyCommandPreset : MRubyCommandPreset { }
// Your custom command decralations
[MRubyObject]
partial struct CharacterMoveCommand : ICommand
{
public string Id;
public Vector3 To;
}
[MRubyObject]
partial struct CharacterSpeakCommand : ICommand
{
public string Id;
public string Text;
}
To execute a script with MRubyContext
, do the following:
// Your ruby script source here
var rubySource = "cmd :speak, id: 'Bob', text: 'Hello'"
using MRubyScript script = context.CompileScript(rubySource);
await script.RunAsync();
In mruby source, the first argument of cmd
is any name registered with [MRubyCommand("...")].
The subsequent key/value list represents the member values of the command type (in this case, CharacterSpeakCommand).
[!TIP] The Ruby cmd method waits until the await of the C# async handler completes but does not block the Unity main thread. It looks like a normal ruby method, but it's just like a Unity coroutine. VitalRouter.MRuby is fully integrated with C#'s async/await.
[MRubyObject]
Types marked with [MRubyObject]
can be deserialized from the mruby side to the C# side.
- class, struct, and record are all supported.
- A partial declaration is required.
- Members that meet the following conditions are converted from mruby:
- public fields or properties, or fields or properties with the
[MRubyMember]
attribute. - And have a setter (private is acceptable).
- public fields or properties, or fields or properties with the
[MRubyObject]
partial struct SerializeExample
{
// this is serializable members
public string Id { get; private set; }
public string Foo { get; init; }
public Vector3 To;
[MRubyMember]
public int Z;
// ignore members
[MRubyIgnore]
public float Foo;
}
The list of properties specified by mruby is assigned to the C# member names that match the key names.
Note that the names on the ruby side are converted to CamelCase.
- Example: ruby's
foo_bar
maps to C#'sFooBar
.
You can change the member name specified from Ruby by using [MRubyMember("alias name")]
.
[MRubyObject]
partial class Foo
{
[MRubyMember("alias_y")]
public int Y;
}
Also, you can receive data from Ruby via any constructor by using the [MRubyConstructor]
attribute.
[MRubyObject]
partial class Foo
{
public int X { ge; }
[MRubyConstructor]
public Foo(int x)
{
X = x;
}
}
Deserialization mruby - C#
[MRubyObject]
works by deserializing mrb_value
directly to a C# type.
See the table below for the support status of mutually convertible types.
mruby | C# |
---|---|
Integer | int , uint , long , ulong , shot , ushot , byte , sbyte , char |
Float | float , double , decimal |
Array | T , List<> , T[,] , T[,] , T[,,] , Tuple<...> , ValueTuple<...> , , Stack<> , Queue<> , LinkedList<> , HashSet<> , SortedSet<> , Collection<> , BlockingCollection<> , ConcurrentQueue<> , ConcurrentStack<> , ConcurrentBag<> , IEnumerable<> , ICollection<> , IReadOnlyCollection<> , IList<> , IReadOnlyList<> , ISet<> |
Hash | Dictionary<,> , SortedDictionary<,> , ConcurrentDictionary<,> , IDictionary<,> , IReadOnlyDictionary<,> |
String | string , Enum , byte[] |
[Float, Float] | Vector2 , Resolution |
[Integer, Integer] | Vector2Int |
[Float, Float, Float] | Vector3 |
[Int, Int, Int] | Vector3Int |
[Float, Float, Float, Float] | Vector4 , Quaternion , Rect , Bounds , Color |
[Int, Int, Int, Int] | RectInt , BoundsInt , Color32 |
nil | T? , Nullable<T> |
If you want to customize the formatting behavior, implement IMrbValueFormatter
.
// Example type...
public struct UserId
{
public int Value;
}
public class UserIdFormatter : IMrbValueFormatter<UserId>
{
public static readonly UserIdFormatter Instance = new();
public UserId Deserialize(MrbValue mrbValue, MRubyContext context, MrbValueSerializerOptions options)
{
if (mrbValue.IsNil) return default;
retun new UserId { Value = mrbValue.IntValue };
}
}
To enable the custom formatter, set MrbValueSerializerOptions as follows.
StaticCompositeResolver.Instance
.AddFormatters(UserIdFormatter.Instance) // < Yoru custom formatters
.AddResolvers(StandardResolver.Instance); // < Default behaviours
// Set serializer options to context.
var context = MRubyContext.Create(...);
context.SerializerOptions = new MrbValueSerializerOptions
{
Resolver = StaticCompositeResolver
};
MRubyContext
MRubyContext
provides several APIs for executing mruby scripts.
using var context = MRubyContext.Create(Router.Default, new MyCommandPreset());
// Evaluate arbitrary ruby script
context.Load(
"def mymethod(v)\n" +
" v * 100\n" +
"end\n");
// Evaluates any ruby script and returns the deserialized result of the last value.
var result = context.Evaluate<int>("mymethod(7)");
// => 700
// Syntax error and runtime error on the Ruby side can be supplemented with try/catch.
try
{
context.Evaluate<int>("raise 'ERRRO!'");
}
catch (Exception ex)
{
// ...
}
// Execute scripts, including the async method including VitalRouter, such as command publishing.
var script = context.CompileScript("3.times { |i| cmd :text, body: \"Hello Hello #{i}\" }");
await script.RunAsync();
// When a syntax error is detected, CompileScript throws an exception.
try
{
context.CompileScript("invalid invalid invalid");
}
catch (Exception ex)
{
}
// The completed script can be reused.
await script.RunAsync();
// You can supplement Ruby runtime errors by try/catch RunAsync.
try
{
await script.RunAsync();
}
catch (Exception ex)
{
// ...
}
script.Dispose();
if you want to handle logs sent from the mruby side, do as follows:
MRubyContext.GlobalLogHandler = message =>
{
UnityEngine.Debug.Log(messae);
};
Ruby API
The mruby embedded with VitalRouter contains only a portion of the standard library to reduce size. Please check the vitalrouter.gembox to see which mrbgem is enabled.
In addition to the standard mrbgem, the following extension APIs are provided for Unity integration.
# Wait for the number of seconds. (Non-blocking)
# It is equivalent to `await UniTask.Delay(TimeSpan.FromSeconds(1))`)
wait 1.0.sec
wait 2.5.secs
# Wait for the number of fames. (Non-blocking)
# It is equivalent to `await UniTask.DelayFrame(1)`)
wait 1.frame
wait 2.frames
# Send logs to the Unity side. Default implementation is `UnityEngine.Debug.Log`
log "Log to Unity !"
# Publish VitalRouter command
cmd :your_command_name, prop1: 123, prop2: "bra bra"
[!NOTE] "Non-blocking" means that after control is transferred to the Unity side, the Ruby script suspends until the C# await completes, without blocking the thread.
SharedState
Arbitrary variables can be set from the C# side to the mruby side.
var context = MRubyScript.CreateContext(...);
context.SharedState.Set("int_value", 1234);
context.SharedState.Set("bool_value", true);
context.SharedState.Set("float_value", 678.9);
context.SharedState.Set("string_value", "Hoge Hoge");
context.SharedState.Set("symbol_value", "fuga_fuga", asSymbol: true);
SharedState can also be referenced via PublishContext.
router.Subscribe((cmd, publishContext) =>
{
// ...
publishContext.MRubySharedState().Set("x", 1);
// ...
});
router.SubscribeAwait(async (cmd, publishContext, cancellation) =>
{
// ...
publishContext.MRubySharedState().Set("x", 1);
// ...
});
[Routes]
class MyPresenter
{
public async UniTask On(FooCommand cmd, PublishContext publishContext)
{
publishContext.MRubySharedState().Set("x", 1);
}
}
Shared variables can be referenced from the ruby side as follows.
state[:int_value] #=> 1234
state[:bool_value] #=> true
state[:float_value] #=> 678.9
state[:string_value] #=> "Hoge Hoge"
state[:symbol_value] #=> :fuga_fuga
# A somewhat fuzzy matcher, the `is?` method, is available for shared states.
state[:a] #=> 'buz'
state[:a].is?('buz') #=> true
state[:a].is?(:buz) #=> true
Memory Usage in Ruby
VitalRouter.MRuby specifies Unity's UnsafeUtility.Malloc
for mruby's memory allocator.
Therefore, mruby's memory usage can be checked from MemoryProfiler, etc.
Supported platforms
VitalRouter.MRuby embeds custom libmruby
as a Unity native plugin.
It will not work on platforms for which native binaries are not provided.
Please refer to the following table for current support status.
Platform | CPU Arch | Build | Tested the actual device |
---|---|---|---|
Windows | x64 | ✅ | ✅ |
arm64 | |||
Windows UWP | ? | ? | |
macOS | x64 | ✅ | ✅ |
arm64 (apple silicon) | ✅ | ✅ | |
Universal (x64 + arm64) | ✅ | ✅ | |
Linux | x64 | ✅ | Tested only the headless Editor (ubuntu) |
arm64 | ✅ | ||
iOS | arm64 | ✅ | (planed) |
x64 (Simulator) |