作者laechan (挥泪斩马云)
看板mud
标题[闲聊] 排程
时间Mon Jan 22 23:45:34 2018
最近刚在 sanc 写好这个东西。
以前在改 tmi2-mudlib 时有稍微提过这东西
┌─────────────────────────────────────┐
│ 文章代码(AID):
#1JZ-f8qq (mud) [ptt.cc] Re: [闲聊] tmi2-mudlib 的更改 │
│ 文章网址:
https://webptt.com/cn.aspx?n=bbs/mud/M.1401940552.A.D34.html │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 文章代码(AID):
#1JaHsSIj (mud) [ptt.cc] Re: [闲聊] tmi2-mudlib 的更改 │
│ 文章网址:
https://webptt.com/cn.aspx?n=bbs/mud/M.1402019228.A.4AD.html │
└─────────────────────────────────────┘
简单的说,我有写一个系统物件叫 times_check.c,它的设计是,
透过启用心跳,它每一心跳时间都会去 call 自己的 heart_beat
,这时 heart_beat 可以这样写:
int heart_beat()
{
t=time();
if(判断到 t 这个时间有需要执行的呼叫时)
call_other(欲呼叫的目标,"
times_check",...);
return 1;
}
上面的 times_check 函数是固定的。有了这支程式,比方以 sanc
的拍卖指令档 /cmds/std/_blarket.c 为例,该指令档内就可以新
增 times_check 函数:
int times_check(string str,mixed vars)
{
// 当 vars 是有值的时
if(sizeof(vars)>0)
return cmd_blarket(vars[0]);
// 当 vars 是无值的时, 我用这个来判断 times_check 函数是
// 否为第一次被 times_check.c 所呼叫, 第一次呼叫的话不会
// 带值
.
.
return 1;
}
以 sanc 的 blarket 指令为例,拍卖一件物品的流程可固定如下
blarket -auc 欲拍卖的物品档名 它会将物品叫出来拍卖
blarket -continue 广播目前的拍卖情况
blarket -continue 广播目前的拍卖情况
blarket -end 结标
我希望上面分别是 2 秒後、20 秒後、40 秒後、60 秒後来进行
时,则每一拍卖对 times_check.c 物件的设定就可这样做
t=time();
foreach(tmp in tmps) // 对每一个欲拍卖物品
{
times_check_ob->set_times_check("-auc "+物品档名,t+2);
times_check_ob->set_times_check("-continue",t+20);
times_check_ob->set_times_check("-continue",t+40);
times_check_ob->set_times_check("-end",t+60);
t=t+68; // 拍卖结束後 10 秒再进行下一个拍卖
}
也就是说,假设我有 10 件物品要卖,它就呼叫 times_check.c
10x4 = 40 次,「将之後要执行的东西通通先设好」,然後就可
以坐等 times_check.c 帮我们在约定好的时间,逐一执行该做的
事情。
则 times_check.c 的资料结构,合理的设计自然是
mapping data=([
"以 time() 字串化做为 key 值":({ 要做的事情1, 要做的事情2, ..}),
"以 time() 字串化做为 key 值":({ 要做的事情1, 要做的事情2, ..}),
.
.
]);
则时间到的时候:
str=""+time(); // 字串化
if(data[str]) // 代表这时间有需要执行的东西
{
foreach(need_to_do in data[str])
{
对 need_to_do 做资料解析;
call_other(要呼叫的目标,"times_check",要带的参数群..);
}
}
// 该做的事情做完了,就把 data[str] 拿掉
map_delete(data,str);
也就是说,我的设计是
一、我先写一支 times_check.c 它每秒都会呼叫一次自己的
heart_beat 函数。
二、然後比方我想让 sanc 的拍卖指令 blarket 支援排程拍
卖,我就指定呼叫的模式,让 times_check.c 在约好的
时间对 blarket 指令做指定的呼叫,再透过这个呼叫,
反过来对 times_check.c 做指定的设定。
三、剩下的事情就可以全部交给 times_check.c 在约好的时
间帮我执行拍卖。sanc 预定在月底正式执行排程拍卖。
四、既然 blarket 可以,就代表其它东西只要依样化葫芦,
也可以做排程。
目前 sanc 的 times_check.c 也应用在 boat 上面,跟其它
mud 一样,sanc 大陆与大陆之间也有定期航班的设计,一般
只需让 boat 继承 /std/boat.c 即可(tmi2-mudlib)。
但是 boat 基本上流程就是这样
抵达 A 地点, 设定船只的出口为 A 地点
几秒後, 广播即将驶离
几秒後. 关闭出口, 广播已驶离将前往 B 地点
几秒後, 广播航行中
几秒後, 广播航行中, 即将抵达 B 地点
抵达 B 地点, 设定船只的出口为 B 地点
几秒後, 广播即将驶离
几秒後. 关闭出口, 广播已驶离将前往 A 地点
几秒後, 广播航行中
几秒後, 广播航行中, 即将抵达 A 地点
抵达 A 地点, 设定船只的出口为 A 地点
.
.
上面看似为一段设定,实际上真正的一段设定只有
抵达 某 地点, 设定船只的出口为 某 地点
几秒後, 广播即将驶离
几秒後. 关闭出口, 广播已驶离将前往 下一 地点
几秒後, 广播航行中
几秒後, 广播航行中, 即将抵达 下一 地点
上面只要做好规划,後面其实都是 loop 的呼叫,那麽自然可
以交给 times_check.c 来做。
我没记错的话,目前大部份 mud 的 boat 写法都是类似的,
比方以 fly_next 为例
void fly_next()
{
now=find_object_or_load(plane[i][1]);
tell_room(now, data["short"]+"靠港了。\n");
tell_room(this_object(),GRN"老船长: "+plane[i][0]+"到了。\n"NOR);
now->set("hide_exits/enter",base_name(this_object()));
now->set("long2",query("out_short"));
set("exits/out",plane[i][1]);
call_out("hurry_up_msg",plane[i][2]-5);
}
我不满意传统 boat 的原因就在於它是以 call_out 来做为主
要流控,而我不太喜欢使用 call_out,与其每一艘船都在那边
call_out,不如让 times_check.c 来控制所有的船,类似航管
员的角色,当船只很多且进出频繁时,同一时间必然有
1.有船正要进来
2.有船正要离开
优秀的航管员不会因为一次要顾很多艘船的进出就混乱,所以
就是看程式怎麽写而已。
在撰写 times_check.c 时也要注意存取资料的频繁度,例如说
我希望 times_check.c 能在 1/31 晚上 21:00 帮我执行拍卖,
则我在 1/22 的现在设定好排程後,times_check.c 必然要储存
这项设定,才能在 1/31 晚上 21:00 帮我执行。
但是执行後的 set_times_check 却是不需要储存的,因为当天
晚上它就可以把该卖的东西都卖完了。所以我的写法是
times_check_ob->set_times_check 这种呼叫会储存起来
times_check_ob->set_times_no_save 这种呼叫不会储存
也就是说我在宣告变数时实际上就分为两个
mapping data;
static mapping tmp_data;
以 static 去宣告的变数,在进行 save_object 时是不会储存
的。所以我上面的程式段实际上为
t=time();
foreach(tmp in tmps) // 对每一个欲拍卖物品
{
times_check_ob->set_times_no_save("-auc "+物品档名,t+2);
times_check_ob->set_times_no_save("-continue",t+20);
times_check_ob->set_times_no_save("-continue",t+40);
times_check_ob->set_times_no_save("-end",t+60);
t=t+68; // 拍卖结束後 10 秒再进行下一个拍卖
}
既然 no_save 就代表一旦在执行中 update times_check.c,
这些设定就会被清除不会保留下来,是以一般都会对该物件加
设 set("pre_clean",1); 或类似的做法,来防止这类的物件被
系统自动 reset。
times_check.c 还有个需注意的事项,就是要避免让它在每一
心跳时间呼叫 heart_beat 时,执行 loading 很重的工作。
例如若只是一些简单的呼叫及简单的物件资料设定,那即便是
几十个物件也都是瞬间(毫秒)就能执行完毕,就不致於影响每
秒要做的事情。
(而且实务上,也很少每一秒都是 loading 很重)
以上,一点心得分享。
Laechan
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 122.117.106.224
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/mud/M.1516635944.A.0A0.html
1F:推 smmoon : 只能推了 114.32.144.77 01/23 08:13
2F:推 ghostmote : 好久没看到技术文118.163.154.205 01/23 17:07
总算有时间可以补充点东西。
欲使用排程的物件 times_check.c
│ │
│对 times_check.c 设定时间及工作 │
├───────────────→┤呼叫 set_times_check 函数
│ │
│ 然後等约定的时间到的时候 │
│ │
│times_check.c 第一次呼叫该物件 │
├←───────────────┤
根据该呼叫 │ │
└─→ │times_check.c->set_times_no_save│
│令 times_check.c 多次呼叫上面的 │
│函数去设定几秒後要做哪些工作 │
├───────────────→┤然後 times_check.c 移除该排程
│ │只留 set_times_no_save 的设定
│ │
│ 然後等约定的时间陆续到了时 │
│ │
都是去呼叫 │times_check.c 在约定的时间呼叫 │
times_check
├←───────────────┤
函数 │times_check.c 在约定的时间呼叫 │
├←───────────────┤
│times_check.c 在约定的时间呼叫 │
├←───────────────┤
│ . │
│ . │
│ . │
│ │
│ 直到全部的呼叫都已结束 │
然後不同的工作排程允许做叠合,彼此之间不会互相影响。
上面可以 work 的条件就是 times_check.c 会周期时间去判断当下的
时间(time())是不是有什麽工作要做,有三种实现做法
1.利用拥有心跳的物件会每一秒呼叫一次自己的 heart_beat 函数
2.或是利用 preload 先载入该物件,然後在其 create 函数内使用
call_out 的语法,再於其 call_out 的目标函数内亦使用 call_out
(也就是 loop call out)
3.或是利用其它 call_out 物件,例如典型的 weather_d.c 天气物
件,假设该天气物件每 60 秒(每分钟)都会固定 call_out 什麽函
数,那就可以利用该函数来做为排程控制。(精准度设为"分"即可)
然後再以排程拍卖为例,一般的 mud,多是使用 call_out 做为拍卖
流控的手段,而且会判断拍卖期间是不是有玩家持续出价,有的话就
会一直延後拍卖结束的时间。
这种做法并不利於使用约定好的排程来控制。
所以 sanc 有另外写一支拍卖用的程式 blarket,它的流程全部都可
由拍卖举办者来控制,亦即要五分钟後结标、十分钟後结标、甚至是
30 分钟後才结标都可以,只要下 blarket -end 即可结标。
(blarket 语源为 black market = 黑市,即取黑市拍卖会的意思)
它的最大好处就是使用者方便控制流程,又不会使用到 call_out 这
个 loading 较高的东西,在 sanc 以这个指令举办特殊的拍卖会是
wiz 专用的,玩家则可以 blarket -bet 来竞标,没记错的话我也有
把这支程式放在 tmi2_mudlib_v3_改 里头。(因为作者是我)
※ 编辑: laechan (122.117.106.224), 01/23/2018 18:08:50
3F:推 typers : 推L大对mud的热情 36.234.4.226 01/23 19:44
4F:→ typers : 但heart_beat为单位的排程,真心不推 36.234.4.226 01/23 19:49
5F:→ laechan : 现在电脑效能都很好滴122.117.106.224 01/23 20:10
6F:推 kyoe : 後期开发的mud大多已经利用h_b来取 60.250.92.145 01/25 10:17
7F:推 kyoe : 代callout,我们叫他timed/scheduled 60.250.92.145 01/25 10:21
8F:推 kyoe : 不过都是新的才用,旧的懒改,哈哈 60.250.92.145 01/25 10:25
9F:推 kyoe : 还是要推推,现在在搞的人越来越少了 60.250.92.145 01/25 10:28
我个人目前有想过在 sanc 实装「地上的物品经过一段时间後会
风化」的设定,以前是想用被动触发式,物品被 move 到地上时
就 set("move_times",time());,但是这样就需要 init 函数的
辅助,因此只能局限在特定种类的物品。
至於 call_out 或 heart_beat 则差不多是同时期想到的其它做
法,用 heart_beat 主要是把该物品比方屍体 corpse 当成生物
,该物品本身就有心跳,那自然可以顺利判断,而且把它当生物
本身也有很多好处(虽然不合理)。
再之後,则产生以 times_check.c 来跑的做法:
1.玩家阵亡时,呼叫出屍体,并做 set_times_no_save 的设定
并辅以 set("remove_times",time()+几秒後);
2.times_check 依照设定,分段去
call_other(这具屍体,"times_check",带不同的参数);
3.最终 times_check 会去移除屍体。在上述执行过程中,如果
times_check 被 update 或 reborn,屍体也会被 look_d.c
依照 query("remove_times") 的判断来移除。
(玩家进入房间时会呼叫 look_d.c 的 look_room 函数观看
房间,利用这一点)
以上是闲聊,刚好有想到。
其实最终都会回到一个关键问题:如果同一时间出现大量屍体呢?
(所以 sanc 没有屍体...虽然不合理)
国外有篇文章
http://lpmuds.net/smf/index.php?topic=1171.0
比方以底下这句
The "Performance" file in the root directory of FluffOS's
source (couldn't find online copy) says not to give objects
heartbeats
unless really necessary, and to do as little in
the heart_beat function as possible.
那其实以下我觉得都对
1.尽量不使用 heart_beat (比方使用 call_out)
2.尽量减少使用 heart_beat (比方只让少数物件有 heart_beat)
那我个人的看法是 call_out 跟 heart_beat 都可以用,但也都
应该少用,unless really necessary。比方只是单纯希望几秒後
去呼叫什麽,秒数不长,呼叫完就没了,那我就会用 call_out,
但如果这类呼叫一直出现(比方喝药水、施法的过程中都会出现)
,我就会考虑 call_out 以外的做法,比方在 sanc 我采取的做
法就是瞬喝药水、瞬施法术(虽然不合理):
> cast pray
[瞬间]
你缓缓念道: 深眠於心灵深处的潜在力量,藉神之力苏醒吧!
你开始念起古老的咒文: ~ 9 ~ ~ ~ 祈祷术 ~
你感觉自己体内的魔力变得活性化,头脑也灵活了不少!
[然後马上要再 cast 时]
> cast pray
你要等一下才可以再施法喔。
heart_beat 也是一样,通常某些我觉得不得已需使用的场合,
该物件都不会是恒久存在的(times_check.c例外),也就是说就
算我让某物件 set_heart_beat(1),我也会让它尽量快点消失,
或者是将它消失的机制写得完善一点。(例如sanc的副本系统)
我一般在使用 heart_beat 时会参考两个标的
1.user 的 heart_beat
2.monster 的 heart_beat
sanc 玩家的 heart_beat 函数其实密密麻麻写了蛮多段东西,
我大概就是以这个来评估我写的其它 heart_beat 物件,跑起
来会不会出问题,目前看起来大概没啥问题。
最後,目前 sanc 避免心跳过程中产生错误的做法只有一个:
catch(call_other(目标物件,"函数",...));
总而言之就是懒人做法 :)
※ 编辑: laechan (122.117.106.224), 01/25/2018 11:44:39
10F:推 msrvoice : 技术文, 好久没看到了, 居还还看懂 180.217.170.77 01/25 22:44
11F:推 taily : l大,程式参数都满长的:D 210.64.180.213 01/26 17:37
12F:推 kruz : 推一下 24.6.220.133 01/30 03:29
13F:推 darkmoon1725: 推推 59.115.58.244 02/02 21:48