MoonSharp 文档三
MoonSharp 文档一-CSDN博客
MoonSharp 文档二-CSDN博客
MoonSharp 文档四-CSDN博客
MoonSharp 文档五-CSDN博客
7.Proxy objects(代理对象)
如何封装你的实现,同时又为脚本提供一个有意义的对象模型
官方文档:MoonSharp
在实际场景中,脚本通常不受你的控制。这会带来一些问题,其中包括:
安全性:这部分内容不在本页的讨论范围内,但如果你的脚本并非来自严格受控的环境,请务必阅读关于沙箱的部分。
向后和向前兼容性:MoonSharp 会尽力避免引入过去或将来的兼容性问题,但你的 API 设计是你自己的责任!
实现上述目标的一个关键方法是封装你的实现细节,确保脚本不会调用那些不应该调用的 API 字段,这样你就可以自由地调整内部模型,而无需担心意外破坏脚本。
有两种封装方式,一种是通过一些非常复杂的方式复制所有 API,另一种是通过“代理对象”(一种“脚本的 DTO”)。
概念非常简单。对于每个你想封装并暴露给脚本的类型,你需要提供一个“代理类型”,这是一个类,它操作封装(目标)类型的实例。
举个例子胜过千言万语:
// Declare a proxy class
class MyProxy
{MyType target;[MoonSharpHidden]public MyProxy(MyType p){this.target = p;}public int GetValue() { return target.GetValue(); }
}// Register the proxy, using a function which creates a proxy from the target type
UserData.RegisterProxyType<MyProxy, MyType>(r => new MyProxy(r));// Test with a script - only the proxy type is really exposed to scripts, yet everything it works
// just as if the target type was actually used..Script S = new Script();S.Globals["mytarget"] = new MyType(...);
S.Globals["func"] = (Action<MyType>)SomeFunction;S.DoString(@"x = mytarget.GetValue(); func(mytarget);
");
除了封装之外,代理对象还可以实现一些巧妙的技巧。
其中一个非常简单但非常有用——正确访问值类型。例如,你可以封装 Unity 的 Transform
类(它完全由值类型组成,但本身是引用类型),并通过一个不同的接口来正确保留引用!
8.Error handling(错误处理)
以及错误生成
官方文档:MoonSharp
MoonSharp 会生成几种类型的异常:
-
InternalErrorException
-
SyntaxErrorException
-
ScriptRuntimeException
-
DynamicExpressionException
所有这些异常都继承自一个共同的 InterpreterException
类型,因此可以在异常过滤器中将这些异常归类在一起。
当然,由于调用代码或 MoonSharp 代码中的错误,也可能会生成其他类型的异常,但至少在理论上,这些异常不会由于脚本代码中的错误而产生。
这些异常还支持一个 DecoratedMessage
属性,该属性包含了错误信息,并附带了生成错误的源代码中的引用(如果可用)。
Script.GlobalOptions.RethrowExceptionNested
有一个名为 RethrowExceptionNested
的全局选项。如果设置为 false
(默认值),异常会在保留堆栈的情况下重新抛出,并且装饰后的错误信息可以在 DecoratedMessage
属性中获取。如果设置为 true
,则会抛出一个新的异常,旧的异常会存储在 InnerException
属性中,并且装饰后的错误信息既可以在 DecoratedMessage
属性中获取,也可以在 Message
属性中获取。
你可以根据个人喜好设置 RethrowExceptionNested
,但通常情况下,在 .NET 环境下工作时,设置为 false
效果更好;而在 Mono、Xamarin 和 Unity 环境下工作时,设置为 true
效果更佳。
内部错误异常(InternalErrorException)
当解释器遇到内部错误时,会抛出 InternalErrorException
。这种情况通常没有太多可以处理的余地,通常应将其视为脚本引擎的致命错误。如果发生这种情况,请尽可能在论坛或 Discord 中报告,并提供所有可能的信息以便复现问题。
语法错误异常(SyntaxErrorException)
当解析器无法解析脚本代码或脚本代码因某些原因无效时,会抛出 SyntaxErrorException
。
需要注意的是,此异常是在调用 Script
的某些方法(如 DoFile
、RunString
等)时抛出的。如果在一个有问题的脚本上调用标准 Lua 库的 load
函数,则会生成一个 ScriptRuntimeException
,并将 SyntaxErrorException
包裹在其中。
脚本运行时异常(ScriptRuntimeException)
这是所有异常中最常见的一个,可能也是最重要的一个。
每当 Lua 抛出错误时(无论是通过调用 error
函数,还是因为运行时错误等),都会抛出一个 ScriptRuntimeException
。例如:
static void ErrorHandling()
{try{string scriptCode = @" return obj.calcHypotenuse(3, 4);";Script script = new Script();DynValue res = script.DoString(scriptCode);}catch (ScriptRuntimeException ex){Console.WriteLine("Doh! An error occured! {0}", ex.DecoratedMessage);}
}
将打印:
Doh! An error occured! chunk_1:(2,5-36): attempt to index a nil value
如果我们想向 Lua 抛出一个错误,只需执行相同的操作,抛出一个 ScriptRuntimeException
即可。非常简单。
static void DoError()
{throw new ScriptRuntimeException("This is an exceptional message, no pun intended.");
}static string ErrorGen()
{string scriptCode = @" local _, msg = pcall(DoError);return msg;";Script script = new Script();script.Globals["DoError"] = (Action)DoError;DynValue res = script.DoString(scriptCode);return res.String;
}
这将返回:
This is an exceptional message, no pun intended.
动态表达式异常(DynamicExpressionException)
当解释器在评估动态表达式时遇到错误时,会抛出 DynamicExpressionException
。有关动态表达式的具体含义,请参阅动态表达式的教程。
9.Script Loaders(脚本加载器)
如何更改 MoonSharp 从文件读取脚本的方式
官方文档:MoonSharp
MoonSharp 旨在支持多平台运行。它无法选择将在哪些平台上运行,因为这是库的最终用户的选择,因此它必须以某种方式能够在各种环境中运行,例如作为 Linux 中的守护进程、作为 WPF 应用程序、作为手机上的应用程序或作为游戏主机上的游戏。例如,像 FileStream
这样简单的 API 在 Windows Store 应用中并不可用。
此外,MoonSharp 也不知道它应该如何在使用它的应用程序中运行和工作。例如,如果你希望 loadfile
从嵌入式资源加载脚本而不是从文件加载,该怎么办?
出于这些原因,MoonSharp 提供了两个对象层次结构:
-
脚本加载器(Script Loaders) - 用于自定义从文件加载脚本的 API 的工作方式。
-
平台访问器(Platform Accessors) - 用于自定义如何完成对操作系统的底层访问。
需要注意的是,通常情况下,脚本加载器与平台访问器是独立的,尽管它们可以根据需要使用对方的方法。这意味着,例如,即使你的平台访问器不支持从文件加载,也不一定意味着你的脚本加载器不能支持,反之亦然。
它们是两个独立的对象,因为它们处理两种不同的职责。脚本加载器负责如何从文件加载脚本,而平台访问器负责处理那些需要调用操作系统 API 的库函数(主要是在 os
和 io
模块中)。
预定义脚本加载程序快速浏览
根据你所使用的平台,你可以选择以下几种脚本加载器:
-
FileSystemScriptLoader:直接访问文件系统中的文件,可自定义,但在可移植类库(PCL)中不受支持。
-
ReplInterpreterScriptLoader:与
FileSystemScriptLoader
相同,但额外使用了与 Lua 相同的逻辑从环境变量(如MOONSHARP_PATH
、LUA_PATH
或"?;?.lua"
,以最先存在的为准)中获取路径。 -
EmbeddedResourcesScriptLoader:提供对给定程序集的嵌入式资源的访问,而不是文件系统。
-
InvalidScriptLoader:抛出异常。
-
UnityAssetsScriptLoader:适用于 Unity3D,用于从文本资源(Text Assets)加载脚本。
如果没有重新定义,MoonSharp 默认使用的脚本加载器按以下规则选择:
-
如果在 Unity3D 环境下运行,默认脚本加载器是
UnityAssetsScriptLoader
,路径为Assets/Resources/MoonSharp/Scripts
。文件必须具有.txt
扩展名(因为 Unity 的行为比较特殊)。 -
如果当前构建为可移植类库(PCL),则选择
InvalidScriptLoader
(除非你采取其他措施,否则无法从文件加载脚本)。 -
其他情况下,默认使用
FileSystemScriptLoader
。
如何指定要使用的脚本加载器
假设我们希望从当前程序集的嵌入资源中加载脚本。
基本上有两种方法,一种是局部方法,另一种是全局方法。 首先,对于给定的脚本,可以通过以下方式修改其脚本加载器:
script.Options.ScriptLoader = new EmbeddedResourcesScriptLoader();
否则,可以使用以下方法指定所有新创建的脚本应使用新的脚本加载程序:
Script.DefaultOptions.ScriptLoader = new EmbeddedResourcesScriptLoader();
如何自定义 require 函数的行为
一个常见的需求是更改 require
函数用于加载模块的目录。
大多数脚本加载器都扩展了 ScriptLoaderBase
类,该类公开了一个 ModulePaths
属性,该属性包含了加载模块时将检查的所有路径。
你可以轻松地更改这些路径:
// These two lines are equivalent:
((ScriptLoaderBase)script.Options.ScriptLoader).ModulePaths = new string[] { "MyPath/?", "MyPath/?.lua" };
// or
((ScriptLoaderBase)script.Options.ScriptLoader).ModulePaths = ScriptLoaderBase.UnpackStringPaths("MyPath/?;MyPath/?.lua");
请注意,ScriptLoaderBase
还会检查当前 _ENV
中的 LUA_PATH
全局变量,以确定使用哪些路径来加载模块。如果你希望忽略 LUA_PATH
全局变量,可以使用:
((ScriptLoaderBase)script.Options.ScriptLoader).IgnoreLuaPathGlobal = true;
谨慎提示:仅仅改变 ModulePaths 和/或 IgnoreLuaPathGlobal 并不足以限制哪些文件可以被加载,形成"沙盒"环境。如果你需要这个功能,请实现一个自定义的脚本加载器。
如何使用 EmbeddedResourcesScriptLoader
将脚本嵌入为资源很容易。
在 Visual Studio 中:
• 在你的项目中创建一个"Scripts"文件夹
• 在该文件夹中添加一个新的文本文件,并将其重命名为 Test.lua
• 在该文件中输入一些 Lua 代码
然后,在解决方案资源管理器中右键单击该文件,会出现以下窗口:
确保 “Build Action” 设置为 “Embedded Resource”。
完成此设置后,我们就使用正确的脚本加载器:
static void EmbeddedResourceScriptLoader()
{Script script = new Script();script.Options.ScriptLoader = new EmbeddedResourcesScriptLoader();script.DoFile("Scripts/Test.lua");
}
在其他 IDE 中也可以遵循类似的步骤,例如 Xamarin。
如何创建自己的脚本加载器
是时候创建你自己的脚本加载器了。
你基本上有两个选择:扩展 ScriptLoaderBase(推荐)或实现 IScriptLoader。
两者都很直观,但为了方便起见,我们还是选择第一个。
我们的脚本加载器实际上会动态生成一个小脚本,其名称是请求的文件名,而不是真正地去加载文件:
private class MyCustomScriptLoader : ScriptLoaderBase
{public override object LoadFile(string file, Table globalContext){return string.Format("print ([[A request to load '{0}' has been made]])", file);}public override bool ScriptFileExists(string name){return true;}
}
实现这一点很容易:
static void CustomScriptLoader()
{Script script = new Script();script.Options.ScriptLoader = new MyCustomScriptLoader() { ModulePaths = new string[] { "?_module.lua" } };script.DoString(@"require 'somemodule'f = loadfile 'someothermodule.lua'f()");
}
运行这些操作将打印:
A request to load 'somemodule_module.lua' has been made
A request to load 'someothermodule.lua' has been made
end