1.怎样具体设计一个游戏呢?程序员使用一个编译器将设计文档中的具体说明编写成实际的功能程序,美术设计人员使用Photoshop和3ds max之类的图像处理和生成软件将概念艺术和框架转化为具体的图形,而音乐制作人员则利用一个MIDI生成器或其他的跟踪软件将他们脑海中的声音转化成音乐游戏。那么,问题来了,没有工具或器械能够将故事情节和游戏角色也“输入”进去。
2.解决的方法:
①让程序员在游戏引擎中手工编写出所有这些编码。以游戏的物品为例,为描述物体,要写一个结构体:
typedef struct _Item
{
char * pstrName; //物品的名称
int iType; //物品的具体类型
int iPrice; //在物品店的价格
int iPower; //具有的能量数量
}Item;
其中iType是物品的类型,游戏引擎需要利用这个数据了解该物品被使用时应具有什么功能。类似这种描述物品功能的常量应该事先定义:
const HEAL = 0;
const MAGIC_RESTORE = 1;
const ARMOR_REPAIR = 2;
const TELEPORT = 3;
这为选择物品类型提供了一个非常实用的方法。如果一个物品类型是Heal,那么它存放的就是游戏玩家的健康值。下一个是魔力值,接着是修复装甲,最后是在一定的游戏条件下,游戏玩家由一个游戏世界快速跳到另一个游戏世界中。
iPower的作用是无论当你准备使用这个物品实现什么样的处理,该物品都应该达到这个数量或者这种程度。即如果物品想要存放HP,而且iPower=32,那么玩家就可以取出这32点生命值并使用它们。
这样就可以定义很多物品了,物品的定义暂且忽略,不过会出现一个问题,那就是物品的平衡问题,该问题贯穿整个游戏开发,并不是一件小事情,要不断地进行修改,但是你在游戏引擎中修改后会花费大量的时间进行重新编译,更加糟糕的是,重新编译的代码中有99.9%的内容并没有改变。
这种方法的弊端已经出现了:首先,当你直接将你的物品描述实现在游戏代码之中时,你就需要重新编译和它相关的所有内容,浪费了大量的时间;其次,是它的组织结构。RPG的游戏引擎问题也很复杂。
②通过将逻辑和具体实现相互分离来改善这种方法:
现在问题的关键是如何将游戏代码和游戏内容加以分离。
用第①种方法定义物品的代码是:
ItemArray[1].pstrName = "Magic Potion Lv 6";
ItemArray[1].iType = MAGIC_RESTORE;
ItemArray[1].iPrice = 250;
ItemArray[1].iPower = 60;
现在,将等号右边的所有内容提取出来,存放到某个ASCII文件中:
Magic Potion Lv 6
MAGIC_RESTORE
250
60
这两种方法表示形式相同,唯一的区别是它所封装的所有C/C++代码都是被分离的,在经过以下步骤装载到游戏中:
(1)打开文件并确定物品描述数组里面的哪个索引是用来存储相关内容的。可采取循环方式
(2)读取第一个字符串并将其存放到变量pstrName中
(3)读取下一行,如果本行的内容是HEAL,就将变量iType赋值为HEAL,以此类推
(4)读取下一段内容,将其由字符串类型转化为整型并将其存放到变量iPrice中
(5)读取下一段内容,将其由字符串类型转化为整型并将其存放到变量iPower中
(6)重复上面的(1)~(5)步,知道所有的物品都被赋值为止
这样,每次运行程序的时候,程序将会在这个ASCII文件中读取数据,而不用重新加载程序代码以修改物品数据,这就是游戏脚本编程的概念。这个例子告诉我们所有类型的脚本编程背后的基本准则——如何避免硬编码
3.简单来说,硬编码是指当你想把物品直接写入到游戏引擎中时所做的工作。也就是一种以一种严格的、固定的和难以编写的方式来编写程序和数据
4.第②种方法已经可以满足大部分的需求了,但是却无法从根本上对这些问题加以解决,是因为我们迟早要用到一些独特而又复杂的物品,这样的系统所能实现的功能是非常有限的。但是如果对单个物品进行编码,比如加入if条件判断写入引擎,那么又陷入了前面硬编码的误区。于是,脚本就用到了。脚本可以使你真正在游戏引擎之外编码,然后再载入游戏引擎中执行
5.那么脚本是如何运行的呢?脚本和传统变成非常相似,唯一的区别在于运行时它们是怎样被加载和执行的。在了解脚本运行过程之前,先了解一下普通程序是如何运行的。
用高级语言编写的代码最终都会转换为低级的,可被机器识别的形式。原因是机器并不能识别这种高级语言,需要编译器将C/C++这种语言转化成为最低级的、字节流形式的纯代码,即转化成为机器码。但机器码难以理解,故一般被写成更加容易理解的汇编语言形式。
一旦代码被编译了,一切都会变得更加迅速。编译器将所有编译的代码交给一个称为linker(链接器)的程序,它将输入的大量的指令集封装成一个带有一定头信息的、简洁紧凑的可执行文件,并最终生成一个.EXE(或是其他操作系统使用的扩展名)文件。当你运行这个可执行文件时,操作系统将调用程序装载器program loader(通常简写为loader),它负责将代码.EXE文件中抽取出来并将其装载到内存中。然后程序装载器将通知中央处理器内存中第一条需要处理的指令的地址,也称为程序入口点(如C/C++主函数main()的入口地址)。接着,程序就可以执行了。
简单来说,这基本上就是计算机科学背后的哲学体系:将问题和算法转化成为高级语言代码,将高级语言代码转换成为低级语言代码,让低级语言代码通过CPU加以执行,从而(有望)解决问题。
那么理解脚本的运行过程就简单了不少。与上述过程不同的是,脚本编译器不能将源码转化为任何类型CPU的机器码,因为这些代码不会直接运行在CPU上,而是通过虚拟机运行的。虽然虚拟机和CPU非常相似,但它们二者仍有一些不同——虚拟机是软件而CPU是硬件。它们的基本工作是获取下一条将要执行的指令,分析指令将要完成的功能并执行该条执行。不同的是虚拟机只能识别它特定的那种汇编语言(字节码)。
虚拟机的另一个特性是,至少在游戏脚本编程的内部,它通常不是孤立的。相反,它是一个特殊的“模型”,可产生其他程序或与之集成。与CPU很相似,CPU也需要和主板、RAM等东西集成,单纯的CPU是没有什么用处的。与虚拟机集成的程序被称为主应用程序,我们最终也是为这个程序编写脚本。
因此,一个脚本系统并不仅仅只是为自己设计一个高级的、C/C++形式的语言,它还需要创立一种低级的汇编语言,即虚拟机器码。
除了一套运行环境,虚拟机还为运行的脚本和主应用程序提供一个通信通道,即交互界面
主应用程序会提供具有一组功能函数的运行脚本,这些功能函数被称为API
6.脚本系统的基本类型:
时刻牢记:大型而又复杂的特征列表看起来的确很好,但是当你不需要它们的时候,它们就只会使你的程序结构凌乱,运行缓慢
①面向过程的语言系统和面向对象的语言系统:
这些系统使用高级的、结构化的或者是面向对象的语言来编写脚本,这些脚本随后被编译成能够在虚拟机内部运行的虚拟机器码,或者不加编译,直接由解释器解释执行。系统使用的虚拟机或解释器将会与主应用程序相集成,使得主应用程序可以调用脚本并和脚本进行交互。特点是非常灵活、自由,事实上可以适用于任何一种主要的计算任务
②基于命令的语言系统:
通常会像LOGO语言一样,非常具体,它们全部由接受一个到两个参数的针对具体程序的命令组成。如下面一段代码:
MovePlayer 10, 20
PlayerTalk "Something is hidden in these bushes..."
PlayAnim SEARCH_BUSHES
PlayerTalk "It's the red sword!"
GetItem RED_SWORD
形成这个虚拟语言的命令都是非常明确的,它们都针对于非常具体的某一种RPG游戏,都针对于非常具体的某一种RPG游戏,很少甚至没有什么灵活性。
③动态链接的模块系统:
当一个复杂的脚本在一个虚拟机上运行时,它的运行速度明显要比纯粹的机器码直接在CPU上运行的速度慢得多。为了避免这个问题,许多游戏采用动态链接脚本模块。即一些C/C++程序段,这些程序段像游戏本身一样被编译成纯粹的机器码,并且在运行时被链接、载入。由于使用常见的C/C++语言编写,运行速度特别快,而且功能强大。
动态链接模块通过游戏向它们提供的API接口和游戏进行通信。通过使用这种API接口,模块可以检索和修改游戏的状态信息,从而从外部控制游戏。通常被称为mods
④编译型代码和解释型代码:
编译型代码就是那些称为机器码的一系列指令,它们就是通过将那些人类可以识别的代码转化而成的机器可以识别的代码。
解释性代码是通过解释器的程序来运行的。解释器是很难实现的,一方面,它们几乎要具有编译器所有的复杂语言分解功能函数;另一方面,它们又要足够迅速地解决这些问题从而能够达到实时的效果
⑤现有的脚本编程方法:
(1)Ruby:
是一种完全面向对象的脚本语言,侧重于系统管理方面的工作。具有一系列很好的高级特性,如垃圾收集、动态库装载,以及多线程。解释型语言
(2)Lua:
Lua是轻量级的强大的扩展编程语言,面向过程的脚本设计系统。最明显的特征是可以用它编写的程序来对它自身进行拓展。因此,它的核心代码很少,通常是用户来实现附加的特征。也能很好地和C/C++进行交互
(3)Java:
Java虚拟机,JVM,可以很容易地调用本地接口JNL和C/C++程序进行结合,因为它广泛应用于专业级的电子商务中,所以JVM对于编译脚本来说是最优的多线程运行环境,且这门语言本身也很灵活而且高度的面向对象