用好面向对象编程,让你的MUD开发少写90%的重复代码

LPC语言做为1989年诞生的编程语言,不算年轻,但也算不上古董,目前流程的开发语言除了万年长青的C语言外,其他的基本也都是80年代和90年代诞生的。LPC语言冷门主要在于应用方向上,而不是于语法功能上,只是因为内置的apply和efun更针对游戏开发,让游戏开发更方便,单独拿语法功能上说,相对目前其它主流编程语言,LPC的很多特性也是非常先进的,完全不弱于其它面向对象编程语言。

LPC语言有着丰富且易用的数据类型,简单易学,而且具有完整的面向对象编程特征,只是目前MUD游戏开发中,多数人并没有充分发挥LPC语言面向对象的特性,让MUD开发变的笨重而麻烦。

分析现有的MUD代码看看,以炎黄为例(炎黄现在是本人维护,但原始代码并不是我写的,仍然经过多个WIZ开发)每个具体对象都是一个文件,游戏中需要一把剑时,WIZ就写了一个sword.c文件实现代码,需要长剑,WIZ就写一个longsword.c的文件实现代码,需要短剑再写一个shortsword.c的代码,而且大量重复的代码。看看一下文件:

// changjian.c
#include <weapon.h>
#include <ansi.h>
inherit SWORD;
void create()
{
        set_name( "长剑",  ({ "chang jian", "sword", "jian" }));
        set_weight(10000);
        if (clonep())
                set_default_object(__FILE__);
        else {
                set("unit", "柄");
                set("long", "一柄普通的长剑。\n");
                set("value", 1000);
        }
        init_sword(10);
        setup();
}
// gangjian.c
#include <weapon.h>
#include <ansi.h>
inherit SWORD;
void create()
{
        set_name( HIW "钢剑" NOR,  ({ "gang jian", "sword", "jian" }));
        set_weight(10000);
        if (clonep())
                set_default_object(__FILE__);
        else {
                set("unit", "柄");
                set("long", "一柄锋利的长剑。\n");
                set("value", 3000);
        }
        init_sword(20);
        setup();
}
// sword1.c
#include <weapon.h>
#include <ansi.h>
inherit SWORD;
void create()
{
        set_name( CYN "镔铁长剑" NOR,  ({ "chang jian", "sword", "jian" }));
        set_weight(10000);
        if (clonep())
                set_default_object(__FILE__);
        else {
                set("unit", "柄");
                set("long", "一柄锋利的长剑。\n");
                set("value", 1200);
        }
        init_sword(15);
        setup();
}
// sword2.c
#include <weapon.h>
#include <ansi.h>
inherit SWORD;
void create()
{
        set_name( "官府用剑",  ({ "chang jian", "sword", "jian" }));
        set_weight(10000);
        if (clonep())
                set_default_object(__FILE__);
        else {
                set("unit", "柄");
                set("long", "一柄官府卫兵特用的长剑。\n");
                set("value", 2000);
        }
        init_sword(15);
        setup();
}
// sword3.c
#include <weapon.h>
#include <ansi.h>
inherit SWORD;
void create()
{
        set_name( "东厂铸剑",  ({ "dongchang jian", "dongchang", "jian" }));
        set_weight(10000);
        if (clonep())
                set_default_object(__FILE__);
        else {
                set("unit", "柄");
                set("long", "一柄官府卫兵特用的长剑,剑脊上铸有“东厂”二字。\n");
                set("value", 2000);
        }
        init_sword(20);
        setup();
}

从面向对象编程的思路看,这合理吗?这完全是基于对象编程而不是面向对象编程,在JAVA或C++等项目开发中这样写,你分分钟得下岗滚蛋。

我们知道面向对象编程的三大基本特性:封装,继承,多态,在本站MUD教程中针对LPC语言也有讲解:LPC 语言基础教程:5.7 LPC语言中的面向对象编程,这里不再重复。

在游戏中所有物品都是对象,长剑、短剑、铁剑、钢剑都是剑类,而不管剑类还是刀类,都属于兵器类,在标准面向对象编程中,应该是这样的代码(以php代码为例):


class Weapon
{
    // 武器通用功能接口
    protected $short; // 名称
    protected $ids; // id列表
    protected $long; // 描述
    protected $unit; // 单位
    protected $value; // 价值
    protected $damage; // 伤害
    protected $type; // 类型
    protected $weight; // 重量

    // 武器公共方法
    public function set_name(string $short, array $ids)
    {
        $this->short = $short;
        $this->ids = $ids;
    }

    public function set_weight(int $weight)
    {
        $this->weight = $weight;
    }

    public function set_long(string $long)
    {
        $this->long = $long;
    }

    public function set_value(int $value)
    {
        $this->value = $value;
    }

    public function short()
    {
        return $this->short;
    }

    public function long()
    {
        return $this->long;
    }

    // 武器其他方法
}

class Sword extends Weapon
{
    function __construct(
        int $damage = 10,
        string $short = '长剑',
        string $long = '一柄普通的长剑。',
        array $id = ["chang jian", "sword", "jian"],
        int $value = 1000,
        int $weight = 10000
    ) {
        $this->set_name($short, $id);
        $this->set_long($long);
        $this->set_weight($weight);
        $this->set_value($value);
        $this->init_sword($damage);
    }

