一.编译
A.MinGW
1:从 https://github.com/CleverRaven/Cataclysm-DDA 下载源码
2:下载IDE CodeBlocks,http://pan.baidu.com/s/1qYNcKZ6,解压到随便哪个目录,再下载TDM-GCC-64,完整安装64位,
然后设置CodeBlocks的编译器为TDM-GCC:
3:下载 http://dev.narc.ro/cataclysm/cdda-win64-codeblocks.7z 里的WinDepend解压到CDDA的根目录,这些是依赖的静态库跟动态库
4:下载LUA 5.1 For Win并安装(需要先装有VC++ 2005)
5:在CDDA根目录下找到CataclysmWin.cbp打开工程,右键项目(Cataclysm)-> Properties -> Build targets ->
双击要编译的类型(如Relase(Lua)),然后在Pre/post build steps标签下,将Pre-build steps里的lua5.1 改为 lua
因为第6步安装好Lua,默认在系统中的环境变量名是lua而不是lua5.1,不然会找不到该命令。
6:选择对应的编译类型,然后编译。
7: 如果报错 ISSUE - "winapifamily.h" no such file or directoyr
复制这里的内容覆盖掉MinGW/include/SDL2/SDL_platform.h的内容 https://hg.libsdl.org/SDL/raw-file/e217ed463f25/include/SDL_platform.h
8:编译好后,将exe文件以及data拷贝到同一目录下(如果有多语言,贴图以及LUA,还要拷贝对应的文件夹lang,gfx以及依赖的dll到运 行目录下)
http://dev.narc.ro/cataclysm/cdda-win64-codeblocks.7z 这里有已经编译好的dll,下载直接拷贝到游戏根目录即可。
*如果不需要LUA,6、7步骤可以省略
B.VS 2015
1: 下载安装VS 2015学习免费版
2: 从 https://github.com/CleverRaven/Cataclysm-DDA 下载源码
3: 下载VS专用版WinDepend,同样解压到CDDA根目录
4:打开CDDA->msvc-full-features->Cataclysm.sln,启动VS工程
5:开始编译项目,编译完后,运行WinDepend里的copy_dll_to_bin.bat提取出所需的dll,然后在WinDepend目录下会生成个bin文件夹,将里面对应平台的dll文件全部拷贝到CDDA根目录,否则直接运行CDDA根目录下的EXE文件会找不到连接库报错。
6:如果要调试:编译完成后,将VS的DEBUG工作目录设置为CDDA根目录(因为默认工作目录是工程所在目录即msvc-full-features,但我们的EXE生成目录是在CDDA根目录,所以需要手动设置调试目录),右键目录->属性-〉调试,将$(ProjectDir)改为$(ProjectDir)..,两个..表示上一级目录的意思。
二.LUA调用C++
CDDA项目里支持LUA脚本调用C++代码,具体的做法是:
调用函数
1.在catalua.cpp里写一个你新建的函数,例如void game_test(int x, inty)
2.在class_definitions.lua的global_functions下注册这个函数,
global_functions = {
[...]
test = {
cpp_name = "game_test",
args = {"int", "int"},
rval = nil
},
[...]
}
3.然后编译的时候,如果有选LUA,则会执行命令脚本,调用generate_bindings.lua将lass_definitions.lua注册的函数warp到catabindings.cpp文件里,生成一个gamelib栈用来存放global_test的函数,添加项{"test", global_test}
4.catalua.cpp在初始化的时候,会初始化lua,将gamelib里的全局函数名注册到lua里一个叫'game'的table下面
5.LUA脚本里调用test的时候,CDDA的LUA引擎会通过'game.test'这个函数名在catabindings.cpp的gamelib寻找与之对应的c++ warp函数(global_test),然后执行global_test,而global_test里又去调用最原先在catalua.cpp里创建的game_test达到LUA调用C++的目的
game.test();
调用类
1.先在c++文件里创建一个类,例如myClass
2.在class_definitions.lua的class里注册,注意各种名字必须与c++里的一一对应
myClass = {
// 构造函数
new = {
{ "string" },
{ "int" },
},
// 变量
attributes = {
name = {type = "string", writable = true},
},
// 函数
functions = {
{name = "fuck", rval = nil, args = {"int", "string"}},
},
},
3.编译的时候,会在catabindings.cpp生成warp方法,然后在LUA里调用的时候,再从catabindings.cpp里调用对应的函数,在调用到具体的类去
三.C++调用LUA
CDDA里C++可以调用在LUA里写的函数(说白了就是在LUA脚本里写on_xx类的回调函数注册监听某种事件,在C++里触发了某种条件后,C++再调用LUA脚本里注册的对应函数
目前官方仅放出4个回调注册支持,分别是:
分别是在新玩家创建完毕、一天过去了、一分钟过去了、技能升级时触发,以"on_day_passed"为例分析这套回调的过程:
1.首先,在LUA脚本里的MOD table里注册"on_day_passed"回调函数
mods["your mod name"] = MOD function MOD.on_day_passed()
// dosome
end
2.在C++脚本里一天过去触发时的地方调用lua_callback来调用lua脚本里的这个回调
四.CDDA MOD模块执行过程
1.一个MOD的基本属性
2.初始化过程
main循环:在主菜单选完角色 -> 按开始游戏 -> 加载角色表
-> 调用game::setup()进行一些游戏的设置
-> 读取核心数据game:load_core_data
-> 初始化LUA
-> 注册gamelib和global_funcs到lua里的game table下,作为Lua里的全局函数
-> lua_dofile执行CDDA根目录下的autoexeclua等函数,用于初始化lua数据
-> load_data_from_dir 执行 data/core 目录下的核心mod
-> load_world_modfiles 读取当前世界所设定的mod文件
-> load_packs 遍历读取mods文件夹下的所有mod
-> load_data_from_dir 一个MOD的完整读取过程
-> 检查mod目录下是否存preload.lua,若存在则luadofile执行它
-> 获取并加载mod目录下的json文件
-> 检查mod目录下是否存main.lua,若存在则luadofile执行它
-> load_data_from_dir 执行save目录下当前世界的存档文件夹(save/mods)里的自定义mod(即世界创建完后,我们还可以动态地在存档文件夹里添加mod,但只能由一个mod)
-> 重复以上mod读取过程
3.MOD中的LUA脚本部分
这方面其实就是二、三里提到的LUA与C++交互的部分了
4.MOD中的JSON数据部分
五.主菜单界面的循环
menu.openging_screen的主循环在选择角色后跳到new_character_tab或者load_character_tab里,等到下一步操作。
六.游戏内战斗初始化过程
main_menu里的new_character_tab或load_character_tab在监听到选择完角色并开始游戏后:
-> world_generator->set_active_world( world ); 设置当前世界为所选的世界
-> game->setup(); 游戏初始化设置
-> load and init mod
-> DynamicDataLoader::unload_data(); 将init里的finalized置为false,然后卸载重置所有动态读取的json数据
-> load_core_data后再load_world_modfiles,加载所有mod并读取运行lua脚本,加载json数据
-> load_world_modfiles完后调用DynamicDataLoader::finalize_loaded_data(); 将init里的finalized置为true,并调用所有json对象的类的finalize()
-> 初始化各种其他的属性,比如天气,怪物之类的
-> game->load(); 读取存档,主要是将上一步初始化的那些数据(比如天气,玩家)进行赋值存档数据
-> 初始化完毕,跳到战斗内循环
七.游戏内战斗主循环过程
main的主while里的g->do_turn便是游戏的主逻辑循环了
-> g::do_turn() 一回合跑一次
-> calendar::turn.increment() 游戏时间系统,让游戏过去一回合,同时更新游戏内时间
-> if (calendar::turn.seconds() == xx) lua_callback("on_xx_passed"); LUA的各种时间类的回调便是在这里
-> u.update_body() && update_weather(); 各种状态的更新
-> handle_action(); 游戏最重要的一部分,所有操作处理集中在这里处理,包括玩家的各种按键输入
-> game::get_player_input()
-> while( handle_mouseview(ctxt, action) ) 这里阻塞循环,等待玩家操作,如果玩家没有任何操作,那么一直卡这里面
-> if( action == "TIMEOUT" ) break; 如果游戏设置为实时模式,那么就算玩家不进行任何操作,到了设定的时间后,也会强制跳出循环进行下一回合
-> draw_weather(wPrint); && draw_pixel_minimap(); 游戏实时更新不受回合影响的内容放这执行,比如播放天气动画
-> case ACTION_XX: xx(); 上一步捕获按键输入后,这一步判断要执行什么动作(比如是移动还是使用物品)
*注: 游戏的主循环并不是每帧都运行,因为这是回合制游戏,只有在上一步的handle玩家执行了操作后,才会继续新一轮循环,否则是阻塞在那等待的,除非设置为即时模式,那么每次倒计时完都会强制下一回合
八.游戏的时间系统
重要概念:
1.回合:每次g::do_turn()都算为一回合,一回合消耗游戏时间6秒,按下"."游戏便会调用player:pause()强制过去一回合,如果是实时模式,那么现实时间每隔一段时间(设定的实时频率值,比如0.5秒)就会强制调用player:pause(),可以理解为游戏在固定的时间后自动帮你按一下"."。
2.游戏时间:游戏内部有一套"日历"时间系统,用于记录游戏内部的时间流逝,与现实时间不同,只有每经过一回合,时间才会向前流动,游戏的时间,比如时、分、秒,都是根据回合来算的,秒 = (回合数 * 6) % 60
3.现实时间:现实系统时间
*注:游戏虽然属于回合制,但与传统的回合制不同,不是你打一回合,我打一回合,其实回合这个概念在游戏中也可以忽略,这个游戏应该算“半即时”制,游戏中应该只算时间概念,即所有的操作都只与时间有关,比如我挥刀10秒,敌人挥刀耗时5秒,那么我砍一次需要差不多2回合,而对方只需要1回合,当然,回合的概念是只存在于代码内部,不会在游戏里表现出来,所以你不会看到“我挥刀,敌人挥刀,等一回合,敌人打中你,再等一回合你才打中敌人”的现象。因为游戏是以你为准心,所以你一挥刀,你立刻就砍中了敌人,但此时游戏里面g::do_turn()跑了两遍,已经悄悄过去两回合了,敌人已经砍了你两刀了。
九.物品相关
1.物品初始化流程
game:setup
-> load_core_data
...
-> load_world
-> load_mods
-> load_all_mod
-> load_frome_file -> load_from_json -> load_object() 从json文件读取数据
-> load_comestible -> load_basic_info 读食谱、合成表啥的
-> set_use_methods_from_json 从json里获取物品的使用Action方法
-> actor:load 从json文件初始化action的其他属性,例如transform的msg,target等就是在这个时候读表的
-> load_map_mod
...
-> init:finalize_load
-> item_factory:finalize
-> all use_methods:finalize
每种JSON文件都有对应的读取函数
程序的开头会调用这个来初始化读取函数
init.DynamicDataLoader:initialize
-> add("skill", &Skill::load_skill)
-> type_function_map.add
-> add("item_action", &item_action)
...
然后在 load_from_json -> load_object时,会从type_function_map里寻找当前该json的type对应的加载函数
2.物品使用流程
game_turn
-> use_action -> game:use_item 玩家使用物品触发Action
-> player:use(item_index)
-> set item as last_use_item
-> switch item type 根据物品类型决定使用方式
tool -> invoke_item
-> item:use_fun_call 如果是item,则在item的方法表里找到对应的物品的调用函数并执行
-> iuse_funname()
-> consume_charges 如果物品是会消耗能量的(比如手电筒),则进行能力是耗损计算
food -> consume
book -> red
关于物品的使用,比如头灯,使用完后变成头灯(开),CDDA并没有用什么来控制物品状态的变化,也没有记录物品的状态属性,而是用了一个小技巧,
用两个物品分别来表示物品的“开/关”状态,比如“头灯”和“头灯(开)”,然后在他们两者USE时执行一个Action,这个Action用来将转换他们。
"use_action": {
"type": "transform", 动作类型为转换,即表示该物品在“使用”后会转变为另一个物品
"msg": "You turn the head torch on.", 使用时会提示的信息
"target": "wearable_light_on",
"active": true,
"need_charges": 1,
"need_charges_msg": "The head torch batteries are dead." 能量不足时提示的信息
}
“头灯”在使用后,会触发transform转换函数,将他变为“头灯(开)”
3.增加并注册物品使用函数
三种注册物品使用函数的方法(最终结果都是注册到iuse_function_list里)
注册的地方在:Item_factory::init()
1.添加类
-> add_actor
-> iuse_function_list:add (new xx_actor 继承 actor)
2.直接在C++里写静态函数,然后给个名字丢到list里
-> add_iuse
-> iuse_function_list:add ("xx", &iuse:xx -> iuse_function_wrapper 继承 actor)
3.LUA注册,在LUA脚本里调用C++的game_register_iuse,然后在C++里再register_iuse_lua添加
..lua.dofile()
-> game_register_iuse
-> item_controller:register_iuse_lua
-> iuse_function_list:add (new lua_iuse_wrapper 继承 actor)
// 调用
iuse_function_list[name] -> use_function (iuse_actor)
{
or heal_actor // 1.
...
or iuse_function_wrapper // 2.
or lua_iuse_wrapper // 3.
}
list[name].call -> use_fun.call -> actor.use
*注1:创建新的item_action,必须在 item_actions.json里注册这个action,否则会读取不到。
在初始化的时候,load_from_json -> load_object时会读到item_action.json,然后用它来初始化item_actions列表,
比如打火机的打火动作的描述:
{
"type" : "item_action",
"id" : "firestarter",
"name" : "Start a fire quickly"
},
然后在游戏中就会在物品描述中看到使用这个物品的使用说明:"Start a fire quickly"
*注2:item_actions.json文件有一个全局的,但MOD没必要修改这个文件,创建一个同名文件丢到
MOD目录下即可,游戏会自动把MOD目录下的此文件识别并加载,并附加到全局的列表里
4.替换原有物品
data里的默认物品可以替换,只要在MOD文件夹里创建同名json对象,就会自动替换,因为MOD比核心基础data后加载
eg:
{
"id": "survivor_light",
"name": "survior light"
}
在自己的MOD里也创建一个
{
"id": "survivor_light",
"name": "幸存者头灯"
}
那么就会将原来的"survior light"替换为"幸存者头灯"
5.物品组
{
"type": "item_group",
"id": "guns_pistol_common",
"//": "Pistols commonly owned by citizens and found in many locations.",
"items": [
[ "glock_19", 85 ],
[ "glock_22", 35 ]
]
}
将现有的一组物品注册为一个物品组,供地图上显示物品用,比如指定地图上某个点掉落物品组中的某个物品
后面的数字表示该物品出现的概率,比如"glock_19"出现的概率是 85/(85+35)
十.地图
1.地形定义放在terrain.json
{
"type" : "terrain",
"id" : "t_brick_wall_line",
"name": "brick wall"
}
2.家具定义放在furniture.json
{
"type" : "furniture",
"id" : "f_file_cabinet",
"name": "filing cabinet"
}
3.房间定义
{
"type": "mapgen",
"om_terrain": "combogarageA_first",
"weight": 0,
"method": "json", 可通过json数据定义房间,或者通过lua脚本动态生成房间,这里是通过json来描述房间内的布局
"object": {
"fill_ter": "t_floor", 表示房间里没有定义砖块属性的地方,默认由什么terrain来填充
"rows": [
... 一个矩阵,用来表示房间的形成,符号的意义参加以下地形、家具等的符号描述
".|-----+--+-| | |.",
".| | --- |.",
".|d | |.",
".|d + |.",
".| | | h |.",
...
],
"terrain": { 地形的描述,比如"-"就表示rows中的该符号位置位置表示是墙
"#": "t_shrub",
"+": "t_door_c",
"-": "t_wall",
".": "t_grass",
},
"furniture": { 家具的描述,比如"."就表示rows中该符号位置有个沙发,当然,该符号也是上诉地形中草地的符号,所以最终结果表示在草地上有个沙发
"0": "f_fireplace",
".": "f_sofa",
},
"toilets": { 厕所,rows中的"t"表示该位置有厕所,因为厕所比较特殊(比如厕所里可以有东西),所以当独放一块
"t": {}
},
"place_items": [
{ "item": "cannedfood", "x": [ 6, 20 ], "y": 5, "chance": 10 }, 地图上掉落的物品,名字是物品组的名字,[6, 20]指x轴6到20中随机一个,chance表示概率
{ "item": "guns_pistol_common", "x": 8, "y": 4, "chance": 100 }
],
"place_monsters": [
{ "monster": "GROUP_ZOMBIE", "x": [ 2, 21 ], "y": [ 2, 21 ], "chance": 2 } 地图上刷僵尸,名字是僵尸组的名字
],
"lua": "game.add_msg(\"这是你第一次来到这间屋子\")" 第一次进入此房间就会触发的LUA脚本,与上面的method:lua用来生成房间的Lua脚本不同
}
}
这里会引用1、2里定义的terrain和furniture内容来拼凑房间
*注2:可以多个房间使用同一个om_terrain,那么在生成房间时,会随机使用同一om_terrain中的某一个
其中生成某个房间的概率,是看该房间的权重(weight)来决定的(默认是1000,500是1/3的概率)
4.以上步骤只是定义了房间,还需要注册房间,才可以在游戏里被引用到
{
"type" : "overmap_terrain",
"id" : "combogarageA_first",
"name" : "房间在游戏里显示的名字",
"rotate" : true, 如果rotate为false,则在地图中该房间默认朝北,并且不能旋转
"sym" : 94, 貌似是该房间在大地图上显示的符号的ASCII码符号?
}
5.注册房子(房间的组合),这里注册的房子将在游戏地图里随机刷出,这才是正在的房子注册
可以通过第4里注册的房间来拼凑成一个房子,[x,y,z]表示房间出现的位置。
{
"type" : "overmap_special",
"id" : "combohouseA",
"overmaps" : [
{ "point":[0,0,0], "overmap": "combohouseA_first_north"},
{ "point":[1,0,0], "overmap": "combohouseA_second_north"},
...
}
房间名字最后面的"_north"表示该房间相对该房子的朝向,如果房间的rotate为false,则这里不能加上方向修饰
以上的json定义表示,注册这样一个房子:房子id为combohouseA,由两个房间组成,其中在[0,0,0]处有一个朝向北的combohouseA_first,
在它的右边,也就是[1,0,0]处有一个朝向北的combohouseA_second房间
更多的属性描述参见“MAPGEN.md”