找回密码
 立即注册
查看: 296|回复: 0

游戏开发设计模式回忆-导语

[复制链接]
发表于 2022-5-15 10:32 | 显示全部楼层 |阅读模式
游戏开发设计模式回忆-导语

为什么要回忆?


  • 在项目推进的过程中,个人做了很多需求开发。
  • 在版本迭代中发现,不好的代码设计其实是在给自己埋无数的坑。 终有一天问题集中爆发的时候,版本风险就极其不可控了,甚至改一处就需要全量测试。
  • 本人也是在之前学过全套的理论,现在浅薄地自认为有了一些经验。
  • 现在回过头看可以更加深刻理解一下这些前辈们留下的成熟经验。
回忆中的核心思想

痛点,这是本人在编码过程中的最 Care (放句洋屁)的一个点。 *
只有了解原先的设计,设计的缺陷及痛点,更好的设计,才是学习代码,精进自己代码的最优解。
为什么还要提及一些老生常谈的概念?

为什么还要提及烂到大街的概念?
因为其实每个最简单的知识,背后隐含的东西都是庞大的。 很多理论我们自以为是地认为知道,但是实际上一被问到就拉稀。
如何在向别人讲述起理论的时候,有整套自己精简过的知识体系对答如流?
这个问题一直是鄙人研究的核心问题。
核心思想指导你解决问题,而不是由解决问题的过程推导你自己有的核心思想
这是一个自上而下的过程而不是自下而上的过程。
起航

面向对象编程

主旨


  • 面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式。
  • 个人简化:在编程过程中把问题里面的内容进行概念抽象,以抽象出的个体类为核心解决问题的编程方法。
特性


  • 该编程方法需要编程语言有以下特性进行支持,同时以下特性也是设计思想。
  • 封装:

  • 目的是为了隔离变化,隐藏内部实现细节,简化外部调用复杂度。
  • 把稳定的对外接口设置为public,易变的内部实现代码细节设置为private
  • 把大量不需要的接口不允许外部调用,简化调用者的可调用函数数量。
class CalculateProcess
{
   private:
   void Step1(){};
   void Step2(){};
   void Step3(){};
   void Step4(){};
   public:
   //启动计算流程
   //其实我们可以看到,对外只需要看到启动和结束流程
   //两个人分工的话,一个人关注这个类的维护,不需要关注调用者如何调用。调用者只需要关注Start()和End接口就行。
   //从这个角度看,其实把变化对外部隔离开了,我自己怎么变都行,外部不需要关心。
   void Start()
   {
      Step1();
      Step2();
      Step3();
      Step4();
   };
   void End(){};
};

  • 继承:

  • 目的是为了复用代码,对我们的抽象进行归类,继承。
<li data-pid="G37IDyC-">多态:

  • 目的是为了子类能够代替父类,隔离变化,对接口的设计可以对依赖关系进行解耦。
//目前以我的观点看继承+多态有三种最显著的应用。
//1.解耦依赖,缩短编译时间。
//2.隐藏实现,保护源代码。
//1和2一起来说一下
//以游戏引擎引入的第三方库为例
//对于游戏引擎开发者来说,只需要关注接口就行。
//第三方库的开发者来说,只需要关注库本身的开发
//节省了各自开发的时间
//而且我们只把.h和lib给使用者,.h里面只有虚函数接口,保护了自己的实现代码。
//.h
enum InstanceType
{
    EPoint2D,
    EPoint3D,
    EPoint4D,
}
class Interface_PointA
{
public:

