理论要点
什么是组件模式:允许单一的实体跨越多个领域,无需这些领域彼此耦合。
要点
- 组件模式:在单一实体跨越了多个领域时,为了保持领域之间相互解耦,可以将每部分代码放入各自的组件类中,将实体简化为组件的容器。
- 组件之间的交互常用手段一般是直接保存需要交互组件的引用或者就是通过观察者模式消息通信。
- Unity引擎在设计中频繁使用了这种设计方法,从而让其易于使用。
使用场合:
组件通常在定义游戏实体的核心部分中使用,当然,它们在其他地方也适用。这个模式在如下情况下可以很好的适用:
1,有一个涉及了多个领域的类,而你想保持这些领域互相隔离。
2,一个类正在变大而且越来越难以使用。
3,想要能定义一系列分享不同能力的类,但是使用接口不足以得到足够的重用部分。
代码分析
1,还记得经典丹麦面包师Bjorn游戏么?我们这里也将用这个来作为示例来分析组件模式的运用:我们会有一个类来表示友好的糕点厨师,同时这个类还包含他在比赛中做的一切行为。由于玩家控制他,这意味着需要读取控制器的输入然后转化为动作。 而且,他需要与关卡相互作用,所以要引入物理和碰撞。 一旦这样做了,他必须在屏幕上出现,所以要引入动画和渲染。 他可能还会播放一些声音。
下面我们就先来简单实现下这个传统的Bjorn面包师类:
class Bjorn
{
public:
Bjorn()
: velocity_(0),
x_(0), y_(0)
{}
void update(World& world, Graphics& graphics);
private:
static const int WALK_ACCELERATION = 1;
int velocity_;
int x_, y_;
Volume volume_;
Sprite spriteStand_;
Sprite spriteWalkLeft_;
Sprite spriteWalkRight_;
};
面包师Bjorn每帧会调用update()方法:
void Bjorn::update(World& world, Graphics& graphics)
{
// 根据用户输入修改英雄的速度
switch (Controller::getJoystickDirection())
{
case DIR_LEFT:
velocity_ -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
velocity_ += WALK_ACCELERATION;
break;
}
// 根据速度修改位置
x_ += velocity_;
world.resolveCollision(volume_, x_, y_, velocity_);
// 绘制合适的图形
Sprite* sprite = &spriteStand_;
if (velocity_ < 0)
{
sprite = &spriteWalkLeft_;
}
else if (velocity_ > 0)
{
sprite = &spriteWalkRight_;
}
graphics.draw(*sprite, x_, y_);
}
它读取操纵杆以确定如何加速面包师。 然后,用物理引擎解析新位置。 最后,将Bjorn渲染至屏幕。
这个示例平凡而简单。 没有重力,动画,或任何让人物有趣的其他细节。 即便如此,我们可以看到,已经出现了同时消耗多个程序员时间的函数,而它开始变得有点混乱。 想象增加到一千行,你就知道这会有多难受了。
2,像上面这样,随着代码的增长,你发现这个类越来越难维护,而且与多个模块耦合,这对于第一手程序员还好,而对于第二手,三手猿们那真会问候你祖先一遍~~这个时候你就应该想想怎么给这个类瘦身,刚好我们这节的组合模式就能很好解决这些问题。
首先,我们先把玩家输入操作分离出来:
class InputComponent
{
public:
void update(Bjorn& bjorn)
{
switch (Controller::getJoystickDirection())
{
case DIR_LEFT:
bjorn.velocity -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
bjorn.velocity += WALK_ACCELERATION;
break;
}
}
private:
static const int WALK_ACCELERATION = 1;
};
下面,我们再来将物理和渲染代码分别分离出来:
class PhysicsComponent
{
public:
void update(Bjorn& bjorn, World& world)
{
bjorn.x += bjorn.velocity;
world.resolveCollision(volume_,
bjorn.x, bjorn.y, bjorn.velocity);
}
private:
Volume volume_;
};
class GraphicsComponent
{
public:
void update(Bjorn& bjorn, Graphics& graphics)
{
Sprite* sprite = &spriteStand_;
if (bjorn.velocity < 0)
{
sprite = &spriteWalkLeft_;
}
else if (bjorn.velocity > 0)
{
sprite = &spriteWalkRight_;
}
graphics.draw(*sprite, bjorn.x, bjorn.y);
}
private:
Sprite spriteStand_;
Sprite spriteWalkLeft_;
Sprite spriteWalkRight_;
};
现在,再看来来,我们最开始很长的Bjorn类已经全移出来了,下面就开始用我们封装的组件来重新组合它:
class Bjorn
{
public:
int velocity;
int x, y;
void update(World& world, Graphics& graphics)
{
input_.update(*this);
physics_.update(*this, world);
graphics_.update(*this, graphics);
}
private:
InputComponent input_;
PhysicsComponent physics_;
GraphicsComponent graphics_;
};
这其实就是我们组件模式的核心思想了,不像封装单个对象那样庞大难维护,也不像继承那样深层次不好管理。这也是u3d引擎运用的核心模式之一。
3,核心思想已经讲完了,接下来就是一些优化或常见问题处理方式了。
上面我们的Bjorn类虽然已经由组件组成实现,但是它还是没将行为抽象出来。Bjron仍知道每个组件的具体行为,我们来改变下,让它组件行为再抽象:我们就拿输入组件下手,将InputComponent变为抽象基类:
class InputComponent
{
public:
virtual ~InputComponent() {}
virtual void update(Bjorn& bjorn) = 0;
};
现在玩家输入就这样实现:
class PlayerInputComponent : public InputComponent
{
public:
virtual void update(Bjorn& bjorn)
{
switch (Controller::getJoystickDirection())
{
case DIR_LEFT:
bjorn.velocity -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
bjorn.velocity += WALK_ACCELERATION;
break;
}
}
private:
static const int WALK_ACCELERATION = 1;
};
这样改装后,我们的Bjorn就可以只保存一个指向输入组件的指针,而不是具体的一个实例对象。
class Bjorn
{
public:
int velocity;
int x, y;
Bjorn(InputComponent* input)
: input_(input)
{}
void update(World& world, Graphics& graphics)
{
input_->update(*this);
physics_.update(*this, world);
graphics_.update(*this, graphics);
}
private:
InputComponent* input_;
PhysicsComponent physics_;
GraphicsComponent graphics_;
};
现在我们实例化Bjorn,组件就是通过参数传入控制了,而不是先前那样固定了:
Bjorn* bjorn = new Bjorn(new PlayerInputComponent());
这样有什么好处呢?想想,我们现在的输入组件是处理玩家输入,如果我们需要加一个“演示模式”,它不需要玩家主动输入,也能让游戏自动运行。那么这个就只需添加另一种输入方式的派生类对象:
class DemoInputComponent : public InputComponent
{
public:
virtual void update(Bjorn& bjorn)
{
// 自动控制Bjorn的AI……
}
};
如果要进入“演示模式”,这样初始化组件就行了:
Bjorn* bjorn = new Bjorn(new DemoInputComponent());
是不是很方便呢!
同样的方式,我们把物理和渲染代码也这么处理了:
class PhysicsComponent
{
public:
virtual ~PhysicsComponent() {}
virtual void update(GameObject& obj, World& world) = 0;
};
class GraphicsComponent
{
public:
virtual ~GraphicsComponent() {}
virtual void update(GameObject& obj, Graphics& graphics) = 0;
};
class BjornPhysicsComponent : public PhysicsComponent
{
public:
virtual void update(GameObject& obj, World& world)
{
// 物理代码……
}
};
class BjornGraphicsComponent : public GraphicsComponent
{
public:
virtual void update(GameObject& obj, Graphics& graphics)
{
// 图形代码……
}
};
既然现在我们组件都进一步抽象了,可以通过参数控制,那么我们的Bjorn类其实可以改成一个统一的基础对象GameObject类:
class GameObject
{
public:
int velocity;
int x, y;
GameObject(InputComponent* input,
PhysicsComponent* physics,
GraphicsComponent* graphics)
: input_(input),
physics_(physics),
graphics_(graphics)
{}
void update(World& world, Graphics& graphics)
{
input_->update(*this);
physics_->update(*this, world);
graphics_->update(*this, graphics);
}
private:
InputComponent* input_;
PhysicsComponent* physics_;
GraphicsComponent* graphics_;
};
构建Bjorn面包师只是其中的一种对象而已,通过用不同组件实例化GameObject,我们可以构建游戏需要的任何对象。下面看看Bjorn可以通过一个接口创建:
GameObject* createBjorn()
{
return new GameObject(new PlayerInputComponent(),
new BjornPhysicsComponent(),
new BjornGraphicsComponent());
}
好,组件模式就讲这么多了。就如其名,核心就是组合的思想来替换我们诟病的继承。
结束!