CDDA 源码解析

一.编译

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:

CDDA 源码解析

3:下载 http://dev.narc.ro/cataclysm/cdda-win64-codeblocks.7z  里的WinDepend解压到CDDA的根目录,这些是依赖的静态库跟动态库

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,不然会找不到该命令。

CDDA 源码解析

6:选择对应的编译类型,然后编译。

CDDA 源码解析

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,下载直接拷贝到游戏根目录即可。

CDDA 源码解析

*如果不需要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)..,两个..表示上一级目录的意思。

CDDA 源码解析

二.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}

CDDA 源码解析

4.catalua.cpp在初始化的时候,会初始化lua,将gamelib里的全局函数名注册到lua里一个叫'game'的table下面

CDDA 源码解析

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个回调注册支持,分别是:

CDDA 源码解析

分别是在新玩家创建完毕、一天过去了、一分钟过去了、技能升级时触发,以"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 源码解析

四.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数据部分

五.主菜单界面的循环

CDDA 源码解析

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”

十一.Effect与Flag

上一篇:session 测试用例详解


下一篇:Overview & Change Log