第15章 游戏中的虚拟系统

本章将介绍游戏中的虚拟系统,如对话系统,任务系统,道具系统,技能系统等。

游戏中的虚拟系统

对于游戏而言,一些虚拟的系统,或者叫逻辑系统是十分重要的,比如看似简单的rpg游戏中的对话处理,多任务及游戏情节控制系统,背包、装备和道具系统,技能系统等。由于他们都是逻辑系统,实现起来往往没有一个定式,这里仅仅给出一些示例代码和建议,至于具体实现,还要跟你的项目中的其他系统相依赖。

对话系统

对话系统对于几乎所有游戏都有用。他们的使用十分广泛,实现的形式也不尽相同。我们这里用一个简单的例子来引导大家实现自己的对话系统。
首先我们分析一下对话系统的组成部分:

  1. 进入对话系统的触发条件,就是何时开启对话,比如rpg里的对话命令,一些NPC的自言自语,或者跑到玩家旁边触发(绑定在状态机中)。
  2. 进入对话时,我要查找NPC身上所有的可用对话及条件,并返回第一个符合条件的对话。
  3. 触发进入对话的相关控制,比如暂停一些游戏控制,对话涉及的人物进入“talking”状态。
  4. 显示对话框,并进行对话框控制,比如逐字显示,跳过对话,图文混排,显示结束后等待等。根据句子类型。
  5. 句子结束后,需要有结束控制,比如对话选项的话,打开选框,跳转,及其他触发(如表情),结束对话等。
    上面是对话系统的流程,我们再看每个对话的数据结构:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    pool = {}
    pool[id] = {
    type = "instant", -- or "choise" or "wait" or "delay"
    content = "test,test,test,test!",
    choises = {
    {content = "", jump = "id",script = function() end},
    {content = "", jump = "id",script = function() end},
    },
    delay = 3 --seconds
    jump = "id",
    script = function() end
    npc = true
    }

id句子池的全局id,可以用来索引。
类型type: instant文字结束关闭对话框;choise多项选择;wait按任意键后继续;delay等待若干时间后继续。
内容content: 这个句子的内容。包括文字颜色,字体及图文混排。
选项choises: 当类型为选择时可用,列出选项,并给出每一个选项对应的跳转id和结束时触发的脚本。
跳转jump: 当有这个字段时跳转到指定字段,没有时跳转到本id的下一个id。
延迟delay: 当类型为延迟时可用。
脚本script: 本段对话结束时触发的脚本。
开关npc: 表示本句是否由npc发出。

NPC身上的话题数据结构:

1
2
3
4
5
6
7
8
9
10
npc.topics = {
{cond = function() end,id = ""},
{cond = function() end,id = ""},
}
for i,v in ipairs(topics) do
if v.cond() then
dialogSystem:newDiag(pool[id],{player,self})
break
end
end

每个npc都有自己的话题,如果没有话题则无法触发对话。根据话题的条件来触发对应的话题。触发顺序为排列顺序。
触发对话时,要传入发生对话的对象。
下面是对话系统内部的控制:

  1. 加入一个对话到current表,并标记对话的参与者。
  2. 对于current表中每一个,逐字展现对话,如果有按键按下则速度加快。
  3. 一个对话展现到末尾时,如果为choice类型或者wait类型则等待玩家回馈。否则跳转分支。
  4. 如果回馈则跳转到指定分支。
  5. 执行跳转的脚本。同时从current表移除自身。

相关绘制系统这里暂不表述了。

任务系统

任务系统也是游戏系统的重要组成。对于简单的游戏而言,唯一的任务就是通关或者打败某人。而对于多任务游戏而言,我们需要更复杂的控制。
我们把任务分为四个大的部分:

1
2
3
4
5
6
local missions = {
pool = {},
active = {},
done = {},
failed ={}
}

其中任务池是指所有尚未触发的任务,active是当前正在执行的任务,done是已经成功完成的任务,failed表示失败的任务。
下面是对于单个任务的数据结构:

1
2
3
4
5
6
7
8
9
local mission = {
name = "test",
type = "killtarget",
state = "pool",
trigger = function() end,
success = function() end,
fail = function() end,
action = function() end,
}

name名称就是任务的名字啦,
type是任务的类型,你可以根据这个任务基类写一些子类,比如限时任务,采集任务,护送任务等。
state当前的任务状态
trigger是任务的触发条件,比如当玩家到达某个位置触发。触发时,任务移动到active里面。
success是任务胜利条件,当达到条件时,任务移动到done里面,并触发胜利脚本,也在这个函数中。
fail是任务失败条件,当达到条件时,任务移动到failed里面,触发失败脚本。
action是任务在active下的特殊脚本。比如护送任务下让npc移动,比如生成特殊的敌人等。

任务控制系统实际上就是每次update(这个更新速度不必过快,没有必要)时,对上述的表进行操作,移动时进行相应的触发即可。

道具系统

道具系统由道具数据,道具管理系统,道具用户界面构成。
道具的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
item ={
id = "123",
name = "绿色的帽子",
type = "equipement", --"consume" 也可以为消耗品 --也可以为激活类的比如cost mana 10
part = "head", --部位
tag = "mission", 标签,可以是任务物品,食物等等
rarity = "normal", --稀有度
description = "装上他你就无敌了", --描述
price = 998, --价格
icon = image, --图标
effect = {charm = 5, def = 10},
pile = 1, --堆叠层数
}

人物的道具系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
role.equipement = {
head = xxx,
body = nil,
} --装备
role.package = {
item1,item2,... --其他道具
}
role.package = { --或者分几个背包,物品可以堆叠。
[1] = {
{item = xxx,count = 1}
}
}

对于装备提供的额外属性,这里要注意,一定不能用叠加的形式,而是在每次重新计算。比如:

1
2
3
4
5
6
7
8
9
10
11
12
function role:resetProp()
for k,v in pairs(role.prop) do
role.prop[k] = role[k]
end
for i,v in ipairs(role.equipements) do
local effect = v.effect
for k,v in pairs(effect) do
role.prop[k] = role.prop[k] + v
end
end
end

技能系统

人物技能的一个数据结构示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ability = {
id = 123,
name = "火球",
description = "一个巨大的火球,一定几率点燃目标"
type = "spell", --法术 也可以是被动等
costType = "mana",
cost = 50,
price = 100,
spelltime = 1,
cd = 3,
lasttime = 0,
damageType = "fire",
damage = 40,
script = function(self,target) if rnd()<0.3 then self:cast(id or "burn",target) end --30%几率造成点燃效果。
}

对于持续效果,实际上和上面装备是一样的,需要在改变是对数据重新计算。与装备不同,它的消失靠的是定时器。

其他