角色与武器的实现
需求:
三种武器类型:手枪、散弹枪、火箭,以“攻击力”和“攻击距离”区分它们的威力。此外,“武器发射”和“击中目标”时会有不同的音效和视觉效果。双方阵营都可以装备这3种武器,但敌方角色使用武器攻击时,会有额外的加成效果来增加攻击时的优势,而玩家角色则没有额外的攻击力。
错误示范
将所有可能组合的代码程序都写出来,把武器声明成一个类,并声明一个枚举类型来定义3种武器;
//武器类
public enum ENUM_Weapon
{
Null = 0,
Gun = 1,
Rifle = 2,
Rocket = 3,
Max
}
//武器接口
public class Weapon
{
protected ENUM_Weapon m_emWeaponType = ENUM_Weapon.Null;
// 數值
protected int m_AtkPlusValue = 0; // 額外增加的攻擊力
protected int m_AtkValue = 0; // 攻擊力
protected int m_AtkRange= 0.0f; // 攻擊距離
public Weapon(ENUM_Weapon Type,int Atkvalue,int AtkRange)
{
m_emWeapon = Type;
m_AtkValue = AtkValue;
m_AtkRange = AtkRange;
}
public ENUM_Weapon GetWeaponType()
{
return m_emWeaponType;
}
//攻击目标
public void Fire(ICharacter theTarget)
{
...
}
//设置额外攻擊力
public void SetAtkPlusValue(int AtkPlusValue)
{
m_AtkPlusValue = AtkPlusValue;
}
...
}
在角色类中增加一个记录当前使用武器的类成员,然后在攻击方法中,根据当前所持武器不同选择不同的特效和声音,以及攻击加成。
// 角色接口
public abstract class Icharacter
{
//拥有一把武器
protected Weapon m_Weapon = null;
//攻击目标
public abstract void Attack(Icharacter theTarget);
}
//Enemy 角色接口
public class IEnemy : ICharacter
{
public override void Attack(Icharacter theTarget)
{
//发射特效
w_Weapon.ShowShootEffect();
int AtkPlusValue = 0;
//按当前武器决定攻击方式
switch(m_Weapon.GetWeaponType())
{
case ENUM_Weapon.Gun:
//显示武器特效和音效
m_Weapon.ShowBulletEffect(theTarget.GetPosition(),0.03f,0.2f);
m_Weapon.ShowSoundEffect("GunShot");
//有概率增加额外加成
AtkPlusValue = GetAtkPluseValue(5,20);
break;
case ENUM_Weapon.Rifle:
...
break;
case ...
}
}
}
// Soldier 角色接口
public class ISoldier : ICharacter
{
//同上
...
}
存在的问题
- 每个继承自Icharacter角色接口的类,在重新定义Attack方式时,都必须针对每一种武器来实现(显示特效和播放音效),或者进行额外的公式计算。所以当要新增角色类时,也要在新的子类中重复编写相同的程序代码。
- 当要新增武器类型时,所有角色子类中的Attack方法,都必须修改,针对新的武器类型编写新的对应程序代码。这样会增加维护的难度,是的武器类型不容易增加。
桥接模式
将抽象与实现分离,使二者可以独立地变化。
桥接模式的定义
客户端只需要知道“接口类”的存在,不必知道是由哪一个实现类来完成功能的。而实现类则可以有好多个,至于使用哪一个实现类,可能会按照当前系统设置的情况来决定。
参与者的说明如下:
-
Abstraction(抽象体接口)
- 拥有指向Implementor的对象引用
- 定义抽象功能的接口,也可做为子类调用实现功能的接口
-
RefinedAbstraction(抽象体实现、扩充)
- 继承抽象体并调用Implementor完成实现功能。
- 扩充抽象体的接口,增加额外的功能
-
Implementor(实现体接口)
-
定义实现功能的接口,提供给Abstraction(抽象体)使用。
-
接口功能可以只有单一的功能,真正的选择则再有Abstraction(抽象体)的需求加以组合应用。
-
-
ConcreteImplementorA/B(实现体)
- 实际完成实现体接口上所定义的方法。
绘制图形
现在我们需要绘制图形,图形形状未定,可能有:球形,立方体,圆柱……;绘制平台也未定,可能有:DX、OpenGL、OpenGL ES……。该如何设计呢。
如果要避免被限制在只能以“继承实现”来完成功能实现,可以考虑使用桥接模式。从需求可以看出,基本上是两个群组之间,关系呈现“交叉组合汇编”的情况。
- 群组一的“抽象类”指的是将对象或功能经“抽象”之后所定义出来的类几口,并通过子类继承的方式产生多个不同的对象或功能。例如上述的“形状”类,其用途是用来描述一个有“形状”的对象应该具备的功能和操作方式。所以,这个群组只负责增加“首次昂类”不负责实现“接口定义的功能”。
- 群组二的“实现类”指的是这些类可以用来实现”抽象类“中所定义的功能。例如上述例子中的OpenGL引擎和DirectX引擎类,它们可以用来实现“形状“类中所定义的”绘出“功能,能将形状绘制到屏幕上,所以,这个群组只负责增加”实现类“。
继承”抽象类“的子类需要实现功能时,只要通过”实现类“的对象引用m_RenderEngine来调用实现功能即可。这样一来,就真正让”抽象与实现分离“,也就是”抽象不与实现绑定“,让”球体“或”立方体“这种抽象概念的类,不在通过产生不同子类的方式去完成特定的”实现方式“(OpenGL或DirectX),将”抽象类群组“与”实现类群组“彻底分开。
运用桥接模式后的”形状“类,不必在考虑要使用OpenGl还是DirectX进行绘制,因为RenderEngenr类接口,已经真正实现与客户端(IShaper)分开了。
具体实现
//绘图引擎
public abstract class IRenderEngine
{
public abstract void Draw(string ObjName);
}
//Directx引擎
public class DirectX : IRenderEngine
{
public override void Draw(string ObjName)
{
Debug.Log("DXRender:" + ObjName);
}
}
//OpenGL引擎
public class OpenGL : IRenderEngine
{
public override void Draw(string ObjName)
{
Debug.Log("GLRender:" + ObjName);
}
}
//形状
public abstract class IShape
{
protected IRenderEngine m_RenderEngine;
public void SetRenderEngine(IRenderEngine theRenderEngine)
{
m_RenderEngine = theRenderEngine;
}
public abstract void Draw();
}
//球体
public class Shape : IShape
{
public override void Draw()
{
m_RenderEngine.Draw("Shape");
}
}
// 立方体
public class Cube : IShape
{
public override void Draw()
{
m_RenderEngine.Draw("Cube");
}
}
// 圆柱体
public class Cylinder : IShape
{
public override void Draw()
{
m_RenderEngine.Draw("Cylinder");
}
}
public class Test
{
public void Mian()
{
//测试绘制DX平台的正方体
IShape m_Shap = new Cube();
m_Shap.SetRenderEngine(new DirectX());
m_Shap.Draw();
}
}
再谈角色与武器的实现
桥接模式除了能够应用在”抽象与实现“的分离之外,还可以应用在:当两个群组应为功能上的需求,想要链接合作,但有希望两组类可以各自发展不受彼此影响时。
参与者说明如下:
- ICharacter:角色的抽象接口拥有一个IWeapon对象引用,并且在接口中声明了一个武器攻击目标WeaponAttackTarget()方法可以让子类调用,同事要求继承的子类必须在Attack()中重新实现攻击目标的功能。
- ISoldier、IEnemy:双方阵营单位,实现攻击目标Attack时,只需要调用父类的WeaponAttackTarget()方法,就可以使用当前装备的武器攻击对手。
- IWeapon:武器接口,定义游戏中对于武器的操作和使用方法。
- WeaponGun、WeaponRifle、WeaponRocket:游戏中可以使用的3种武器的实现。
使用桥接模式的优点
运用桥接模式后的ICharacter(角色接口)就是群组一”抽象类“,它定义了”攻击目标“功能,但真正实现”攻击目标“功能的类,则是群组二IWeapon(武器接口)”实现类“,对于ICharacter及其继承类都不必例会IWeapon群组的变化,尤其是游戏开发后期可能增加武器类型。而对于ICharacter来说,它面对的只有IWeapon这个接口类,相对地,IWeapon类群组也不必例会角色群组内的新增或修改,让两个群组之间的耦合度降到最低。
桥接模式的应用
- 游戏角色可以驾驶不同的行动载具,如汽车、飞机、水上摩托车
- 奇幻类型游戏的角色可以施展法术,除了多样的角色之外,“法术”本身也是另一个复杂的系统,火系法术、冰系法术……,远程法术、近战法术、补血法术……,想额外加上使用限制的话,就必须使用桥接模式让角色与法术群组妥善结合