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:
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”
十一.Effect与Flag
CDDA 源码解析的更多相关文章
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- jQuery2.x源码解析(缓存篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...
- Spring IoC源码解析——Bean的创建和初始化
Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器 ...
- jQuery2.x源码解析(构建篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...
- jQuery2.x源码解析(设计篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...
- jQuery2.x源码解析(回调篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...
随机推荐
- 《Ansible权威指南》笔记(1)——安装,ssh密钥登陆,命令
2016-12-23 读这本<Ansible权威指南>学习ansible,根据本书内容和网上的各种文档,以及经过自己测试,写出以下笔记.另,这本书内容很好,但印刷错误比较多,作者说第二版会 ...
- [python]python try异常处理机制
#python的try语句有两种风格 #一:种是处理异常(try/except/else) #二:种是无论是否发生异常都将执行最后的代码(try/finally) try/except/else风格 ...
- 移动WEB像素相关知识
了解移动web像素的知识,主要是为了切图时心中有数.本文主要围绕一个问题:怎样根据设备厂商提供的屏幕尺寸和物理像素得到我们切图需要的逻辑像素?围绕这个问题以iphone5为例讲解涉及到的web像素相关 ...
- C#基础---Queue(队列)的应用
Queue队列,特性先进先出. 在一些项目中我们会遇到对一些数据的Check,如果数据不符合条件将会把不通过的信息返回到界面.但是对于有的数据可能会Check很多条件,如果一个数据一旦很多条件不 ...
- 关于对For循环嵌套优化的问题
1.案例描述 由于一次Java面试的笔试题,当时没有写出很好的解决方案,特此专门撰写一篇博客来加以记录,方便日后的查看 面试题目如下:从性能上优化如下代码并说明优化理由? for (int i = 0 ...
- liunx关闭防火墙
Redirecting to /bin/systemctl stop iptables.service systemctl stop iptables.service ?????? centos从7开 ...
- 怎样简单灵活地将DataTable中的数据赋值给model
最近在做的一个项目中,有13个方法都需要用到同一种处理方式:通过SQL语句从数据库获取一条指定的数据,并将该数据中的每个值都赋值给一个model,再将这个model中的数据通过微信发送出去.每个方法都 ...
- 撸一段 SQL ? 还是撸一段代码?
记得刚入公司带我的研发哥们能写一手漂亮的 SQL,搜索准确.执行快.效率高. 配合Web项目中的查询展示数据的需求,基本是分分钟完成任务. 那段时间基本是仰视的态度,每天都去讨教一点手写 SQL 的要 ...
- 关于Django 错误 查询之后结果序列化出现的问题is not JSON serializable
由于查询出来的结果是instance (实例 /对象) 无法实例化, 在model结果加 .value()
- oracle的round函数和trunc函数
--Oracle trunc()函数的用法/**************日期********************/1.select trunc(sysdate) from dual --2013- ...