Conscript:用于 .NET 的可嵌入、已编译的脚本语言






4.97/5 (56投票s)
一个用于增强任何 .NET 应用程序的脚本语言 API
背景
嵌入式脚本引擎在多个领域都有应用,包括扩展或修改软件应用程序核心功能的能力。在游戏开发领域,脚本为游戏引擎开发者提供了一种将控制权交给设计者的手段,允许设计者在无需游戏引擎开发者干预的情况下实现游戏的情节事件、NPC 行为等,而游戏引擎开发者通常需要将游戏玩法逻辑硬编码到游戏引擎中。
引言
继上一篇文章中介绍的解释型 Eezeescript 可嵌入脚本语言之后,本文介绍了 Conscript,这是一个更复杂的 .NET 应用程序脚本引擎。Conscript 将脚本源编译为 Conscript VM 的字节码,从而获得比解释引擎更快的脚本执行速度。本文还附带了脚本引擎的源代码和二进制文件以及一个说明性演示。
本文介绍的演示应用程序是一个 Windows 应用程序,提供了一个简单的 Conscript 测试环境。用户可以在左侧面板中键入脚本,然后单击“运行脚本”按钮来编译和运行脚本。该应用程序公开了一个主机函数 `Print`,允许用户将脚本中的数据输出到底部右侧的输出面板。右上角面板显示了 Conscript 程序集形式的已编译脚本。
架构概览
脚本系统将用高级 C 语言编写的脚本编译为专为 Conscript 设计的虚拟指令集。虚拟指令集让人联想到 Intel 指令集,并提供额外的指令来简化编译器的设计。虚拟机是一个混合系统,具有用于函数帧和参数传递的独立堆栈;基于分层字典的内存系统;并发管理系统;以及允许调用已注册到脚本系统的本机函数的接口。此外,指令操作数和存储在内存中的变量是变体类型,支持整数、浮点值、布尔值、字符串和关联数组。许多指令(如 ADD)可以对不同类型进行操作。在某些情况下,甚至支持混合类型操作数。一个典型的例子是将字符串与整数连接。
使用脚本引擎 API
本节将快速介绍将 Conscript 集成到 .NET 解决方案并使用其 API。
将 Conscript 集成到 .NET 解决方案
通过添加对 `Conscript.dll` 程序集的引用,可以将 Conscript 主机 API 集成到现有的 .NET 解决方案中。所有 API 类都定义在 `Conscript` 命名空间内,该命名空间必须使用 `using` 子句指定,或者在其 API 类前加上前缀。
准备脚本环境
脚本系统通过创建 `ScriptManager` 类的一个或多个实例来初始化。每个实例代表一个可以加载和执行脚本的脚本环境。每个实例还提供一个全局变量作用域,脚本可以使用该作用域来共享数据。
ScriptManager scriptManager = new ScriptManager();
加载脚本
有了脚本管理器后,可以通过创建 `Script` 类的实例来加载脚本。脚本对象的构造函数需要对脚本管理器的引用和一个用于标识脚本的名称。默认情况下,此名称对应于磁盘文件名。
Script script = new Script(scriptManager, "scripts\Wizard.cns");
脚本对象的构造函数会自动将脚本源编译为 Conscript 字节码。默认情况下,生成的字节码使用“窥孔”汇编优化器进行优化。会添加调试指令以方便将生成的代码与原始源进行映射。这些设置可以通过设置 `ScriptManager` 的 `DebugMode` 和 `OptimiseCode` 属性来控制。
// omit debug symbols from code
scriptManager.DebugMode = false;
// do not optimise code
scriptManager.OptimiseCode = false;
准备执行脚本
脚本对象仅表示其中包含的编程指令,而不表示其执行状态。要执行脚本,必须创建 `ScriptContext` 类的实例。该类的构造函数需要对要执行的脚本的引用或对脚本某个函数的引用。脚本引用意味着将执行 `main` 函数。脚本上下文提供执行控制,以及在变量定义、要执行的下一条语句等方面的执行状态访问。`ScriptContext` 类表示脚本的运行实例。因此,可以通过创建多个引用相同脚本的脚本上下文,在同一个脚本管理器中执行同一脚本对象的多个实例。
// create a context for the script's main function
ScriptContext scriptContext = new ScriptContext(script);
// also creates a context for the script's main function
ScriptContext scriptContext = new ScriptContext(script.MainFunction);
// create a context for one of the script's named functions
ScriptFunction scriptFunction = script.Functions["WanderAround"];
ScriptContext scriptContext = new ScriptContext(scriptFunction);
执行脚本
脚本上下文对象允许通过其 `Execute` 方法的三个变体来执行引用的脚本。这些是:无限期执行脚本;在给定时间间隔内执行脚本;或执行最多指定语句数的脚本。
第一种方法变体允许引用的脚本函数无限期地执行,直到函数结束。如果脚本包含无限循环,此方法将无限期地阻塞,除非生成中断。`Execute` 方法返回自调用以来执行的总语句数。
// execute indefinitely, or until termination, or until
// a script interrupt is generated
scriptContext.Execute();
`Execute` 方法的第二种变体允许脚本上下文执行最多指定数量的语句。在没有更多要处理的语句或生成中断的情况下,脚本上下文可能会在达到最大值之前退出执行。
// execute up to a maximumum of 10 statements
scriptContext.Execute(10);
`Execute` 方法的第三种变体接受一个 `TimeSpan` 来定义脚本执行允许的最大时间间隔。在没有更多要处理的语句或生成中断的情况下,该方法可能会在给定间隔之前退出执行。给定一个在不同语句之间平衡良好的脚本,此方法的一个可能用途是确定脚本系统在目标环境中的速度(以每秒执行的语句数衡量)。
// execute for up to 10 milliseconds
TimeSpan tsInterval = new TimeSpan(0, 0, 0, 0, 10);
scriptContext.Execute(tsInterval);
可以使用 `Execute` 的第二种和第三种变体来实现虚拟多线程脚本环境。全局变量可用作信号量来同步并发运行的脚本。
中断和重置脚本
脚本上下文通常会无限期地执行其引用的脚本函数,或者在给定的时间间隔内,直到执行完指定数量的语句,或者直到没有更多语句可处理。在某些情况下,需要提前中断执行,例如在执行特定语句时将控制权返回给主机,或者因为脚本计算量过大而无法一次性执行。
Conscript 提供了两种生成脚本中断的方法:
- 使用 `yield` 语句在脚本的特定点显式中断执行
- 启用脚本上下文对象的 `InterruptOnHostfunctionCall` 属性,以便在执行注册的主机函数时自动生成中断
稍后文章中将介绍的“绝望地下城”演示游戏使用第二种方法,允许将每个 NPC 脚本编写为似乎与其他脚本独立运行。每当执行主机函数时,控制权就会交还给关联的字符对象。这反过来又允许字符对象排队并处理相应的操作。在操作进行时,脚本上下文处于暂停状态。当操作完成时,将恢复脚本的执行。
可以通过脚本上下文的 `Reset` 方法重置正在执行的脚本。调用此方法的净效果是丢弃上下文中定义的局部变量,清除执行帧堆栈,并将语句指针重置为指向脚本上下文中引用的脚本块的第一个语句。此外,还将清除上下文请求的任何活动锁。全局变量不受影响,并在脚本上下文重置后保留。
自定义脚本加载器
为了允许从其他来源(如存档文件、网络或数据库)加载脚本,可以开发一个自定义脚本加载器类,并将其绑定到脚本管理器以用于加载脚本。脚本加载器类可以是实现 `ScriptLoader` 接口的任何类。加载器用于检索 `Script` 类构造函数中指定的脚本以及原始脚本中定义的任何其他 include 脚本。
// custom script loader class
public class MyScriptLoader
: ScriptLoader
{
public List<string /> LoadScript(String strResourceName)
{
// loader implementation here...
}
}
// in initialisation code...
ScriptLoader scriptLoader = new MyScriptLoader();
scriptManager.Loader = scriptLoader;
访问局部和全局变量作用域
允许脚本与主机应用程序通信或控制主机应用程序的一种方法是,应用程序轮询关联脚本上下文的局部变量以及关联脚本管理器的全局变量。它通过查询 `ScriptContext` 对象的 `LocalDictionary` 属性、`Script` 对象的 `ScriptDictionary` 属性或 `ScriptManager` 对象的 `GlobalDicitonary` 属性来实现。
// get value of local variable
int iIndex = (int) scriptContext.LocalDictionary["index"];
// get value of script variable
int iScore = (int) script.ScriptDictionary["s_playerScore"];
// get value of global variable
bool bNewQuest =
(bool) scriptManager.GlobalDictionary["g_wizardQuestAvailable"];
注册主机函数
允许脚本与主机应用程序接口的更强大的替代方法是向脚本管理器注册主机函数,并在脚本上下文或管理器级别分配脚本处理程序。脚本处理程序反过来为主机函数提供实现。这些函数首先通过创建 `HostFunctionPrototype` 类的实例来定义,以定义函数的名称、参数和返回值。与脚本定义的函数不同,主机函数可以在编译时强制执行参数和返回值的类型检查。因此,使用主机函数的脚本必须在已预先注册了所需主机函数的脚本管理器中加载。
// define host function to move player
HostFunctionPrototype hostFunctionPrototype = new HostFunctionPrototype(
null, "Player_MoveTo", typeof(int), typeof(int));
提供了许多构造函数,可以快速构造最多三个参数的函数原型。对于需要更多参数的函数,还提供了一个接受类型列表 `List
// prepare parameter type list
List<Type> listParameterTypes = new List<Type>();
listParameterTypes.Add(typeof(String));
listParameterTypes.Add(typeof(int));
listParameterTypes.Add(typeof(int));
listParameterTypes.Add(typeof(bool));
listParameterTypes.Add(typeof(AssociativeArray));
// define function prototype with many parameters
HostFunctionPrototype hostFunctionPrototype = new HostFunctionPrototype(
typeof(float), "FuncWithManyParams", listParameterTypes);
定义了主机函数原型后,就可以将其注册到脚本管理器。函数原型可确保在编译和运行时识别相应的函数,并且传入的参数在数量、类型和顺序上都与函数原型中定义的相匹配。以下语句注册了一个没有处理程序的主机函数原型。
// register host function
m_scriptManager.RegisterHostFunction(hostFunctionPrototype);
上面所示的注册方法要求将处理程序对象附加到从使用该主机函数的脚本创建的每个 `ScriptContext`。处理程序可以是实现 `HostFunctionHandler` 接口的任何类,该接口由 `OnHostFunctionCall(...)` 方法组成。一个好的方法是扩展拥有相关脚本上下文的类。以下示例说明了这一点,该示例实现了游戏中敌人 AI 脚本的主机函数。
public class Enemy
: HostFunctionHandler
{
// class implementation
private ScriptContext m_scriptContext
public Enemy(..., Script script, ...)
{
// create script context
m_scriptContext = new ScriptContext(script);
// assign self as host function handler
m_scriptContext.Handler = this;
}
// HostFunctionHandler implementation
public object OnHostFunctionCall(String strFunctionName,
List<object> listParameters)
{
// handle random number generator
if (strFunctionName == "GetRandom")
{
int iMin = (int)listParameters[0];
int iMax = (int)listParameters[1];
return s_random.Next(iMin, iMax);
}
// handle enemy movement
else if (strFunctionName == "Move")
{
int iDeltaX = (int)listParameters[0];
int iDeltaY = (int)listParameters[1];
Move(iDeltaX, iDeltaY);
}
// handle enemy direction
else if (strFunctionName == "SetDirection")
{
Direction = (EntityDirection)(int)listParameters[0];
}
// handle enemy direction flag
else if (strFunctionName == "SetAutomaticDirection")
{
AutomaticDirection = (bool)listParameters[0];
}
// handle enemy position query
else if (strFunctionName == "GetPosition")
{
AssociativeArray associativeArrayPosition =
new AssociativeArray();
associativeArrayPosition["X"] = this.X;
associativeArrayPosition["Y"] = this.Y;
return associativeArrayPosition;
}
// handle player position query
else if (strFunctionName == "GetPlayerPosition")
{
AssociativeArray associativeArrayPosition =
new AssociativeArray();
associativeArrayPosition["X"] = m_player.X;
associativeArrayPosition["Y"] = m_player.Y;
return associativeArrayPosition;
}
// handle enemy fire
else if (strFunctionName == "CastSpell")
{
Image imageSpell = Properties.Resources.EnemySpell;
int iDamage = m_enemyType == EnemyType.Wizard ? 40 : 20;
Spell spell =
new Spell(m_enemies.Room.Spells,
imageSpell, false, iDamage, Direction);
spell.X = X;
spell.Y = Y - 30;
m_enemies.Room.Spells.Add(spell);
return null;
}
return null;
}
在上述示例中值得注意的是,没有进行参数验证,也无需验证,因为验证是在编译时完成的。
为每个脚本上下文定义不同处理程序的能力允许主机函数具有不同的实现和/或上下文,具体取决于绑定了相应脚本处理程序的脚本上下文。例如,脚本中 `Move(x, y)` 函数的实现可能会影响玩家角色、非玩家角色或射弹的移动。
或者,也可以将主机函数原型直接与关联的处理程序一起注册。
// register global Sine function
HostFunctionPrototype hostFunctionPrototype = new HostFunctionPrototype(
typeof(float), "Sin", typeof(float));
m_scriptManager.RegisterHostFunction(hostFunctionPrototype, trigHandler);
// register global Cosine function
HostFunctionPrototype hostFunctionPrototype = new HostFunctionPrototype(
typeof(float), "Cos", typeof(float));
m_scriptManager.RegisterHostFunction(hostFunctionPrototype, trigHandler);
// register global Tangent function
HostFunctionPrototype hostFunctionPrototype = new HostFunctionPrototype(
typeof(float), "Tan", typeof(float));
m_scriptManager.RegisterHostFunction(hostFunctionPrototype, trigHandler);
此方法允许关联一个在同一脚本管理器中创建的所有脚本上下文之间共享的通用处理程序。一个典型的例子是注册许多脚本中需要用到的三角函数和其他通用数学函数。
注册主机模块
单独注册共享主机函数需要大量的设置代码。为了缓解这个问题,提供了一个 `HostModule` 接口,允许批量注册多个主机函数。主机模块的实现包括定义一个返回模块实现的函数原型列表的方法,以及一个 `HostFunctionHandler` 接口的处理程序方法。这提供了函数实现。以下示例说明了前面三角函数示例的另一种实现。
public class TrigonometryModule
: HostModule
{
// list of functions stored statically for one-off creation
private static ReadOnlyCollection<hostfunctionprototype /> s_listHostFunctionPrototypes;
// module constructor
public TrigonometryModule()
{
// if list created, don't do anything else
if (s_listHostFunctionPrototypes != null) return;
// create list of function prototypes
List<hostfunctionprototype /> listHostFunctionPrototypes =
new List<hostfunctionprototype />();
HostFunctionPrototype hostFunctionPrototype = null;
// add Sine prototype to list
hostFunctionPrototype =
new HostFunctionPrototype(typeof(float), "Sin", typeof(float));
listHostFunctionPrototypes.Add(hostFunctionPrototype);
// add Cosine prototype to list
hostFunctionPrototype =
new HostFunctionPrototype(typeof(float), "Cos", typeof(float));
listHostFunctionPrototypes.Add(hostFunctionPrototype);
// add Tangent prototype to list
hostFunctionPrototype =
new HostFunctionPrototype(typeof(float), "Tan", typeof(float));
listHostFunctionPrototypes.Add(hostFunctionPrototype);
s_listHostFunctionPrototypes =
listHostFunctionPrototypes.AsReadOnly();
}
// returns list of available functions
public ReadOnlyCollection<hostfunctionprototype /> HostFunctionPrototypes
{
get { return s_listHostFunctionPrototypes; }
}
// implements functions
public object OnHostFunctionCall(String strFunctionName,
List<object> listParameters)
{
if (strFunctionName == "Sin")
// implement Sine
return (float)Math.Sin((float)listParameters[0]);
else if (strFunctionName == "Cos")
// implement Cosine
return (float)Math.Cos((float)listParameters[0]);
else if (strFunctionName == "Tan")
// implement Tangent
return (float)Math.Tan((float)listParameters[0]);
// unknown function (should not happen)
throw new ExecutionException(
"Unimplemented function '" + strFunctionName + "'.");
}
}
主机模块注册与单个函数注册非常相似。
// create module instance
TrigonometryModule trigonometryModule = new TrigonometryModule();
// register module
m_scriptManager.RegisterHostModule(trigonometryModule)
未来的文章将介绍一个更高级的 Conscript 演示,其中包含脚本语言的全功能 IDE 应用程序。IDE 通过实现一个允许用户将任何 .NET 程序集加载到 IDE 中的插件系统来扩展主机模块的概念。程序集会被扫描,查找所有实现 `HostModule` 接口的类,并自动注册它们的主机函数。
错误处理
API 使用错误、编译错误和脚本运行时错误会触发 `ConscriptException`、`ParserException` 和 `ExecutionException` 的异常实例。每个异常可能包含任意数量级别的内部异常。编译器异常 `ParserException` 提供了描述词法分析或语法分析错误发生的源位置和可能的标记的附加属性。
脚本语言指南
此脚本引擎是比 Eezeescript 简单得多的自然演进,并具有功能齐全的类 C 语言。每个脚本可以由多个全局或脚本变量声明组成,后跟多个函数声明。
注释
可以在脚本中穿插注释,可以是单行注释(用 `//` 表示)或多行块注释(形式为 `/* ... */`)。
// this is a single-line comment
/* This is
a block
comment */
全局和脚本变量声明
全局声明可以在共享同一全局上下文的多个脚本中多次指定。每当遇到全局声明时,将在全局作用域中初始化关联变量,除非遇到同名的脚本变量。另一方面,脚本变量仅对同一脚本中声明的函数可访问。任一作用域的变量都用 null 值初始化。
// global variable declaration
global g_world;
// script variable declarations
var s_name, s_surname;
函数声明
函数声明需要一个函数标识符,后跟多个无类型的参数标识符。函数必须返回 null 引用、本机值或数组值。函数可以调用其他函数,包括递归自引用或注册的主机函数。
// computes the absolute value of a number
function abs(x)
{
if (x >= 0)
return x;
else
return -x;
}
语句块
语句块将多个分号分隔的语句括在一对大括号中。语句块定义函数体,并在控制语句中使用。
function main()
// function body
{
// arbitrary statement block
{
var x = 10;
x = x * 2;
}
if (x < 20)
// conditional block
{
var y = x + 5;
}
}
当用于控制语句时,仅包含一条语句的语句块可以省略大括号。
if (x >= 0)
// single statement
return x;
else
// single statement
return -x;
语句
语句是 Conscript 脚本语言的核心,可以是空语句(用分号表示)、局部变量声明、表达式、控制结构、并发控制结构或线程调用。此外,一个语句本身可以是一个用大括号括起来的语句块,从而允许任意嵌套语句块。
表达式
由于脚本语言的 C 语言特性,表达式包含各种结构。这些包括算术、逻辑和关系表达式、变量赋值和函数调用。表达式可以由任意组合的操作数和运算符组成,并遵循与 C 语言类似的运算符优先级规则。默认运算符顺序可以使用括号覆盖。表达式可以作为独立语句、数组元素引用或函数调用的参数出现。赋值也作为表达式处理,其值等于赋值的右侧。反之,独立表达式的返回值将丢失。
// simple assignment
count = rows * columns + 2;
x = 4.5
name = "joe";
found = true;
list = {}; // empty list
fullname = name + " " + surname;
// array element assignment
list[index + 1] = "Desk";
dictionary["orange"] = 15;
matrix[row][column] = 1.0;
// member assignment
dictionary.orange = 15;
// equivalent to: dictionary["orange"] = 15;
address.fullname.surname = "Smith";
// combined assignment operators
count += 5;
list += {"Chair", "Sofa"};
x -= delta;
// standalone non-assignment expressions
(1 + 2) * (3 + 4);
true;
Print("Hello World!");
// function calls
root = sqrt(x);
sortedList = sort(list);
// complex expressions
x1 = - (b + sqrt(b * b - 4 * a * c)) / (2 * a);
wet = raining && noUmbrella || plumbingProblem;
inRange = x >= lower && x <= upper;
脚本可以调用脚本定义的函数或注册为主机函数的本机定义的 .NET 代码。脚本引用的所有主机函数必须在尝试编译之前注册。
局部变量声明
局部变量的定义方式类似于脚本变量,并且其作用域仅限于定义它们的函数体。但是,局部声明可以包含基于函数调用、字面量和先前定义的变量的初始赋值表达式。如果省略赋值,则默认分配 null 值。
// declare three variables with a null value
var x, y, z;
// declare and initialise two variables
var name = "Joe", surname = "Smith";
// declare two variables but initialise only the second
var count, found = true;
// equivalent declarations
var list;
var list = null;
条件语句
Conscript 提供了两种条件语句:if-then-else 块和 switch 块。if-then-else 块基本上与其 C 语言的等价物相同。与 C 和其他 C 风格的语言不同,switch 块允许对任何类型的变量进行比较,并且需要使用语句块而不是“break”关键字来定义 case 条件。
// 'if' condition without an 'else' clause
if (count > MAX)
count = MAX;
// 'if' condition with an 'else' clause
if (x >=a && x <= b)
{
x = (a + b) / 2;
inRange = true;
}
else
{
inRange = false;
}
// switch statement example
switch (item.Type)
{
// health item
case "health":
{
player.health += 10;
if (player.health > MAX_HEALTH)
player.health = MAX_HEALTH;
}
// weapon upgrade
case "weapon":
player.weapon += 1;
// poison or mine
case "poison":
case "mine":
player.health = 0;
// unknown item type
default:
return UNKNOWN_ITEM_TYPE;
}
迭代语句
脚本语言提供了三种循环结构:“while”循环、“for”循环和“for-each”循环。“while”和“for”循环的语法非常类似于 C 语法。“for-each”语法借鉴了支持类似结构的托管语言,可用于数组元素或字符串变量的字符。
// while loop
while (!found)
{
// loop code goes here...
found = true;
}
// for loop
for (var index = 0; index < list.size; index++)
sum += list[index];
// for-each loop
foreach (key, value in dictionary)
Print(key + ": " + value);
// for-each loop (key only)
foreach (key in dictionary)
Print(key + ": " + dictionary[key]);
所有循环结构都支持“break”和“continue”关键字,分别用于退出循环或跳到下一迭代。
// process indices 4-7 only
for (var index = 0; index < 10; index++)
{
// skip if less than 4
if (index < 4) continue;
// exit loop if greater than 7
if (index > 7) break;
// do something with index here...
}
多线程
根据脚本引擎使用的执行模式,脚本可能需要长时间保留执行控制,这可能会影响主机应用程序。“yield”语句可用于触发中断,使脚本引擎将控制权交还给主机应用程序。
// process running in parallel with host
for (var index = 0; index < 100000; index++)
{
// do processing here...
// yield control every hundred cycles
if (index % 100 == 0)
yield;
}
多线程可以通过脚本引擎 API 执行多个脚本上下文来实现,或者通过使用“thread”关键字异步执行脚本定义的函数来实现。此调用方法会导致被调用的函数与脚本的主执行线程并行执行。
// semaphore variable
var finished;
// function to run in a separate thread
function RunProcess(processId)
{
// do stuff
for (var index = 0; index < 100; index++)
Print("Process " + processId + ": " + index);
// count one finished process
++finished;
}
function main()
{
// initialise semaphore
finished = 0;
// invoke two threads
thread RunProcess(1);
thread RunProcess(2);
// wait until both threads are complete
while (finished < 2)
yield;
}
并发控制
Conscript 支持通过锁定构造的等待-通知并发机制和临界区机制。
// lock constant
var QUEUE_LOCK;
// array representing queue
var queue;
// queueing process
function enqueue()
{
// initialise queue element
var value= 0;
// loop indefinitely
while(true)
{
// lock queue for modification
lock QUEUE_LOCK
{
// add element and display status
queue += value;
Print("Queued: " + value);
++value;
}
}
}
// dequeueing process
function dequeue()
{
// loop indefinitely
while(true)
{
// lock queue for modification
lock QUEUE_LOCK
{
// iterate over queue to get first element
var index, value;
foreach (index, value in queue)
{
// remove first element from queue
queue -= value;
Print("Dequeued: " + value);
// exit immediately after first element
break;
}
}
}
}
// main function
function main()
{
// initialise lock constant and queue
QUEUE_LOCK = "queue_lock";
queue = {};
// spawn queueing and dequeueing threads
thread enqueue();
thread dequeue();
// loop indefinitely to allow threads to execute
while(true) yield;
}
模块化编码
Conscript 提供了一种简单的代码包含机制,类似于 C 和 C++ 中提供的 `#include` 预处理器指令。
//-- common script --------------------
global UP, DOWN, LEFT, RIGHT
function InitialiseConstants()
{
UP = 0;
DOWN = 1;
LEFT = 2;
RIGHT = 3;
}
//-- specialised script ---------------
// include common script
include "common.cns";
function SetDirection(direction)
{
switch (direction)
{
case UP : { ... }
case DOWN : { ... }
case LEFT : { ... }
case RIGHT : { ... }
}
}
关注点
为虚拟机架构设计指令集,以适应更高级的语言,而不是反过来,具有多种优势。这些优势包括包含复杂指令、变体类型内存寻址和寄存器的能力。这种方法提供了另一个优势:复杂指令减少了脚本可执行文件的总体大小,这意味着 VM 可以更快地执行指令,而大部分处理是在 VM“本机”中完成的。
相关文章
- EezeeScript:用于 .NET 的简单可嵌入脚本语言
- 绝望地下城:一个使用 Conscript 的脚本演示游戏
- Conscript IDE:Conscript 脚本语言的集成开发环境 (IDE) 实现。
历史
- 2007 年 6 月 12 日 - 发布第一个版本