    function init_sword(int $damage)
    {
        $this->damage = $damage;
        $this->unit = '柄';
        $this->type = 'sword';

        // 剑类其他公共属性
    }

    // 剑类其他方法
}

当我们需要一柄长剑时:

$changjian = new Sword();

当我们需要一柄钢剑时:

$gangjian = new Sword(20, '钢剑', '一柄锋利的长剑。', ["gang jian", "sword", "jian"], 3000);

当我们需要一柄镔铁长剑时:

$sword1 = new Sword(15, '镔铁长剑', '一柄锋利的长剑。');
$sword1->set_value(1500);

当我们需要一柄官府用剑时:

$sword2 = new Sword(15, '官府用剑', '一柄锋利的长剑。');
$sword2->set_value(2000);

当我们需要一柄东厂铸剑时:

$sword3 = new Sword(20, '东厂铸剑', '一柄官府卫兵特用的长剑,剑脊上铸有“东厂”二字。。', ["dongchang jian", "dongchang", "jian"], 2000);

这些事很容易理解的事,我们不会把为每把剑各写一段代码,而是通过类封装和继承,对象做为类的实例。

但是,为什么在MUD游戏开发中就这么一个对象一个文件的具体写了呢?在MUD开发中面向对象编程怎么就写的这么LOW了?这不是搞笑吗?可以说,这些完全是一种陋习,如果你要找理由和我说这样代码加载速度更快,我只能回复呵呵。

对比以上5把剑的文件,的确继承了一个通用的SWORD类,这个相当于标准的剑类接口,提供了init_sword这个方法。ES2的代码架构其实是非常科学合理的,继承上也是标准的面向对象的写法,从代码上看,每把剑都继承了剑的父类(inherit SWORD;),而这个SWORD又继承了装备类(inherit EQUIP;),装备类EQUIP又继承了道具类和装备接口(inherit ITEM;inherit F_EQUIP;),而道具类ITEM则继承了对象回收、数据管理、移动和名称等接口。

代码构架设计的合理,但是最后对类和对象的处理,并没有把长剑文件当成类,而是做具体的对象写了五个,把每个文件当成了一个具体的对象来使用,这就变的不太对了。

虽然MUD中class关键字是结构体,但是并不是代表不能和其他语言一样使用标准的类,我们在MUD中总是把每一个.c文件都当一个游戏对象并不正确,更精确的说,LPMUD中每一个.c文件都是一个蓝图对象,这个本质上就相当于其他语言中的类,其它高级语言中推荐一个类写一个文件,而LPC中直接强制了一个类一个文件。开发者需要转变自己的观念,记死一条:在LPMUD中每一个.c文件都是一个类,create()函数是类的构造函数,不过LPC中的类不存在析构函数,因为LPC语言中内存永远不会显示回收,需要通过clean_up来调用destruct回收对象,存在一个类似的方法move_or_destruct()用来处理被回收对象内部的对象。

比较特别的是LPC中的类没有细分静态类、非静态类、接口等,LPC中的类不只是可以被继承,还既可以像其他面向对象语言的静态类直接访问(load_objectclass::methed()),又可以像其他语言中的非静态类被实例化(clone_objectnew)。

现在我们完全可以用标准的面向对象的思路写代码,按照类的标准改造如下,只保留一个长剑文件sword.c,代表通用的长剑类,通过构造函数create传参初始化。

// /std/weapon/sword.c
#include <weapon.h>
#include <ansi.h>
inherit SWORD;

varargs void create(int damage, string short, string long, string *ids, int value, int weight)
{
    set_name( short || "长剑",  ids || ({ "chang jian", "sword", "jian" }));
    set_weight(weight || 10000);
    if (clonep())
        set_default_object(__FILE__);
    else {
        set("unit", "柄");
        set("long", long || "一柄普通的长剑。\n");
        set("value", value || 1000);
    }
    init_sword(damage || 10);
    setup();
}

当游戏需要需要普通长剑时:

object changjian = new("/std/weapon/sword");

当游戏需要一把钢剑时:

object gangjian = new("/std/weapon/sword", 20, "钢剑", "一柄锋利的长剑。\n", ({ "gang jian", "sword", "jian" }), 3000);

同理,需要其他类型的剑,直接传不同参数即可。这样不管游戏需要多少不同名称、描述、伤害的剑,都不用增加文件写代码。

只不过这样做还是不够规范,正常我们游戏同一类型的武器应该完全一样的参数值,好的做法是数据标准化,使用数据库或mapping存储,游戏调用时只用传唯一的ID,具体实例化参数则通过ID查询获取。

改造标准长剑代码如下:

// /std/weapon/sword.c
#include <weapon.h>
#include <ansi.h>
inherit SWORD;

