返回

5.桥接模式

将抽象与实现分离,使二者可以独立地变化。

角色与武器的实现

需求:

三种武器类型:手枪、散弹枪、火箭,以“攻击力”和“攻击距离”区分它们的威力。此外,“武器发射”和“击中目标”时会有不同的音效和视觉效果。双方阵营都可以装备这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
{
    //同上
    ...
}

存在的问题

  1. 每个继承自Icharacter角色接口的类,在重新定义Attack方式时,都必须针对每一种武器来实现(显示特效和播放音效),或者进行额外的公式计算。所以当要新增角色类时,也要在新的子类中重复编写相同的程序代码。
  2. 当要新增武器类型时,所有角色子类中的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类群组也不必例会角色群组内的新增或修改,让两个群组之间的耦合度降到最低。

桥接模式的应用

  • 游戏角色可以驾驶不同的行动载具,如汽车、飞机、水上摩托车
  • 奇幻类型游戏的角色可以施展法术,除了多样的角色之外,“法术”本身也是另一个复杂的系统,火系法术、冰系法术……,远程法术、近战法术、补血法术……,想额外加上使用限制的话,就必须使用桥接模式让角色与法术群组妥善结合