    static Interface_PointA* GetInstance(InstanceType InInstanceType);
private:

};
//.cpp编译出来的.lib
#include "XXX.h"
class  Point2D :public Interface_PointA
{
public:

private:

};
static Interface_PointA* GetInstance(InstanceType InInstanceType)
{
    switch(InInstanceType)
    {
        case InstanceType::EPoint2D:
        return new Paper2D();
        break;
    }
    return nullptr;
}
//3.搭建框架,分工合作,隔离变化。
//搭框架的人可以和细化框架的人分工合作
//各自处理各自的需求
class CalculateProcess
{
   protected:
   virtual void Step1(){};
   //子类可以重写
   virtual void Step2(){};
   virtual void Step3(){};
   //子类可以重写
   virtual void Step4(){};
   public:
   void Start()
   {
      Step1();
      Step2();
      Step3();
      Step4();
   };
   void End(){};
};
class CalculateProcessDetail:CalculateProcess
{
   virtual void Step1(){};
   virtual void Step2(){};
   virtual void Step3(){};
   virtual void Step4(){};
};

  • 可选:抽象
接口类与抽象类的区别


  • 接口类一般支持多继承,不能包含属性
  • 抽象类一般只应用在单继承场景,可以包含属性。
设计模式中的各种原则

<hr/>

  • -SRP 单一职责原则 一个类或者模块只完成一个功能分支。 个人觉得功能分支更为恰当.
<hr/>

  • -OCP 开闭原则 对扩展开放,对修改关闭。 提前留好拓展点
//伪代码如下:
//改写Monster类,受到伤害给其他东西一个通知
void takedamage(AActor* InDamagedResource,int DamageAmount)
{

}

//=============改写方法1
//Monster类
//声明变量
AActor *NotifyedActor1;
AActor *NotifyedActor2;
AActor *NotifyedActor3;
///...
AActor *NotifyedActor100;
void takedamage(AActor* InDamagedResource,int DamageAmount)
{
   NotifyedActor1.Notify();
   NotifyedActor2.Notify();
   NotifyedActor3.Notify();
   ///...
   NotifyedActor100.Notify();
}

//=============改写方法2
//向扩展开放,向修改关闭修改
//省略了验证指针有效性步骤
//示例代码太复杂就看不懂了
//Monster类
class AActor:public Interface_A
{
public:
    virtual void Notify()
    {
        //
    }
};
class AActor1 :public Interface_A
{
public:
    virtual void Notify()
    {
        //
    }
};
typedef void (Interface_A::*DelegateFunc)()   ;

class Monster
{
public:

    struct DelegateData
    {
        Interface_A* P;
        DelegateFunc FUNC;
        DelegateData(Interface_A* InNewActor, DelegateFunc InNewFunc)
        {
            P = InNewActor;
            FUNC = InNewFunc;
        }
    };

    vector<DelegateData > NotifyedActorArray;

    void AddNotify(Interface_A* InNewActor,DelegateFunc InNewFunc)
    {
        NotifyedActorArray.push_back(DelegateData(InNewActor, InNewFunc));
    }
    void TakeDamage()
    {
        for (size_t i = 0; i < NotifyedActorArray.size(); i++)
        {
            (NotifyedActorArray.P->*NotifyedActorArray.FUNC)();
        }

    }

};

  • 改写方法1有什么问题?

  • 每次增加通知都需要修改Monster
  • 只能调用Actor类的Notify函数
<li data-pid="q9yxtXyY">如果此时有需要,同时需要通知多个非Actor对象的多个函数? 你该如何处理?

  • 需要给每个通知都加一个变量和加一行代码。
试想一下:
每个版本只要修改了这个部分, 一定是要进行测试的,
修改了Monster类, 意味着影响范围会非常大
没有哪个程序员能保证自己修改100%没有问题。
付出的成本会很高。
测试时间+CoderReview时间
============================

  • 改写方法2如何?

  • 增加了代理机制。
  • 同时需要通知多个非Actor对象的多个函数。
  • 每次新加通知不需要修改Monster类。
<li data-pid="RCjWjh-k">如果此时有需要,同时需要通知多个非Actor对象的多个函数? 你该如何处理?<li data-pid="c7yHNlzg">需要给每个通知都加一个变量和加一行代码。试想一下:
每个版本只要修改了这个部分一定是要进行测试的,
这种类似的按照开闭原则设计的类或者模块大量存在,
其实会大大降低测试时间+CoderReview时间
在大规模商业化程序设计中,时间就是金钱。
<hr/>

  • -LSP 里式替换原则

    • Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
    • 在不知道子类具体内容的情况下,在程序中父类指针指向的对象可以直接拿子类对象来替换。

  • 换句话说:你的子类对象是可以在你没有看过子类代码的情况下,直接替换父类对象的。
  • 满足这个定义就要求:

    • 子类不能改变父类的基本流程。
    • 子类的修改仅限于对父类拓展与增强。

