类型对象模式:让角色职业与技能优雅共舞

说实话,类型对象模式(Type Object)这个名字听起来有点学术,但它的思想其实特别朴素。我最早接触它是在做一款MMORPG的职业技能系统时——那时候我还在想,怎么让几十种职业、上百种技能不把代码撑爆。

说白了,类型对象模式就是:把「类型」本身也当作一个对象来管理。你想想看,我们平时写代码,一个「战士」职业,一个「法师」职业,如果每个职业都写一个类,那职业一多,类就爆炸了。类型对象模式的做法是——把职业定义成数据,而不是代码。

核心思想:类型定义(Type Definition)与实例对象(Instance Object)分离,类型本身作为可共享的对象存在。

为什么需要类型对象模式?

我在项目中遇到过这样一个场景:游戏里有「战士」「法师」「弓箭手」三个职业,每个职业有5个技能。一开始,我老老实实写了三个类,每个类里硬编码技能列表。结果策划说,下周要加10个新职业,每个职业20个技能……

嗯,那时候我就知道,这条路走不通了。

类型对象模式正好解决这个问题:

  • 类型定义:职业是什么、有哪些技能、属性加成多少——这些统统作为数据存储
  • 实例对象:每个具体的角色(比如玩家A的战士)引用一个职业类型
  • 类型共享:所有同职业的角色共享同一个职业类型对象,节省内存

模式结构图

先看一张图,帮你快速建立整体认知:

Type(类型定义) 职业名称:战士 基础属性:力量+10 技能列表:[猛击, 盾墙] Instance(实例) 角色名:张三 等级:15 类型引用 → 战士 共享池 战士类型(1份) 法师类型(1份) 弓箭手类型(1份) 多个实例共享同一个类型对象 100个战士角色 → 只存1份战士类型定义 实例列表 张三(战士) | 李四(战士) | 王五(法师) | 赵六(弓箭手) | ... 所有战士实例 → 指向同一个战士类型对象

Java实现:职业与技能系统

来看一个具体的例子。我习惯先定义类型接口:

// 技能类型
public class SkillType {
    private String name;
    private int damage;
    private int manaCost;
    private double cooldown;
    
    public SkillType(String name, int damage, int manaCost, double cooldown) {
        this.name = name;
        this.damage = damage;
        this.manaCost = manaCost;
        this.cooldown = cooldown;
    }
    
    // getters...
}

// 职业类型
public class ClassType {
    private String className;
    private int baseStrength;
    private int baseIntelligence;
    private List<SkillType> skills;
    
    public ClassType(String className, int baseStr, int baseInt, List<SkillType> skills) {
        this.className = className;
        this.baseStrength = baseStr;
        this.baseIntelligence = baseInt;
        this.skills = skills;
    }
    
    public SkillType getSkill(int index) {
        return skills.get(index);
    }
}

然后定义角色实例:

public class Character {
    private String name;
    private int level;
    private ClassType classType;  // 引用类型对象
    
    public Character(String name, ClassType classType) {
        this.name = name;
        this.classType = classType;
        this.level = 1;
    }
    
    public int getStrength() {
        return classType.getBaseStrength() + (level - 1) * 2;
    }
    
    public void useSkill(int index) {
        SkillType skill = classType.getSkill(index);
        System.out.println(name + " 释放了 " + skill.getName());
    }
}
我的习惯:类型对象一旦创建就不允许修改(不可变对象)。这样多个实例共享时不会出现互相干扰的问题。我曾经因为类型对象可变,导致一个玩家改了技能冷却时间,结果全服战士都跟着变了——那场面,挺尴尬的。

C++实现:更精细的内存控制

C++里我们可以用智能指针来管理类型对象的生命周期:

#include <memory>
#include <vector>
#include <string>

class SkillType {
public:
    SkillType(std::string name, int damage, int mana, double cd)
        : name_(name), damage_(damage), manaCost_(mana), cooldown_(cd) {}
    
    std::string getName() const { return name_; }
    int getDamage() const { return damage_; }
    
private:
    std::string name_;
    int damage_;
    int manaCost_;
    double cooldown_;
};

class ClassType {
public:
    ClassType(std::string name, int str, int intel,
              std::vector<std::shared_ptr<SkillType>> skills)
        : name_(name), baseStr_(str), baseInt_(intel), skills_(skills) {}
    
    std::shared_ptr<SkillType> getSkill(int index) const {
        return skills_[index];
    }
    
private:
    std::string name_;
    int baseStr_;
    int baseInt_;
    std::vector<std::shared_ptr<SkillType>> skills_;
};

class Character {
public:
    Character(std::string name, std::shared_ptr<ClassType> classType)
        : name_(name), classType_(classType), level_(1) {}
    
    void useSkill(int index) {
        auto skill = classType_->getSkill(index);
        std::cout << name_ << " 释放了 " << skill->getName() << std::endl;
    }
    
private:
    std::string name_;
    int level_;
    std::shared_ptr<ClassType> classType_;
};

类型对象 vs 传统继承

你可能会问:为什么不直接用继承?比如 Warrior extends Character?

我举个例子你就明白了。假设有10个职业,每个职业有20个技能。用继承,你要写10个类,每个类里重复写技能逻辑。用类型对象模式,你只需要1个Character类 + 10个类型定义数据。

对比维度 传统继承 类型对象模式
类数量 职业数 × 技能组合数 1个通用类 + N个类型数据
新增职业 新建一个类,可能还要改父类 添加一条类型数据即可
运行时切换 不支持(除非用状态模式) 直接换类型引用
内存占用 每个实例都带一份方法表 类型数据共享,实例很轻量
注意:类型对象模式不是万能的。如果职业之间的行为差异极大(比如战士和法师的移动方式完全不同),那还是用继承或策略模式更合适。类型对象模式最适合「同一种行为,不同参数」的场景。

实战中的避坑指南

我曾经在做一个装备系统时,把装备属性也做成了类型对象。结果发现一个问题:装备有耐久度,每个实例的耐久度都不一样。如果所有实例共享同一个类型对象,那耐久度就乱套了。

解决方案其实很简单:类型对象只存「不变数据」。像基础属性、技能列表这些所有实例都一样的东西,放类型对象里。像耐久度、当前经验值这些实例特有的,放实例对象里。

另一个坑是类型对象的缓存。我建议用一个工厂类来管理:

public class ClassTypeFactory {
    private static Map<String, ClassType> cache = new HashMap<>();
    
    public static ClassType getClassType(String className) {
        return cache.computeIfAbsent(className, name -> {
            // 从配置文件或数据库加载
            return loadClassTypeFromConfig(name);
        });
    }
}

这样做的好处是:同一个职业只创建一次类型对象,后续所有角色都共享它。内存省了,加载也快了。

总结一下

类型对象模式,说白了就是把「类型」从代码里抽出来,变成数据。这样做的好处很明显:

  • 灵活:新增职业不用改代码,改配置文件就行
  • 省内存:100个战士共享1份类型定义
  • 易维护:类型数据集中管理,改一处全生效

我个人觉得,这个模式特别适合做游戏的角色系统、职业技能系统,甚至是电商的商品分类系统。只要你有「一类对象共享同一套定义」的需求,都可以考虑用类型对象模式。

嗯,今天就聊到这儿。代码示例都在上面了,你可以直接拿去用。遇到问题欢迎交流。


公众号:蓝海资料掘金营,微信deep3321