varargs void create(int id)
{
    string short = WEAPON_DB->short("sword", id);
    string *ids = WEAPON_DB->ids("sword", id);
    string long = WEAPON_DB->long("sword", id);
    int weight = WEAPON_DB->weight("sword", id);
    int value = WEAPON_DB->value("sword", id);
    int damage = WEAPON_DB->damage("sword", id);

    set_name(short || "长剑", ids || ({"chang jian", "sword", "jian"}));
    set_weight(weight || 10000);
    if (clonep())
        set_default_object(__FILE__);
    else {
        set("unit", "柄");
        set("long", long || "一柄普通的长剑。\n");
        set("value", value || 1000);
    }
    init_sword(damage || 10);
    setup();
}

剑类的具体数据统一由WEAPON_DB文件管理,参考代码:

// 兵器数据库

// short ids long damage value weight
private nosave mixed *sword = ({
    ({"长剑", ({ "chang jian", "sword", "jian" }) ,"一柄普通的长剑。\n", 10, 1000, 10000}),
    ({"钢剑", ({ "gang jian", "sword", "jian" }) ,"一柄锋利的长剑。\n", 20, 3000, 10000}),
    ({"镔铁长剑", ({ "chang jian", "sword", "jian" }) ,"一柄普通的长剑。\n", 15, 1200, 10000}),
    ({"官府用剑", ({ "chang jian", "sword", "jian" }) ,"一柄官府卫兵特用的长剑。\n", 15, 2000, 10000}),
    ({"东厂铸剑", ({ "dongchang jian", "sword", "jian" }) ,"一柄官府卫兵特用的长剑,剑脊上铸有“东厂”二字。\n", 20, 2000, 10000}),
    // 更多剑类兵器
});

private nosave mixed *blade = ({
    // 刀类兵器
});

string short(string type, int id)
{
    if (type == "sword")
    {
        return sword[id][0];
    }
}

string *ids(string type, int id)
{
    if (type == "sword")
    {
        return sword[id][1];
    }
}

string long(string type, int id)
{
    if (type == "sword")
    {
        return sword[id][2];
    }
}

int damage(string type, int id)
{
    if (type == "sword")
    {
        return sword[id][3];
    }
}

int value(string type, int id)
{
    if (type == "sword")
    {
        return sword[id][4];
    }
}

int weight(string type, int id)
{
    if (type == "sword")
    {
        return sword[id][5];
    }
}

兵器数据标准化:

// 长剑
object sword = new("/std/weapon/sword");
// 钢剑
object sword1 = new("/std/weapon/sword", 1);
// 镔铁长剑
object sword2 = new("/std/weapon/sword", 2);
// 官府用剑
object sword3 = new("/std/weapon/sword", 3);
// ...

如果游戏数据有调整,也只用动这一个文件,游戏所有相同的剑数据都自动修改。

基本上,游戏中所有类别的物品都可以通过类似的方式实现代码的精简和数据的统一管理。比如NPC、食物、动物和房间。毫不夸张的说,这样可以让游戏代码精简70%。

现在代码是更科学了,但如果直接这样用,代码要改的太多了,毕竟我们游戏中整体结构上每个对象对应唯一的路径的,物品加载也是使用set("objects", (["/std/weapon/sword": 1,]));这种方式,如果直接改成传参的,不够简洁优雅,怎么办?

对WEB开发经验比较丰富的程序员,应该马上能想到解决方案:参数放在路径中,使用路由处理,比如:钢剑"/std/weapon/sword/1"、官府用剑""/std/weapon/sword/3",代码中依然只有"/std/weapon/sword.c"这一个类文件,当游戏中调用"/std/weapon/sword/1"时把1做为参数传给类来实例化。

MASTER_OB中的apply方法compile_object就是LPC中的路由,当游戏加载"/std/weapon/sword/1"时,文件不存在,驱动自动转compile_object处理,我们在compile_object中调用"/std/weapon/sword.c"来实例化。

如下示例:

// MASTER_OB中的方法
mixed compile_object(string str)
{
    return call_other(VIRTUAL_D, "compile_object", str);
}

路由compile_object把收到的路径做参数调用VIRTUAL_D处理,这里VIRTUAL_D就是一个控制器,科学!完美!

// VIRTUAL_D中的方法,可以进一步优化处理不同的虚拟对象
mixed compile_object(string file)
{
    string *path, virtual;
    object ob;
    int n;

    path = explode(file, "/");
    n = sizeof(path) - 1;
    virtual = replace_string(file, "/" + path[n], "");

    if (file_size(virtual + ".c") < 1)
        return "对象不存在!";

    if (!(ob = new(virtual, to_int(path[n]))))
        return "编译失败!";

    return ob;
}

更巧妙的是可以虚实结合,要知道我们访问/std/weapon/sword/1对应的是/std/weapon/sword/1.c这个文件,因为文件不存在转虚拟对象处理,但如果我们直接建一个具体的文件,就不会走虚拟对象了,这样对某些有特别功能的对象,就可以直接具体的实现。

游戏中的环境也可以这样实现,面向对象编程+虚拟对象路由,对某些地图很大、物品很多的游戏来说,足以让你的游戏开发少写90%的重复代码。关于虚拟对象本站有教程:MUD游戏开发进阶:强大的虚拟对象,这里不再重复讲解。

京ICP备13031296号-4