前言
过年这几天写了几篇Angular2的学习笔记,算是有点“不务正业”,这次为大家带来我正在参与开发的一款游戏。这个小的系列大概有三、四篇组成,随着项目进展我会把这个坑填上。系列的内容是从程序的角度观察和反思这个游戏,包括它构建的基本思想和设计、编码过程中遇到的困难、重构来降低复杂度等。这个项目目前仅依赖于cocos2d,实际上他就是使用cocos2d来作为引擎的。需要说明的是,这个系列记述的是软件的开发过程和反思,不是一些结论性的东西来供读者参考,请不要把这些文章当做建议或者教程,如果你在阅读过程中有更好的简介和建议欢迎提出。
项目简介
这个游戏从程序的角度看(不包括设计、美术、音乐等),主要是两部分:cocos2d上面建立的框架、Lua实现的脚本。游戏2d的,主要玩法是玩家在一个棋盘式(格子)的地图上从一个点到另一个点,地图上有各种地形、道具、陷阱和墙壁,玩家何以拾取道具、破坏陷阱、合成道具从而完成游戏。玩家可以进行一些动作如走、跑、匍匐等,玩家有血量、体力等属性,以后可能还会加入战斗系统。游戏是半即时的,例如随时间的流逝体力会上升(站立时)以及滚石之类的陷阱等。最后游戏会加入对steam的支持,以上架为目标。
设计
Json:
为了方便地图编辑、数值修改,游戏数据采用Json记录。
分析需求得出游戏涉及到的对象包括:
包:游戏按包管理和加载资源,玩家可以开发自己的包来扩展游戏,包与包之间包含扩依赖关系,例如玩家包依赖于系统包,那么玩家设计的地图即可使用系统包的地形而不用重新开发。
地图:游戏包括很多地图,地图组织了一个地图块的矩阵,每个块可能包含地形(terrain)、道具(tool)、陷阱(trap)、墙(wall)、角色(role)。
地形、道具、陷阱:他们分配在格子上,在需要的时候调用Lua脚本。
墙:在格子之间放置,具有方向性(A到B阻塞、B到A阻塞或者双向阻塞)。
角色:可以在格子间移动,具备一些数值。
上面提到的这些可以提供统一的Lua脚本支持、Json中的属性可以直接在Lua中获取,这样玩家可以自由定义属性并添加脚本。Json提供对cocos2d的动画的直接支持,在Json中可以定义动画,这样在Lua中可以直接更改显示的动画。
游戏包含两个Json文件:
packages.json :
{
"default":
{
"Name":"default",
"Dir":"default/",
"DescriptionFile":"description.json"
},
"UnitTest":
{
"Name":"UnitTest",
"Dir":"UnitTest/",
"DescriptionFile":"description.json"
}
}
package.json :
{
"Roles":
{
"default-Roles-player1":
{
"Name":"default-roles-player1"
,
"ScriptFile":"roles/player1/script/script.lua"
,
"FunctionName":"default-roles-player1-update"
,
"FunctionMask":1
,
"Animations":
[
{
"Name":"default-roles-player1-animations-default"
,
"PLIST_File":"roles/player1/animations/default/default.plist"
,
"Frames":
[
"default-roles-player1-animations-default-frame1.png"
,
...
,
...
]
,
"SpanTime":"0.33"
,
"IsLoop":"true"
}
,
...
,
...
],
"CurrentAnimation":"default-roles-player1-animations-default"
}
},
"Tools":
{
"default-tools-knife":
{
"Name":"default-tools-knife"
,
"ScriptFile":"tools/knife/script/script.lua"
,
"FunctionName":"default-tools-knife-update"
,
"Animations":
[
{
"Name":"default-tools-knife-animations-default"
,
"PLIST_File":"tools/knife/animations/default/default.plist"
,
"Frames":
[
"default-tools-knife-animations-default-frame1.png"
,
...
,
...
]
,
"SpanTime":"0.33"
,
"IsLoop":"true"
}
,
...
,
...
],
"CurrentAnimation":"default-tools-knife-animations-default"
}
}
,
"Traps":
{
"default-traps-cirrus":
{
"Name":"default-traps-cirrus"
,
"ScriptFile":"traps/cirrus/script/script.lua"
,
"FunctionName":"default-traps-cirrus-update"
,
"Animations":
[
{
"Name":"default-traps-cirrus-animations-default"
,
"PLIST_File":"traps/cirrus/animations/default/default.plist"
,
"Frames":
[
"default-traps-cirrus-animations-default-frame1.png"
,
...
,
...
]
,
"SpanTime":"0.33" ,
"IsLoop":"true"
}
,
...
,
...
],
"CurrentAnimation":"default-traps-cirrus-animations-default"
}
}
,
"Terrains":
{
"default-terrains-dirt":
{
"Name":"default-terrains-dirt"
,
"ScriptFile":"terrains/dirt/script/script.lua"
,
"FunctionName":"default-terrains-dirt-update"
,
"Animations":
[
{
"Name":"default-terrains-dirt-animations-default"
,
"PLIST_File":"terrains/dirt/animations/default/default.plist"
,
"Frames":
[
"default-terrains-dirt-animations-default-frame1.png"
,
...
,
...
]
,
"SpanTime":"0.33"
,
"IsLoop":"true"
}
,
...
,
...
],
"CurrentAnimation":"default-terrains-dirt-animations-default"
}
}
,
"Walls":
{
"default-walls-wall":
{
"Name":"default-walls-wall",
,
"ScriptFile":"walls/wall/script/script.lua"
,
"FunctionName":"default-walls-wall-update"
,
"Animations":
[
{
"Name":"default-walls-wall-animations-default"
,
"PLIST_File":"walls/wall/animations/default/default.plist"
,
"Frames":
[
"default-walls-wall-animations-default-frame1.png"
,
...
,
...
]
,
"SpanTime":"0.33"
,
"IsLoop":"true"
}
,
...
,
...
],
"CurrentAnimation":"default-walls-wall-animations-default"
}
}
"Maps":
{
"default-maps-map1":
{
"Name":"default-maps-map1"
,
"Description":"this is the description of default-maps-map1"
,
"Width":10
,
"Height":10
,
"TileWidth":56
,
"TileHeight":56
,
"ScriptFile":"maps/map1/script/script.lua"
,
"FunctionName":"default-maps-map1-update"
,
"Data":
{
"Tiles":
[
[{"Terrain":{"Back":{"RefName":"default-terrains-dirt"},"Front":{"RefName":"default-terrains-ditr"}},"Trap":{"RefName":"default-item-traps-cirrus"},"Tool":{"RefName":"default-item-tools-knife"}},{},{},{},{},{},{},{},{},{}]
,
[...]
,
[...]
,
[...]
,
[...]
,
[...]
,
[...]
,
[...]
,
[...]
,
[...]
],
"Walls":
[
{
"Wall":"default-walls-wall",
"Tile_X":2,
"Tile_Y":2,
"Direction":"RIGHT",
"Via":"FORWARD",
"IsRotate":false
}
,
...
,
...
],
"Roles":
[
{
"RefName":"UnitTest-roles-player1",
"Tile_X":2,
"Tile_Y":2,
"RoleType":"PLAYER",
"ScriptFile":"roles/player1/script/script.lua",
"FunctionName":"instanceUpdate",
"FunctionMask":1
}
,
...
,
...
]
}
}
}
}
对于packages.json很简单,包含了每个包的名称、目录及介绍,暂时没有涉及到包依赖,不过基本可以预见,以后包依赖的功能可以通过数组的方式简单的引入。
对于package.json,每一个包对应一个这样的json,包含了这个包的详细定义。
其中涉及到数据和实例的概念,与编程语言中的概念不同,后边我会介绍。首先是这个包的数据,包含Roles、Tools、Traps、Terrains、Walls和Maps。这些数据(集)是游戏的基础,其中Maps中定义了这个游戏中的实例,分别针对每个格子说明了它包含的内容以及所有墙壁和角色。
Roles、Tools、Terrains、Walls他们的内容相似,我以一个Role的定义为例:
{
"Name":"default-roles-player1"
,
"ScriptFile":"roles/player1/script/script.lua"
,
"FunctionName":"default-roles-player1-update"
,
"FunctionMask":1
,
"Animations":
[
{
"Name":"default-roles-player1-animations-default"
,
"PLIST_File":"roles/player1/animations/default/default.plist"
,
"Frames":
[
"default-roles-player1-animations-default-frame1.png"
,
...
,
...
]
,
"SpanTime":"0.33"
,
"IsLoop":"true"
}
,
...
,
...
],
"CurrentAnimation":"default-roles-player1-animations-default"
}
Name定义了一个全局唯一的名字,ScriptFile定义了这个数据对应的脚本、FunctionName定义了脚本的回调函数名、FunctionMask定义了这个数据需要注册的Lua回调(会在Lua部分详述),Animations定义了这个数据的一系列动画,CurrentAnimation定义了当前使用的默认动画。
这里面关于Lua的部分(ScriptFile、FunctionName、FunctionMask)、Animation的部分(Animations)是通用的,凡是支持Lua和Animation的定义都应该使用这样的格式。
在Maps中定义了所有的地图,首先采用矩阵的方式来表示所有的块及其中所有的实例、通过列表表示的墙和角色,他们基本都可以通过Key的值很容易的理解其功能,我就不详细解释了。
关于数据和实例:
每一个地图上显示的物件都有一个数据存在,这个数据实例化许多实例。以小刀为例,有一个小刀的实例定义在Tools中,这定义了小刀的基本内容,在Map的矩阵中定义了小刀的实例,实例可能包括这个小刀的附魔、临时的属性等。这样可以设计许多具有不同特点的小刀,例如不同的贴图动画,在数据中声明所有的贴图,然后在实例中为不同的小刀做不同的标记,在Lua中通过标记来判断播放不同的动画。这些小刀又都具有数据中的相同特点,例如可以切断藤蔓。
这里所说的实例和实例化与编程语言(这里是C++)中的概念不同,请注意区分。可以这样说,小刀的数据实例化了一个数据类,小刀的实例实例化了一个实例类,这里的实例和实例化是语言层面的,进而小刀的实例实例化了小刀的数据,这里是游戏逻辑层面的。以后我用到的大部分“实例”和“实例化”是指游戏逻辑层面的。
Lua:
数据和实例可以绑定Lua脚本,在Json部分已经有定义,框架在加载Json时会执行指定的Lua脚本文件,将FunctionName指定的函数加载到Lua虚拟机中。框架采用事件调用的方式,在需要的时候调用这个函数,传入一个字符串标明是哪个事件。例如小刀数据的Lua文件为knife.lua,FunctionName为Update,框架在载入时会执行knife.lua,加载Update函数,以后所有关于小刀数据的回调都会使用这个Update函数。当在初始化时会调用这个Update并传入INIT标明这是在初始化时调用的。FunctionMask是一个二进制位标记的掩码,例如01b代表初始化,10b代表更新,11b代表同时注册初始化和更新。通过这个掩码框架会关闭不需要用到的回调,提高效率。
在Lua中可以通过一些全局函数来获取当前对象的属性,例如在lua代码中:
name = lua_get_string("name")
可以获取当前对象的属性,set怎可以设置属性,Json中的字段可以直接调用的Lua中。
框架会记录当前Lua调用所指向的对象,这个对象是静态的,因此无法提供对多线程的支持,这可能会在以后需要的时候优化。
框架设计:
框架包含三部分,这里不讨论cocos2d,仅仅是我实现的内容。
基础库、单元测试库、游戏主体
| 游戏主体
|——————————————————————
| 基础库 | 单元测试库
|——————————————————————
| cocos2d-x
我曾经找过一个google的单元测试库,但似乎和cocos存在兼容问题,后来打算自己写一个,这个库不是主要任务,因为通过断言已经可以满足大部分需求。
基础库包括对游戏支持的通用组件,目前主要实现了单例类、Lua调用类、KV类、矩阵类、对象管理器等,我会把这个库开源出来和大家分享。
游戏主体是游戏的实现。
下一篇文章中我会着重对这些库进行介绍。