使用MUDCORE框架从零开发LPMUD游戏:自定义玩家注册

《使用MUDCORE框架从零开发LPMUD游戏》系列内容,通过教程既能熟悉 MUDCORE 框架的使用,也能学习LPCMUD游戏的开发,版权所有 mud.ren。

上一节我们自定义了登录实现,玩家出生在我们自己的游戏环境中,但是,目前玩家连线对象和玩家对象都还是框架示例的对象,分别是 /mudcore/system/object/login.c/mudcore/system/object/user.c 。我们每个游戏都有一些独有的功能,比如玩家属性不同的游戏是不一样的,为了满足特定的游戏开发,我们可以自定义这二个对象直接自己实现login.cuser.c

自定义注册很简单,只需要我们在globals.h中指定自己的LOGIN_OBUSER_OB对象,就可以实现游戏连线和玩家对象调用我们自己开发的,不再使用mudcore默认的对象。

下面我们开发实现这个功能,另外还让注册的玩家有以下属性:

file

我们知道玩家对象也是生物对象,为了代码复用,我们把生物对象共同的特性单独放置在 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;
}

运行游戏看看指令效果:

file file

另外,游戏在开发中会逐步补充完善以前实现的功能,现在我们先给 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_OBUSER_OB来实现我们自己的登录注册功能,主要功能代码在LOGIN_OBUSER_OB_LIVING模块和CHAR_D守护进程中实现。另外也增加了一系列游戏相关的指令。

最后 commit 我们当前版本:

git add .
git commit -m 'v2.1'

附本节版本源码下载地址,如果下载使用,请在项目目录执行 git submodule update

京ICP备13031296号-4