Answers:
脚本是一种编程抽象,其中(概念上)您有一个程序(脚本)在另一个程序(主机)中运行。在大多数情况下,编写脚本的语言与编写主机的语言不同,但是任何程序内部程序抽象都可以视为脚本。
从概念上讲,启用脚本的常见步骤如下(我将对主机使用伪c,对脚本使用伪lua。这不是确切的步骤,但更像启用脚本的一般流程)
在主机程序中创建一个虚拟机:
VM m_vm = createVM();
创建一个函数并将其公开给VM:
void scriptPrintMessage(VM vm)
{
const char* message = getParameter(vm, 0); // first parameter
printf(message);
}
//...
createSymbol(m_vm, "print", scriptPrintMessage);
请注意,公开函数(print
)的名称不必与函数本身(scriptPrintMessage
)的内部名称匹配。
运行一些使用该功能的脚本代码:
const char* scriptCode = "print(\"Hello world!\")"; // Could also be loaded from a file though
doText(m_vm, scriptCode);
这里的所有都是它的。然后,该程序以以下方式运行:
你打电话doText()
。然后将控制权转移到虚拟机,该虚拟机将执行里面的文本scriptCode
。
脚本代码找到以前导出的符号print
。然后它将控制权转移到该功能scriptPrintMessage()
。
当scriptPrintMessage()
完成时,控制将转移回虚拟机。
当所有文本输入scriptCode
执行doText()
完毕后,将结束,并且控制将在后面的行中转移回您的程序doText()
。
因此,通常来说,您要做的就是在另一个程序中运行一个程序。从理论上讲,没有脚本是无法做的,但是如果没有脚本,脚本就无法做,但是这种抽象使您可以轻松地做一些有趣的事情。他们之中有一些是:
关注点分离:一种常见的模式是使用C / C ++编写游戏引擎,然后使用诸如lua之类的脚本语言编写实际游戏。做对了,游戏代码可以完全独立于引擎本身进行开发。
灵活性:脚本语言通常是解释性的,因此,对脚本进行更改并不一定需要重建整个项目。做对了,您甚至可以更改脚本并查看结果,而无需重新启动程序!
稳定性和安全性:由于脚本在虚拟机中运行,因此如果执行正确,则有错误的脚本不会使主机程序崩溃。当允许用户向游戏编写自己的脚本时,这一点特别重要。请记住,您可以创建任意数量的独立虚拟机!(我曾经创建一个MMO服务器,其中每个匹配项都在单独的lua虚拟机上运行)
语言功能:使用脚本语言时,根据您选择的宿主语言和脚本语言,您可以利用每种语言必须提供的最佳功能。特别是,lua的协程是一个非常有趣的功能,很难用C或C ++实现
脚本不是完美的。使用脚本有一些常见的缺点:
调试变得非常困难:通常,常见的IDE中包含的调试器并非旨在调试脚本中的代码。因此,控制台跟踪调试比我想要的要普遍得多。
一些脚本语言(例如lua)具有调试功能,可以在某些IDE(例如Eclipse)中加以利用。这样做非常困难,老实说,我从未见过脚本调试和本地调试一样有效。
顺便说一句,Unity开发人员对此事极度缺乏兴趣是我对他们的游戏引擎的主要批评,也是我不再使用它的主要原因,但我离题了。
IDE集成:您的IDE不太可能知道从程序中导出了哪些功能,因此,诸如IntelliSense之类的功能不太可能与您的脚本一起使用。
性能:脚本是通常解释的程序,适用于架构可能与实际硬件不同的抽象虚拟机,因此脚本执行的速度通常比本地代码慢。但是,某些虚拟机(例如luaJIT和V8)做得很好。如果您大量使用脚本,这可能会很明显。
通常,上下文更改(主机到脚本和脚本到主机)非常昂贵,因此,如果遇到性能问题,可能需要将其最小化。
您如何使用脚本取决于您自己。我已经看到用于执行任务的脚本很简单,例如加载设置,以及使用非常薄的游戏引擎以脚本语言制作整个游戏一样复杂。我什至曾经看到过一个非常奇特的游戏引擎,它混合了lua和JavaScript(通过V8)脚本。
脚本编写只是一种工具。如何使用它制作出色的游戏完全取决于您。
printf
在您的示例中使用(或至少使用printf("%s", message);
)
通常,您可以将某些本机函数绑定或公开给Lua(通常可以使用实用程序库来实现,尽管您可以手工完成)。这样,当您的游戏执行Lua代码时,Lua代码就可以调用您的本机C ++代码。从这个意义上讲,您假设Lua代码只是调用本机代码是正确的(尽管Lua有其自己的标准功能库;您无需为所有内容都调用本机代码)。
Lua代码本身由Lua运行时解释,它是您通常在自己的程序中链接为库的C代码。您可以在Lua主页上了解有关Lua如何工作的更多信息。特别地,Lua并不是您所猜测的“未编译的类”,尤其是在考虑C ++类的情况下,因为在实践中C ++几乎不会动态编译。但是,您的游戏运行的Lua运行时和Lua脚本创建的对象确实会占用游戏系统内存池中的空间。
像Lua这样的脚本语言可以以多种方式使用。正如您所说的,您可以使用Lua调用主程序中的函数,但也可以根据需要从C ++端调用Lua函数。通常,您会建立一个界面,以使您所选择的脚本语言具有一定的灵活性,因此您可以在一系列场景中使用该脚本语言。像Lua这样的脚本语言的妙处在于它们是解释而不是编译的,因此您可以即时修改Lua脚本,因此您不必坐等您的游戏进行编译,只需要您重新编译即可。做的不适合您的口味。
Lua命令仅在C ++程序要执行时才被调用。这样,仅在调用代码时才解释该代码。我想您可以将Lua视为与主程序分开执行的bash脚本。
通常,您想对可能需要更新或迭代的内容使用脚本语言。我已经看到很多公司将其用于GUI,因此它为界面提供了很多可定制性。如果您知道他们是如何创建Lua界面的,则还可以自己修改GUI。但是您可以使用Lua进行其他多种选择,例如AI逻辑,有关武器的信息,角色对话等。
大多数脚本语言(包括Lua)都在虚拟机(VM)上运行,该虚拟机基本上是一个将脚本指令映射到“真实” CPU指令或函数调用的系统。Lua VM通常以与主应用程序相同的过程运行。对于使用它的游戏来说尤其如此。Lua API为您提供了一些功能,您可以在本机应用程序中调用这些功能来加载和编译脚本文件。例如luaL_dofile()
,将给定脚本编译成Lua 字节码,然后运行它。然后,该字节码将由运行在API内的VM映射到本机机器指令和函数调用中。
将诸如C ++之类的本地语言与脚本语言连接的过程称为binding。在Lua情况下,其API提供的功能可帮助您向脚本代码公开本机功能。因此,您可以例如定义一个C ++函数say_hello()
并使该函数可从Lua脚本中调用。Lua API还提供了通过C ++代码创建变量和表的方法,这些变量和表在脚本运行时将可见。通过组合这些功能,可以将整个C ++类公开给Lua。相反也可能,Lua API允许用户修改Lua变量并从本地C ++代码调用Lua函数。
大多数(如果不是全部)脚本语言都提供API,以促进脚本代码与本机代码的绑定。大多数代码也被编译成字节码并在VM中运行,但有些可能逐行解释。
希望这有助于澄清您的一些问题。
首先,脚本语言通常不进行编译。这是通常将它们定义为脚本语言的很大一部分。相反,它们经常被“解释”。这本质上的含义是,存在另一种语言(通常是一种编译的语言)正在实时读取文本并逐行执行操作。
该语言与其他语言的区别在于脚本语言趋向于更简单(通常称为“高级”)。但是,它们也趋向于变慢一些,因为编译器倾向于优化编码“人为因素”带来的许多问题,并且对于机器而言,生成的二进制文件往往更小且读取速度更快。另外,使用已编译的程序,从运行另一个程序来读取正在运行的代码的开销也较小。
现在,您可能会想:“好吧,我觉得这要容易一些,但是为什么有人为了某种额外的易用性而放弃所有性能呢?”
在这种假设下,您可能不会孤单,但是脚本语言的轻松程度(取决于您对它们的处理方式)很值得在性能上做出牺牲。
基本上: 如果开发速度比正在运行的程序的速度更重要,请使用脚本语言。 游戏开发中存在很多这样的情况。特别是在处理诸如高级事件处理之类的琐碎事情时。
编辑:lua在游戏开发中趋于流行的原因是因为它可以说是世界上最快(如果不是最快)的公开脚本语言之一。但是,以这种额外的速度,它牺牲了一些便利性。话虽如此,它仍然可以说比直接使用C或C ++更方便。
重要修改: 经过进一步研究,我发现关于脚本语言的定义还有很多争议(请参阅Ousterhout的二分法)。将语言定义为“脚本语言”的主要批评是,它对所解释或编译的语言的语法或语义均不重要。
尽管通常被认为是“脚本语言”的语言通常是按传统方式进行解释而不是编译,但其定义为“脚本语言”的长短实际上取决于人们如何看待它们以及其创建者如何定义它们。
一般而言,如果一种语言符合以下条件(根据上面的链接文章),就可以轻松地将其视为脚本语言(假设您同意Ousterhout的二分法):
此外,如果一种语言被设计为与另一种编程语言(通常不被认为是脚本语言)互动和起作用,那么它通常是一种脚本语言。