作者laechan (小太保)
看板mud
标题[闲聊] 副本系统
时间Wed Apr 16 14:39:33 2014
这个最近刚完成,分享一下。
首先,取游戏里头某个现成区域的某几个房间,例如取底下
011 014-015
| |
012-013
011 是「起点」,015 是「终点」,它们位於同一个区域目录,而且
它们只是 001~100 的「其中一段」,比方 011 可能还有往西的出口
、015 可能还有往东的出口等等。
我定义所取出的这一段区域为「副本基底区域」。
然後我们写一个 instance_room.c 当做这个副本的专用房间,它会
定义一些东西、宣告一些东西、...。有了这房间,这时就可以这样
做(比方定义 INSTANCE_ROOM = 这个副本专用房间)..
// files = 所取出来的房间档名
foreach(room_file in files)
{
iroom=clone_object(INSTANCE_ROOM);
iroom->set("origin_file",tmp);
oroom=find_object_or_load(room_file);
keys_data=keys((mapping)oroom->query_ob_data());
foreach(tmp in keys_data)
iroom->set(tmp,oroom->query(tmp));
irooms[tmp]=iroom;
}
上面的意思就是说,以 011 为例,「区域的 011(也就是 oroom)」
的所有资料,都复制到「副本的 011(也就是 iroom)」,然後再让
一个 mapping irooms 用来储存对映关系:
irooms["/x/xxx/room/011"] = 011 的 iroom;
irooms["/x/xxx/room/012"] = 012 的 iroom;
.
.
但是以区域的 011 来说,它可能有两个出口
"south" : "/x/x/room/012",
"west" : "/x/x/room/010",
因为副本的 011 资料是 cp 自区域的 011,所以这时假如在副本
的 011 往南走,会走到「区域的 012」,而不是「副本的 012」
,因此,做完上述的 foreach 後,还需要再做底下的 foreach
foreach(room_file in files)
{
iroom=irooms[room_file];
keys_data=keys((mapping)iroom->query("exits"));
foreach(tmp in keys_data)
{
if(irooms[iroom->query("exits/"+tmp)])
iroom->set("exits/"+tmp,irooms[iroom->query("exits/"+tmp)]);
else
iroom->delete("exits/"+tmp);
}
}
上面的意思就是说,副本的 011,south 这个出口所接的 012 也
在副本区域的范围内时(在 irooms 找得到 012),就把副本的011
的 south 改成「副本的 012」:
原本 "south":"/x/xxx/room/012"
改成 "south":(物件)副本的 012
而 west 这个出口所接的 010 并没有在副本区域的范围内,这时
则把 "west" 这个出口给删掉。
这样所取出的副本 011~015 就能确实「截头去尾」。
要这样改有个前提,就是要针对游戏本身房间的移动做出相对映的
修正。例如游戏一般是把 "south" 後面所接的东西预设为字串,
但是在副本区域里面,"south" 所接的则是物件,因此要做相对映
的修改,例如..
原本 exit_file=room->query("exits/"+exit);
me->move_player(exit_file,MOVE_MSG);
改成 exit_file 以 mixed 而非 string 宣告
mixed exit_file;
则对 move 或 move_player 来说 exit_file 不管是字串或
是物件,一般都是接受的(它有内设判断)
产生完副本房间後,要把玩家 move 到起点的方法如下..
ppl->move(irooms["/x/xxx/room/011"]);
到这里,就能产生副本区域,以及把玩家叫进去副本区域。
这时要说明的是,某一段基底区域,可能同时为多个副本所使用,
因此副本的生怪一定不是套用基底区域的设定,而是由每一个副本
所控制的,这时就产生了一个 mapping 资料如下..
mapping instance_mobs=([
"/x/xxx/room/011":(["/x/xxx/mob/bat1":1,"/x/xxx/mob/bat2":2]),
"/x/xxx/room/012":(["/x/xxx/mob/bat1":2,"/x/xxx/mob/bat2":2]),
.
.
]);
也就是说可能我们某个副本的 011,会产生 bat1 一只 bat2 两只
,012 会产生 bat1 两只 bat2 两只,...
那麽,接着上面的 foreach,我们就可以让副本产生的同时,生怪
亦完成:
foreach(room_file in files)
{
iroom=irooms[room_file];
keys_data=keys(instance_mobs[room_file]);
foreach(tmp in keys_data)
{
for(i=0;i<instance_mobs[room_file][tmp];i++)
{
mob=clone_object(tmp);
mob->add("id",({"INSTACE_MOB"}));
mob->move(iroom);
}
}
}
上面的意思就是说,每一个 iroom 都按照 instance_mobs 的配置
,把怪物 clone_object 出来後移到 iroom,配置几只就 clone出
几只、配置几种就 clone 出几种。
三个 foreach 分别做底下三件事
一、产生出基底区域的副本(clone)
二、将副本区域的出口设定完毕
三、让每一个副本房间产生出怪物,并让部份房间有心跳
最後,让终点有心跳
irooms["/x/xxx/room/015"]->set_heart_beat(1);
为什麽要有心跳呢?比方说 015 是终点,我们希望玩家抵达终点
後会「依序触发」底下效果
1.怪物大喊:人类!受死吧!
2.玩家把怪物杀光後,出现一只 /x/xxx/npc/man.c
3.man 说:人类!感谢你解救了我们!
4.玩家拿到 10 金币
5.终点出现 out 的出口
这时候 INSTANCE_ROOM 的 heart_beat 就可以这样写
// 副本房间的这个函数每一秒会被呼叫一次
int heart_beat()
{
int flags,t;
object *usr,ppl,room=this_object();
string origin_file=room->query("origin_file);
flags=(int)query("instance_flags");
t=time();
switch(origin_file)
{
// 针对终点这个房间
case "/x/xxx/room/015":
// 去做 flags 流程判断
switch(flags)
{
// 流程1: 玩家一开始进入的时候(房间有怪物)
case 0:
tell_room(room,"怪物:人类!受死吧!\n");
set("instance_flags",1);
break;
// 流程2: 副本里的怪物全死光後
case 1:
if(!present("INSTANCE_MOB",room))
{
clone_object("/x/xxx/npc/man")->move(room);
tell_room(room,"man:人类!感谢你解救了我们!\n");
set("instance_flags",2);
}
break;
// 流程3: 终点的所有玩家拿到 10 金币
case 2:
usr=all_inventory(room);
foreach(ppl in usr)
if(userp(ppl))
ppl->add("gold",10);
set("instance_flags",3);
break;
// 流程4: 房间出现 out 的出口比方接往 001
case 3:
room->set("exits/out","/x/xxx/room/001");
// 所有流程结束,让房间停止心跳
room->set_heart_beat(0);
break;
}
break;
}
return 1;
}
这是一种以 flags 做为流程控管方式的做法,每执行一个流程
,就让 flags+1,则透过 heart_beat 的定时被呼叫,就能确保
副本可依序执行每一个流程。基本上终点通常都需要心跳,其它
房间就视自己的需要决定要不要让它们有心跳。
上面有一行
string origin_file=room->query("origin_file);
因为对该副本来说,每一个房间都是 INSTANCE_ROOM,所以我加
了一个 origin_file 栏位用来纪录它是 base on 哪个房间,它
不一定要是 base_name(基底房间),只要是足以识别的栏位即可
这样心跳函数就可以依 origin_file 这个栏位来做 switch,决
定哪个房间要执行什麽流程。
至於其它细节部份就与各游戏的风格有关,这里就不详述。
这个副本系统已经在圣殿完成并实装,圣殿采取的是以副本脚本
物件做为流程控制,跟上面以 INSTANCE_ROOM 的做法大同小异,
为方便解说所以举 INSTACE_ROOM 的做法为例,其本质是差不多
的。目前已见到的几个好处..
一、任何游戏内「现存的区域」都可以拿来当成副本区域
└包括国家区域、帮派区域、任务区域、..
二、任何游戏内「现存的怪物」都可以拿来当成副本怪物
└包括国家怪物、帮派怪物、任务怪物、..
三、同一段区域可为不同副本共同使用
└线上游戏《幻想神域》就有采取这样的偷懒做法
四、同一基底区域可同时间产生多个副本
└这样就不会产生玩家在同一个区域抢怪抢资源的问题
五、甚至同一个副本可依情况「取不同的区域」
└这在线上游戏《暗黑破坏神2、3》有看过
六、以 heart_beat 搭配 flag 的写法可办到非常多的事
└亦即现存的其它游戏的副本「理论上」都能参考到自己的
游戏内,而且不会花太多时间去做设定就能办到
原则上游戏内的区域越大越多、怪物与 NPC 数量越多,能写成
的副本数量也会越多。线上游戏《幻想神域》的缺点,就是它们
一开始就是以「副本」为主,导致游戏本身相当依赖副本的数量
、变化性、有趣度及升级实用度,而 mud 不一样,mud 本身就
有相当数量的区域..
数量 :区域多、怪物多、npc 多,可写成的副本就多
变化性:只要让每个副本取不同的区域,至少就有基本的变化性
有趣度:透过 random 让玩家每次进同一副本都能有不同体验
实用度:副本相对於 mud 只是一个「辅助系统」,例如可设定
玩家在副本里打怪「可获得更多金钱、经验值」「可得
到在一般区域打不到的东西」这类的
以上,一点心得分享。
Laechan@sanc
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 210.61.157.53
※ 文章网址: http://webptt.com/cn.aspx?n=bbs/mud/M.1397630376.A.BB6.html
※ 编辑: laechan (210.61.157.53), 04/16/2014 14:41:28
1F:推 nosod :这太专业了 有请msr大大 219.68.232.98 04/17 13:14
2F:推 happyhero :推~ 49.158.118.57 04/18 03:58
3F:推 msrvoice :你的 idea 真多, 推~ 111.235.224.91 04/23 03:13
4F:推 tawi :!!!! 163.21.235.250 05/15 11:46