《使用MUDCORE框架从零开发LPMUD游戏》系列内容,通过教程既能熟悉 MUDCORE 框架的使用,也能学习LPCMUD游戏的开发,版权所有 mud.ren。
上一节我们自定义了登录实现,玩家出生在我们自己的游戏环境中,但是,目前玩家连线对象和玩家对象都还是框架示例的对象,分别是 /mudcore/system/object/login.c 和 /mudcore/system/object/user.c 。我们每个游戏都有一些独有的功能,比如玩家属性不同的游戏是不一样的,为了满足特定的游戏开发,我们可以自定义这二个对象直接自己实现login.c和user.c。
自定义注册很简单,只需要我们在globals.h中指定自己的LOGIN_OB和USER_OB对象,就可以实现游戏连线和玩家对象调用我们自己开发的,不再使用mudcore默认的对象。
下面我们开发实现这个功能,另外还让注册的玩家有以下属性:

我们知道玩家对象也是生物对象,为了代码复用,我们把生物对象共同的特性单独放置在 inherit/living.c,方便所有生物对象继承,而这个模块又继承了mudcore框架的CORE_LIVING模块。mudcore内置的CORE_LIVING提供了以下生物接口:
inherit _ACTION;
inherit _ATTACK;
inherit _COMMAND;
inherit _CONDITION;
inherit _DBASE;
inherit _MESSAGE;
inherit _MOVE;
inherit _NAME;
inherit _SAVE;
inherit _TEAM;
int is_living() { return query_temp("living"); }
默认生物对象提供了基本的生物特性,没有实现生物属性和心跳功能,属性是游戏特色功能,不同游戏有不同的属性设置,mudcore框架没有内置,所以要自己实现。我们现在增加心跳和属性模块:
新建 /inherit/attribute.c,代码如下:
// 角色属性接口ATTRIBUTE
#include <dbase.h>
// 设置属性值
int set_attr(string attr, int value)
{
return set("attr/" + attr, value);
}
// 更新属性值
int add_attr(string attr, int value)
{
return add("attr/" + attr, value);
}
// 获取属性值
int query_attr(string attr)
{
return query("attr/" + attr);
}
// 力量
int query_str()
{
return query_attr("str");
}
// 敏捷
int query_agi()
{
return query_attr("agi");
}
// 体格
int query_vit()
{
return query_attr("vit");
}
// 智力
int query_int()
{
return query_attr("int");
}
// 灵巧
int query_dex()
{
return query_attr("dex");
}
// 幸运
int query_luk()
{
return query_attr("luk");
}
// 最大HP
int query_max_hp()
{
return query_attr("max_hp") + query_vit() * 10;
}
// 最大MP
int query_max_mp()
{
return query_attr("max_mp") + query_int() * 5;
}
// 当前HP
int query_hp()
{
return query_attr("hp");
}
// 当前MP
int query_mp()
{
return query_attr("mp");
}
// 攻击
int query_attack()
{
return query_str() * 8 + query_luk() / 5;
}
// 防御
int query_defense()
{
return query_vit() * 2;
}
// 法术攻击
int query_m_attack()
{
return query_int() * 6 + query_luk() / 5;
}
// 法术防御
int query_m_defense()
{
return query_int() / 2;
}
// 体力恢复力
int query_hp_mend()
{
return query_vit() / 5;
}
// 灵力恢复力
int query_mp_mend()
{
return query_int() / 3;
}
//恢复力
int query_mend()
{
return query_hp_mend() + query_mp_mend();
}
// 恢复
int set_hp_full() { return set_attr("hp", query_max_hp()); }
int set_mp_full() { return set_attr("mp", query_max_mp()); }
void set_all_full()
{
set_hp_full();
set_mp_full();
}
// 清零(die)
int set_hp_zero() { return set_attr("hp", 0); }
int set_mp_zero() { return set_attr("mp", 0); }
void set_all_zero()
{
set_hp_zero();
set_mp_zero();
}
// 判断
int is_hp_full() { return query_hp() >= query_max_hp(); }
int is_mp_full() { return query_mp() >= query_max_mp(); }
int is_all_full()
{
return is_hp_full() && is_mp_full();
}
新建 /inherit/heart_beat.c,代码如下:
// 生物心跳控制HEART_BEAT
void heart_beat()
{
}
修改 inherit/living.c 文件,继承心跳模块和属性模块,并增加一些基础功能,代码如下:
// 生物特征模块
inherit CORE_LIVING;
inherit _ATTRIBUTE;
inherit _HEART_BEAT;
void create()
{
seteuid(0); // export_uid
}
// 判断是否是有生命的,包括 disable_living 的角色
int is_character() { return 1; }
// 角色激活
void setup()
{
seteuid(getuid(this_object()));
set_heart_beat(1);
enable_living(); // COMMAND 中定义的生物激活方法
#ifdef CHAR_D
CHAR_D->setup(this_object());
#endif
}
// 从游戏中移除这个生物
void remove()
{
destruct(this_object());
}
因为我们更新了inherit模块,需要更新头文件,直接运行create_include inherit更新/include/_inherit.h文件。
现在,实现我们自己的连线对象,/obj/login.c,代码如下:
inherit _DBASE;
inherit _SAVE;
void time_out();
string query_save_file();
void logon()
{
call_out("time_out", 60);
if (interactive(this_object()))
set_temp("ip_number", query_ip_number(this_object()));
LOGIN_D->login(this_object());
}
void net_dead()
{
debug_message(this_object() + " net_dead!");
remove_call_out("time_out");
call_out("time_out", 1);
}
/**
* 以下为自定义方法
*/
void time_out()
{
if (objectp(query_temp("user_ob")))
{
return;
}
if(interactive(this_object()))
write("\n您花在登录的时间太久了,下次想好再来吧。(-_-)!\n");
destruct(this_object());
}
string query_save_file()
{
string id;
id = query("id", 1);
if (! stringp(id)) return 0;
return sprintf(DATA_DIR "login/%s", id);
}
在 /include/globals.h 中定义 LOGIN_OB 为当前对象文件后即可生效。
然后,我们实现玩家对象 /obj/user.c,代码如下:
inherit _LIVING;
// 判断是否 user 对象,和 efun userp() 稍有区别
int is_user() { return 1; }
void net_dead()
{
set_temp("net_dead", 1);
set_heart_beat(0);
call_out("user_dest", 60);
say(query("name") + "断线了。\n");
}
/**
* 以下为自定义方法
*/
string get_id()
{
string id;
id = geteuid();
if (!id) id = getuid();
if (!id) query("id");
return id;
}
string query_save_file()
{
return sprintf(DATA_DIR "user/%s", get_id());
}
// called by the LOGIN_D when a net_dead player reconnects.
void reconnect()
{
set_heart_beat(1);
delete_temp("net_dead");
remove_call_out("user_dest");
tell_object(this_object(), "重新连线完毕。\n");
}
void user_dest()
{
if (environment())
{
tell_room(environment(), query("name") + "断线超过 1 分钟,自动退出这个世界。\n");
}
command("quit");
}
// 从游戏中移除这个玩家
void remove()
{
// 玩家数据存档
command("save");
destruct(this_object());
}
在 /include/globals.h 中定义 USER_OB 为当前对象文件后即可生效。
修改后的 /include/globals.h文件代码如下:
// The global include file is included automatically.
#ifndef GLOBALS_H
#define GLOBALS_H
// 游戏管理员帐号
#define WIZARD "test"
// 核心目录
#define INHERIT_DIR "/inherit/"
#define DAEMON_DIR "/system/daemons/"
// 系统目录
#define WORLD_DIR "/world/"
// 核心对象
#define LOGIN_OB "/obj/login"
#define USER_OB "/obj/user"
// 功能模块
#include "_inherit.h"
// 守护进程
#include "_daemon.h"
// 标准继承对象
#include "stdob.h"
// 指令路径
#define CMD_PATH_WIZ ({"/cmds/wiz/", "/mudcore/cmds/wizard/"})
#define CMD_PATH_STD ({"/cmds/std/", "/mudcore/cmds/player/"})
// 其他
#define MOTD "/system/etc/motd"
#define PRELOAD "/system/etc/preload"
#define START_ROOM WORLD_DIR "start_room"
// mudcore框架头文件(请放在最底部)
#include <mudcore.h>
#endif
重启游戏,看看登录是否正常,然后存档试试,存档数据中显示的对象文件已经改变为我们自己实现的对象。
最后,我们自定义玩家初始状态,从 CORE_LOGIN_D 可以看出,新玩家数据初始化时通过 CHAR_D 中的 void init_player() 方法实现的。
新建 /system/daemons/char_d.c,代码如下:
// 初始化玩家角色
void init_player(object user)
{
user->set("birthday", time());
user->set("title", "初心者");
user->set("vocation", "common");
user->set("exp", 0); // 经验
user->set("lv", 1); // 等级
user->set_attr("str", 6); // 力量
user->set_attr("agi", 6); // 速度
user->set_attr("vit", 6); // 耐力
user->set_attr("luk", 3); // 幸运
user->set_attr("charm", 3); // 幸运
user->set_attr("mend", 0); // 恢复魔力
user->set_attr("int", 0); // 攻击魔力
user->set_attr("max_hp", 14); // 最大HP
user->set_attr("max_mp", 0); // 最大MP
user->set_attr("sp", 0); // 技能点
user->set_attr("hp", 14); // 当前HP
user->set_attr("mp", 0); // 当前MP
}
void setup(object ob)
{
string race;
mapping my;
if (!stringp(race = ob->query("race")))
{
race = "人类";
ob->set("race", race);
}
my = ob->query_entire_dbase();
switch(race)
{
case "人类":
if (undefinedp(my["attr"])){
init_player(ob);
}
break;
case "魔物":
if (undefinedp(my["attr"])){
ob->set_attr("str", 15); // 力量
ob->set_attr("agi", 8); // 速度
ob->set_attr("vit", 7); // 耐力
ob->set_attr("max_hp", 20); // 最大HP
ob->set_attr("max_mp", 0); // 最大MP
ob->set_attr("hp", 20); // 当前HP
ob->set_attr("mp", 0); // 当前MP
}
if(my["attr"]["hp"] < my["attr"]["max_hp"])
my["attr"]["hp"] = my["attr"]["max_hp"];
if(my["attr"]["mp"] < my["attr"]["max_mp"])
my["attr"]["mp"] = my["attr"]["max_mp"];
break;
case "野兽":
// BEAST_RACE->setup(ob);
break;
default:
debug_message("CHAR_D: undefined race " + race + ".\n");
}
if(undefinedp(my["attr"]["hp"]) || my["attr"]["hp"] > my["attr"]["max_hp"])
my["attr"]["hp"] = my["attr"]["max_hp"];
if(undefinedp(my["attr"]["mp"]) || my["attr"]["mp"] > my["attr"]["max_mp"])
my["attr"]["mp"] = my["attr"]["max_mp"];
}
因为我们新增了守护进程,需要更新头文件,直接运行create_include daemon更新/include/_daemon.h文件。
因为有重新设置玩家数据,我们可以删除存档文件 /data/login/ 和 /data/user/ 重新注册账号。
最后,我们先补充一些指令:
- 玩家存档指令
save-/cmds/std/save.c
#include <ansi.h>
int main(object me, string arg)
{
// 最后存档时间
me->set("last_saved_at", time());
if (me->save())
write(GRN "你截止目前的事迹已被历史铭记 o(^▽^)o\n" NOR);
else
write(RED "【系统】档案存储失败 T_T\n" NOR);
return 1;
}
int help(object me)
{
write(@HELP
指令格式 : save
这个指令可以让你主动保存个人游戏资料。
HELP );
return 1;
}
- 游戏基本信息指令
mudinfo-/cmds/std/mudinfo.c
#include <ansi.h>
inherit _CLEAN_UP;
string duration()
{
int t, d, h, m, s;
string time;
t = uptime();
s = t % 60;
t /= 60;
m = t % 60;
t /= 60;
h = t % 24;
t /= 24;
d = t;
if (d)
time = d + "天";
else
time = "";
if (h)
time += h + "小时";
if (m)
time += m + "分";
time += s + "秒";
return time;
}
int main(object me, string arg)
{
string msg;
if (arg == "-v")
{
write("\nMUD 名称:" + MUD_NAME + "\n");
write("驱动环境:" + __ARCH__ + "\n");
write("驱动版本:" + __VERSION__ + "\n");
write("执行效率:" + query_load_average() + "\n");
write("内存占用:" + memory_info() / 1024 / 1024 + "M\n");
write("运行时间:" + duration() + "\n");
write("注册用户:" + sizeof(get_dir(DATA_DIR "user/*.o")) + "人\n");
write("在线玩家:" + sizeof(users()) + "人\n");
write("游戏生物:" + sizeof(livings()) + "位\n");
write("载入对象:" + sizeof(objects()) + "个\n");
}
else
{
msg = WHT BBLU "\n Mud名称 运行时间 游戏地址 端口 在线人数" NOR;
msg += "\n-------------------------------------------------------------\n";
msg += sprintf(" %-14s%-20s%-11s%-10d%-5d" NOR, MUD_NAME, duration(), "mud.ren", __PORT__, sizeof(users()));
msg += "\n-------------------------------------------------------------\n";
msg += " " + TIME_D->real_time_description() + "\n";
msg += " 游戏日历:" + TIME_D->game_time_description("黑铁") + "\n";
write(msg);
}
return 1;
}
int help(object me)
{
write(@HELP
指令格式 : mudinfo
这个指令可以让你查看游戏基本运行信息。
HELP );
return 1;
}
注意:这里我们使用了时间系统,请新建/system/time_d.c守护进程:
// TIME_D:时间系统守护进程
inherit CORE_TIME_D;
现在我们的守护进程会越来越多,一般守护进程都是服务性质的,最好预加载处理,为了方便,我们修改预加载文件/system/et/preload为指定守护进程目录:
/system/daemons/
这样游戏启动时会自动加载这个目录下的所有守护进程。
- 对象存档资料查看指令
data-/cmds/wiz/data.c
/*****************************************************************************
Copyright: 2020, Mud.Ren
File name: data.c
Description: 管理员专用,显示指定对象资料
Author: xuefeng
*****************************************************************************/
inherit _CLEAN_UP;
int help(object me);
int main(object me, string arg)
{
mapping list, tlist;
mixed *klist, *tlist2;
object ob, ob1;
string text, id, spec;
int loop;
if (!wizardp(me))
return 0;
if (arg)
{
sscanf(arg, "%s %s", spec, arg);
if (arg == "-t" || arg == "-d")
{
spec = arg;
arg = 0;
}
if (spec && spec != "-d" && spec != "-t")
{
arg = spec + " " + arg;
spec = 0;
}
}
if (!arg || arg == "")
ob = me;
else if (sscanf(arg, "%s in %s", arg, id) == 2)
{
ob1 = present(id, environment(me));
if (!ob1)
ob1 = present(id, me);
if (!ob1)
return notify_fail("本地无此生物: " + id + "\n");
if (!ob = present(arg, ob1))
return notify_fail("该生物身上无此对象: " + arg + "\n");
}
else if (arg == "here")
ob = environment(me);
else
{
arg = lower_case(arg);
ob = present(arg, environment(me));
if (!ob)
ob = find_player(arg);
if (!ob)
ob = find_living(arg);
if (!ob)
ob = present(arg, me);
if (!ob)
return notify_fail("Data: 无法找到此对象: " + arg + "。\n");
}
list = ob->query_entire_dbase();
tlist = ob->query_entire_temp_dbase();
if (!mapp(list) && !mapp(tlist))
return notify_fail("Data: 此对象并没有任何的资料。\n");
if (mapp(list) && sizeof(list))
{
klist = keys(list);
klist = sort_array(klist, "sort_keys", this_object());
}
if (mapp(tlist) && sizeof(tlist))
{
tlist2 = keys(tlist);
tlist2 = sort_array(tlist2, "sort_keys", this_object());
}
if (!klist || !sizeof(klist))
return notify_fail("Data: 此对象并没有储存任何资料。\n");
if (!spec || spec == "-d")
{
text = "Object: " + base_name(ob) + ".c\n";
for (loop = 0; loop < sizeof(klist); loop++)
{
if (strlen(klist[loop]) > 7)
text += klist[loop] + "\t: ";
else
text += klist[loop] + "\t\t: ";
text += sprintf("%O \n", list[klist[loop]]);
}
text += "\n总共有 " + sizeof(klist) + " 个储存的资料。\n\n";
}
else
text = "";
if (!spec || spec == "-t")
{
text += "TEMP DATA :\n\n";
for (loop = 0; loop < sizeof(tlist2); loop++)
{
if (strlen(tlist2[loop]) > 7)
text += tlist2[loop] + "\t: ";
else
text += tlist2[loop] + "\t\t: ";
text += sprintf("%O \n", tlist[tlist2[loop]]);
}
text += "\n总共有 " + sizeof(tlist2) + " 个暂存的资料。\n\n";
}
write(text);
return 1;
}
// This function is used to sort the key array.
int sort_keys(string one, string two)
{
return strcmp(one, two);
}
// This function strips out the secured props from the data array.
int filter_props(string prop, object target)
{
return 1;
}
int help(object me)
{
if (!wizardp(me))
return 0;
write(@LONG
指令格式: data <-t|-d> <玩家|对象|here> <in 玩家或生物>
指令范例: data <-- 看自己的所有 data base
data -t <-- 看自己的暂存资料
data -d <-- 看自己的永久资料
data -t here <-- 看所在房间的暂时资料
data ivy <-- 看 ivy 这个玩家(或对象)的所有资料
data cloth in ivy <-- 看 ivy 身上的 cloth 的资料
指令说明:
此命令让你可以观看所指定对象的资料,且将会以 more 的
方式输出,如果没有指定数,则自动以使用者为参数。其中的 -t 选
项可以显示暂时变数, -d 选项可以只显示永久变数, 所要看的对象
可以是玩家或任何对象。
LONG );
return 1;
}
我们实现了玩家指令存档(save),管理员专用指令 data,MUDCORE框架提供了quit指令,这里我们先不用自己实现,另外框架自带的variables指令和data类似,而且更为强大,能列出对象的所有全局变量。
- 查看玩家属性
score-/cmds/std/score.c
#include <ansi.h>
int top_list(string ob1, string ob2)
{
int score1, score2;
mapping my = this_player()->query_combat_record("mob");
score1 = my[ob1];
score2 = my[ob2];
return score2 - score1;
}
string score(object me)
{
string msg;
mapping my;
my = me->query_entire_dbase();
msg = HIC "\n≡" HIY "----------------------------------------------------" HIC "≡\n" NOR;
msg += sprintf(" |%-34s%-16s| \n", "【" + (my["title"] || "---") + "】" + me->short(),
"职业:" + chinese(my["vocation"]));
msg += sprintf(" |%-50s| \n", "");
msg += sprintf(" |%-34s%-16s| \n", " 生日:" + ctime(my["birthday"]), "性别:" + my["gender"]);
msg += " |--------------------------------------------------| \n" NOR;
msg += sprintf(" |%-50s| \n", "");
msg += sprintf(" |%-17s%-17s%-16s| \n", "HP:" + me->query_hp() + " / " + me->query_max_hp(), "MP:" + me->query_mp() + " / " + me->query_max_mp(), "SP:" + me->query_sp() + " / " + me->query_max_sp());
msg += sprintf(" |%-50s| \n", "");
msg += sprintf(" |%-15s%-15s%-20s| \n", "力量:" + me->query_str(), "速度:" + me->query_agi(), "耐力:" + me->query_vit());
msg += sprintf(" |%-15s%-15s%-20s| \n", "幸运:" + me->query_luk(), "魅力:" + me->query_charm(), "");
msg += sprintf(" |%-15s%-15s%-20s| \n", "回复魔力:" + me->query_mend(), "攻击魔力:" + me->query_magic(), "");
msg += sprintf(" |%-15s%-15s%-20s| \n", "攻击:" + me->query_attack(), "防御:" + me->query_defense(), "");
msg += sprintf(" |%-50s| \n", "");
msg += sprintf(" |%-15s%-15s%-20s| \n", "等级:" + my["lv"], "经验:" + my["exp"], "金币:" + my["coin"]);
msg += sprintf(" |%-50s| \n", "");
msg += HIC "≡" HIY "----------------------------------------------------" HIC "≡\n" NOR;
msg += YEL " 提示:使用 score -s 查看个人状态。\n" NOR;
return msg;
}
string score_hp(object me)
{
string msg, hpbar, mpbar, status = "";
mapping condition;
object *team;
// 组队状态
if (sizeof(team = me->query_team()) > 1)
{
msg = "*队伍成员状态*\n";
foreach(object member in team)
{
hpbar = graph_draw(member->query_hp(),member->query_max_hp(), 2, 1, 10);
mpbar = graph_draw(member->query_mp(),member->query_max_mp(), 4, 7, 10);
msg += sprintf(HIM "%-20s" HIG "HP:%-15s\t" HIB "MP:%s\n" NOR,member->short(), hpbar, mpbar);
}
}
else
{
if (mapp(condition = me->query("condition")))
{
foreach (string key, mapping value in condition)
{
status += replace_string(key, "#", "/")->query_condition_name() + " ";
}
}
hpbar = graph_draw(me->query_hp(),me->query_max_hp(), 2, 1, 10);
mpbar = graph_draw(me->query_mp(),me->query_max_mp(), 4, 7, 10);
msg = sprintf(HIG "HP:%s\t" HIB "MP:%s" NOR, hpbar, mpbar);
msg += "\n------------------------------------------------------\n";
msg += sprintf("状态:%s\n", status);
}
return msg;
}
int main(object me, string arg)
{
string msg;
switch (arg)
{
case "-s":
msg = score_hp(me);
break;
default:
msg = score(me);
}
write(msg);
return 1;
}
int help(object me)
{
write(@HELP
指令格式 : score [-s]
这个指令可以让你查看个人档案资料。
HELP );
return 1;
}
运行游戏看看指令效果:

另外,游戏在开发中会逐步补充完善以前实现的功能,现在我们先给 shutdown 指令增加玩家存档,新增/cmds/wiz/shutdown.c,代码修改如下:
int main(object me, string arg)
{
if (!wizardp(me)) return 0;
// 玩家存档处理
users()->remove();
shutdown(0);
return 1;
}
int help(object me)
{
if (!wizardp(me)) return 0;
write(@HELP
指令格式: shutdown
关闭游戏服务器。
HELP);
return 1;
}
现在基本的玩家相关功能开发完成。
本节代码
本节内容中我们通过在globals.h中自定义LOGIN_OB和USER_OB来实现我们自己的登录注册功能,主要功能代码在LOGIN_OB、USER_OB、_LIVING模块和CHAR_D守护进程中实现。另外也增加了一系列游戏相关的指令。
最后 commit 我们当前版本:
git add .
git commit -m 'v2.1'
附本节版本源码下载地址,如果下载使用,请在项目目录执行 git submodule update。