<hr/>

  • -ISP 接口隔离原则 Clients should not be forced to depend upon interfaces that they do not use 当前的类不应该强行继承他们根本用不到的接口.
  • 个人认为两个方向看这个问题

  • 用到接口的类
  • 接口类本身
<li data-pid="WMc1MUM0">包含多个功能分支的类依赖了多个包含多个功能分支的接口,部分接口压根用不到。<li data-pid="QrOLe81T">修改建议:

  • 拆分用到接口的业务类,保证单一职责。
  • 拆分臃肿的接口,保证单一职责。
<hr/>

  • -DIP 依赖倒置原则
  • High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
  • 翻译过来:高层模块不应该依赖低层模块。高层模块和低层模块依赖抽象,抽象之间互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
//给各位看一段代码
//我们此时有一个第三方库
//此时我们想在引擎中引入,而且在调试的时候希望利用引擎Log系统输出第三方的LOG信息
//而且有一个要求:第三方组件不能依赖引擎,因为第三方组件是面向所有引擎的。
//你应该如何去做?
//================================第三方库.h==================
class Abstarct_LogManager
{
public:
    virtual void Print(const char* InTest) = 0;
};
void Start(Abstarct_LogManager* B);
//================================第三方库.CPP(.lib)==================
//不给到用户,只给.lib
#include "Test.h"
Abstarct_LogManager* log;
void Start(Abstarct_LogManager* B)
{
    log = B;
    log->Print("Hello1");
    log->Print("Hello2");
    log->Print("Hello3");
    log->Print("Hello4");
}
//==============================给第三方库创建中间层Middle.h==================
#include"Test.h"
template<typename T>
class Impl_Abstarct_LogManager:public Abstarct_LogManager
{
public:
    T* Outer;
    Impl_Abstarct_LogManager(T* InOuter)
    {
        Outer = InOuter;
    }
    virtual void Print(const char* InTest)
    {
        Outer->EnginePrint(InTest);
    };
};
//==============================引擎层==================
//引擎链接.lib,拿过来.h
#include"Test.h"
class LogManager
{
public:
    LogManager();
    ~LogManager();
    void EnginePrint(const char* InTest)
    {
        cout << InTest <<endl;
    }
private:

};
void InitialEngine()
{
   LogManager Logmanager;
   Impl_Abstarct_LogManager<LogManager>b(&Logmanager);
   Start(&b);
}
//为什么这么做?因为这么做实现了
//1.依赖关系正确:
//引擎层依赖中间层和第三方库,
//中间层依赖第三方库,
//单向依赖,
//没有任何双向依赖。
//2.保证了第三方库的通用性
//第三方库不需要针对引擎进行单独修改
//3.第三方库通过抽象和引擎层交互,而具体实现依赖于抽象
//如此设计第三方库不需要修改即可适配所有引擎,对应引擎只需要自己实现一个中间层就行。<hr/>

  • DRY 原则: 不要写重复的代码。 我觉得这一条最难应用,因为没人愿意写重复代码。 功能重复,流程校验重复这些都算,但是如果考虑到风险原因,改与不改就真成问题了。
<hr/>

  • YAGNI 原则: 当前的设计满足当前的需求且留有一定拓展性,不需要过度设计。
<hr/>

  • KISS 原则:Keep it Simple and Stupid 其实就是在保证代码质量的前提下,让代码设计的尽量简单易读,别整一些花里胡哨的~
<hr/>

  • LOD 法则: 高内聚,低耦合 高内聚是一个功能分支相关的高度依赖的集中到一起。 低耦合是减少多个功能分支之间的直接依赖关系。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-27 07:44 , Processed in 0.090104 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表