上一篇:Xcode与C++之游戏开发:Pong游戏

游戏对象

在前面的 Pong 游戏中,没有用不同的类去代表墙,球拍和球,仅仅使用了一个 Game 类。像 Pong 这种简单游戏当然没问题,但它不是可扩展的解决方案。为了可扩展性,将墙、球拍、球分别用不同的类表示是更好的选择。

游戏对象(game object),指的是游戏中任何需要更新和绘制的事物。表示游戏对象存在不同的方法,有的采取层次结构,有的采用组合,也有更复杂的设计模式。但不管是哪种表示方法,游戏都需要某种方式来跟踪和更新这些游戏对象。

有时候,开发者会把在游戏中只绘制,不更新的对象称为静态对象。这些对象对玩家可视,但是从来不需要更新,比如关卡的背景、那些人畜无害的游戏建筑。相反,有的游戏对象只更新但不绘制,例如摄像机的视角,还有比如触发器。恐怖游戏可能希望在玩家接近门时出现僵尸。在这种情况下,关卡设计师会放置一个触发器对象,可以检测玩家何时接近并触发生成僵尸的动作。实现触发器的一种方法就是将其作为一个不可见的框,更新每一帧时检查与玩家的交集。

游戏对象模型

有很多的游戏对象模型,或者说有不止一种方式代表游戏对象。

类层次继承

一种游戏对象模型是在标准的面向对象的类层次结构中声明游戏对象,有时称为单一类层次结构,因为所有游戏对象都从一个基类继承。要用这种游戏模型,首先需要有一个基类。比如说,像这样的。

class Actor
{public:virtual void Update(float deltaTime);virtual void Draw();
};

之后,就可以拥有不同的子类。

class PacMan : public Actor
{public:void Update(float deltaTime) override;void Draw() override;
};

这种实现的一个缺点是每个游戏对象必须拥有基类的所有的属性和方法。但就像之前说的那样,在某些游戏对象上调用 Draw 是在浪费时间。

随着游戏功能的增加,问题可能会更为明显。例如游戏有两个角色是可以移动的,但是有的角色不能移动。如果把移动的代码放到基类 Actor 中,但又不是每个对象都可以移动。按照计算机界的环境法则,表达力不够,就加一层。那么可以在再编写 MovingActor,但这无疑会使得类继承上变得更加复杂。

更进一步地,当两个兄弟类稍后需要在它们之间共享特征时,一个庞大的类层次结构可能导致更大的困难。例如,侠盗猎车手的游戏可能会有一个 Vehicle 类。从这个类中,创建两个子类可能是有意义的:LandVehicle(用于穿越陆地的车辆)和 WaterVehicle(用于水上交通,如船)。

那么如果哪一天想不开,想要有水陆两栖车。由于 C++ 允许多继承,所以一种解决方法是定义一个 AmphibiousVehicle 同时继承 LandVehicleWaterVehicle。多继承也意味着 AmphibiousVehicle 沿着两条不同的路径从 Vehicle 继承。这种类型的层次结构(称为菱形继承)可能会导致问题,因为子类可能会继承虚函数的多个版本。因此,通常建议避免采用多继承

组件化

越来越多的游戏为了避免使用庞大的继承体系,采取了基于组件化(component-based)的游戏对象模型。这种模型越来越流行,一个很重要的原因是 Unity 游戏引擎使用了这种模型。这种实现方案中,有一个游戏对象类,但是没有子类。采取的是“有一个”(has-a)组件的集合对象来实现需要的功能。

继承是“is-a”(是一个)的关系。若遵循里氏替换原则,有父类出现的地方,就可以用子类替换(“子类”也是一个“父类”)。

举个例子,就上面那张 Pic-Man(吃豆人) 的类继承图而言,PinkyGhost 的子类,Ghost 又是 Actor 的子类。如果采取基于组件的模型,Pinky 是一个 GameObject,包含4个组件:PinkBehaviorCollisionComponentTransformComponentDrawComponent

如果可以用谷歌,搜索吃豆人,可以在线玩这个游戏(google官方出品)。

这些组件都可以拥有自己的特定的属性和方法。例如,DrawComponent 可以用于处理在屏幕上绘制对象的功能,而 TransformComponent 用来存储游戏世界中游戏对象的位置和变化。

class GameObject
{public:void AddComponent(Component* comp);void RemoveComponent(Component* comp);
private:std::unordered_set<Component*> mComponents;
};

注意 GameObject 仅仅包含添加和移除组件的函数。例如,每个 DrawComponent 可以注册有 Renderer,这样 DrawComponent 需要绘制帧的时候,Renderer 可以意识到。使用基于组件的模型的一个优点就是可以很容易地为游戏对象添加它所需要的特别功能。任意的对象需要绘制,就可以包含一个 DrawComponent

组件化的缺点是纯组件系统相同游戏对象下的组件依赖是不明确的。比如说,DrawComponent 需要知道 TransformComponent 才能知道到哪里才应该绘制。这就意味着 DrawComponent 需要询问自己的 GameObject 关于 TransformComponent 的信息。依赖这种实现,这些查询就会成为显而易见的性能瓶颈。

具有组件的层次结构

为了在上述两种模型中找到一个折中方案,可以考虑将继承与组件结合起来。这种混合的游戏模型被用在了虚幻4引擎中。同样是一个 Actor 基类,也带有虚函数,但同时也有一个 vector 类型的组件集合。

class Component
{public:// 构造函数// (值越低的更新顺序,则组件越早更新)Component(class Actor* owner, int updateOrder = 100);// 析构virtual ~Component();// 通过增量时间更新组件virtual void Update(float deltaTime);int GetUpdateOrder() const { return mUpdateOrder; }
protected:// 属于的角色class Actor* mOwner;// 组件的顺序int mUpdateOrder;
};

Actor 类有几点值得注意。状态的枚举 State 跟踪角色的状态。例如,Update 仅仅在 EActive 状态下更新角色。EDead 则代表游戏要移除角色。Update 函数调用先调用 UpdateComponents,之后调用 UpdateActorUpdateComponents 循环遍历所有组件并依次更新。UpdateActor 的基类实现是空的,但 Actor 的子类将用特定的行为重写 UpdateActor 函数。

某些情况下,Actor 类需要接收 Game 类,包括创建附加的角色。一种实现方法是使游戏对象作为单例(singleton)。单例设计模型使得全局可以获取这个类的一个实例。但是单例模式可能会导致其它的问题,比如全局需要多个实例的时候。因此这里采取的是另外一种实现,被称为依赖注入(dependency injection)。

在依赖注入的实现中,构造函数接收 Game 类的指针。一个角色使用这个指针去创建其它的角色。Vector2 是角色的位置,除此之外 mScalemRotation 则用来放缩和旋转角色。注意,旋转采用的是弧度,而不是角度。

Component 类中 mUpdateOrder 值得注意。它可以用来确定要更新的组件之前或者之后的其它组件。这在很多情形下是有用的。例如,跟踪玩家的相机(camera)组件可能想要在移动(movement)组件移动玩家之后更新。为了保持这种顺序,因此 AddComponent 在添加新组件时会排序组件向量。最后,Component 类中有一个指针指向了自己的角色(actor)。这样一来,组件可以在必要的时候获取变形的数据或者任何其它信息。

class Component
{public:// 构造函数Component(class Actor* owner, int updateOrder = 100);// 析构virtual ~Component();// 通过增量时间更新组件virtual void Update(float deltaTime);int GetUpdateOrder() const { return mUpdateOrder; }
protected:// 属于的角色class Actor* mOwner;// 组件的顺序int mUpdateOrder;
};

这种混合模型可以避免深层次的继承,但可以确定的是,这个模型的继承深度会比纯组件化模型要深。一般而言,混合模型可以避免,但不是完全可以消除组件之间交流的问题。这是因为每个角色都有自己重要的属性,比如变换的数据。

其它方案

还有其它的游戏模型的实现。有的采用接口类来定义不同的函数集。每个游戏对象通过实现必要的接口来代表它。也有的模型拓展了组件模型,进一步地避免包含整个游戏对象。

每一种游戏模型都有它的优点和缺点。之后会采用继承和组件化的混合模型,相对而言,它是个不错的模型,复杂度也相对可控。

在游戏循环中集成游戏对象

要在游戏循环中融入混合游戏对象模型,需要多写一些代码,但是并不复杂。先在 Game 中添加两个 std::vector 向量,类型是 Actor* ,一个用来包含活动的 actors(mActors),一个用来包含一个待定的 actors(mPendingActors)。

 // 游戏中所有的 actorstd::vector<class Actor*> mActors;// 任意待定的 actorstd::vector<class Actor*> mPendingActors;

之所以需要待定的 actors(mPendingActors),是为了处理更新 actors时(遍历 mActors),决定创建新的 actor。在这种情况下是不能直接添加元素到 mActors 的,因为正在用迭代器遍历(一旦添加,迭代器就失效了)。所以,我们把元素加入到这个待定 actors mPendingActors 中,等到遍历完 actors 后,把它加入到 mActors中。

接下来,添加两个函数 AddActorRemoveActorAddActor 添加 actor 到 mPendingActors 或者 mActors。至于加到哪个向量之中,取决于目前是否在更新 mActors(通过一个 mUpdatingActors 布尔量进行判断)。

void Game::AddActor(Actor* actor)
{// 如果正在更新 actors,需要添加到待定向量中if (mUpdatingActors){mPendingActors.emplace_back(actor);}else{mActors.emplace_back(actor);}
}

类似的,RemoveActor 从相应的 vector 中移除 actor。

void Game::RemoveActor(Actor* actor)
{// 是否在待定 actor 中auto iter = std::find(mPendingActors.begin(), mPendingActors.end(), actor);if (iter != mPendingActors.end()){// 交换到尾部(避免复制)std::iter_swap(iter, mPendingActors.end() - 1);mPendingActors.pop_back();}// 是否在 actor 中iter = std::find(mActors.begin(), mActors.end(), actor);if (iter != mActors.end()){// 交换到尾部(避免复制)std::iter_swap(iter, mActors.end() - 1);mActors.pop_back();}
}

将需要删除的元素与最后一个元素进行交换,再移除最后一个元素,可以避免向量删除中间元素时后面的元素需要向前覆盖的性能损耗。这是一个数组删除时的惯用手法,也可以说是C++的惯用法之一。

UpdateGame 方法中要在计算增量时间(delta time)后更新所有的 actors。首先循环遍历 mActors 中的每个 actor 并且调用 Update。接下来,可以将待定的 actors 转移到 mActors 中。最后,如果有哪个 actor 的状态是 EDead,则删除。

void Game::UpdateGame()
{// 计算增量时间// 从上一帧等待16mswhile (!SDL_TICKS_PASSED(SDL_GetTicks(), mTicksCount + 16));float deltaTime = (SDL_GetTicks() - mTicksCount) / 1000.0f;if (deltaTime > 0.05f){deltaTime = 0.05f;}mTicksCount = SDL_GetTicks();// 更新所有的 actorsmUpdatingActors = true;for (auto actor : mActors){actor->Update(deltaTime);}mUpdatingActors = false;// 将待定的actor加入到mActorsfor (auto pending : mPendingActors){mActors.emplace_back(pending);}mPendingActors.clear();// 添加废弃 actor 到另一个临时向量std::vector<Actor*> deadActors;for (auto actor : mActors){if (actor->GetState() == Actor::EDead){deadActors.emplace_back(actor);}}// 删除废弃的actor (从 mActors 中移除)for (auto actor : deadActors){delete actor;}
}

从游戏的 mActors 添加和删除 actor 也会增加代码的复杂性。后面的代码,Actor 对象会在构造器和析构函数中自动增加和删除。当然,这就意味着 Shutdown 的编写要更加仔细一点:

    while (!mActors.empty()){delete mActors.back();}

好了,这一篇仅仅是为了简单的介绍一下游戏对象设计模式。下一次,我们来讨论游戏中的精灵(sprite)。

下一篇:Xcode与C++之游戏开发: 精灵(Sprite)

Xcode与C++之游戏开发: 游戏对象相关推荐

  1. H5数独游戏开发——游戏通关及重玩

    如何判断游戏是否通关?通关后如何重新开始游戏,让用户重玩?今天给大家详细介绍如何实现以上功能.先看一下数独游戏通关后的提示界面,截图如下: 首先,我们需要判断游戏是否通关,那么如何判断呢?有两个条件: ...

  2. H5数独游戏开发——游戏中的事件处理

    假设你正在玩一款角色扮演游戏(RPG),主角走进了一家店铺,你用鼠标点击店铺里管家,这时屏幕显示一个待售物品菜单,然后你可以选购你需要的物品.那么,你知道从你点击鼠标的那一刻起,游戏是怎样响应的吗?让 ...

  3. Android游戏开发–游戏循环

    游戏循环是每个游戏的心跳. 到目前为止,我们仅使用了非常简单的一种(您可以在此处找到),无法控制我们更新游戏状态的速度或速度以及要渲染的帧. 概括地说,最基本的游戏循环是while循环,该循环一直执行 ...

  4. android_Android游戏开发–游戏循环

    android 游戏循环是每个游戏的心跳. 到目前为止,我们使用的是非常简单的游戏(您可以在此处找到),无法控制我们更新游戏状态的速度或速度以及要渲染的帧. 概括地说,最基本的游戏循环是while循环 ...

  5. android游戏开发_Android游戏开发–游戏循环

    android游戏开发 游戏循环是每个游戏的心跳. 到目前为止,我们仅使用了非常简单的一种(您可以在此处找到),无法控制我们更新游戏状态的速度或速度以及要渲染的帧. 概括地说,最基本的游戏循环是whi ...

  6. 1.15 从0开始学习Unity游戏开发--游戏UI

    上一章中,我们剩下最后一个任务,需要支持鼠标控制准心来进行设计,那么准心本质上就是一个始终呈现在屏幕上的一个图片,你当然可以用一个3D物体来制作,之前讲解渲染概念的时候也提到过,我们的屏幕就是相机的近 ...

  7. 数独游戏开发——游戏主面板显示

    上次给大家介绍了如何实现资源加载进度条,资源加载完毕后就会进入游戏主界面.我们接着来实现游戏的功能--显示游戏主面板. 目前我们的数独游戏在资源加载完毕后就没有后续动作了,即游戏画面一直停留在进度条显 ...

  8. 游戏运营全过程剖析,游戏开发,游戏运营,游戏推广问题分析

    产品策划:一个好的idea至关重要,结合当时的社会环境,游戏开发水平,玩家喜好,市场需求等考虑.精美的画面,流畅的游戏节奏,情节,完善的游戏系统能让你的游戏立于不败之地. 产品开发:游戏主线框架定好后 ...

  9. Unity游戏开发-游戏热更新以及登录流程

    本篇主要分享基于热更新的游戏初始化方案. 整体初始化的流程大致为:检查是否需要解压资需要则解压,之后再检查是否存在需要热更新的资源文件需要则更新,更新完成后则初始化结束可进入登录界面. 关于登录这块的 ...

最新文章

  1. CDH大数据集群安全风险汇总
  2. SourceTree 3.0.17如何跳过注册进行安装? — git图形化工具(一)
  3. AbstractListView源码分析5
  4. linux时钟告警,Linux记录-告警脚本
  5. ai边缘平滑_AI基础教程113:“效果”菜单之“画笔描边”(一)喷溅效果
  6. 前端学习(2767):下拉刷新的学习
  7. c#打印,输出一句话
  8. 用openbabel将pdbqt文件转成pdb
  9. LAMP架构以及论坛的安装
  10. 十六进制的字符对照表
  11. OI中常见的数学符号
  12. 字节跳动Java工资待遇等级_字节跳动面试题:你的平均薪水是多少?
  13. win10系统更新服务器出错怎么办,win10系统正式版自动更新出错的解决方法
  14. ES插件es-head下载和安装
  15. 51单片机ADDA数模转换
  16. python中sticky_position: sticky 详解(防坑指南)
  17. python九宫格拼图,Python生成九宫格图片
  18. android studio最新教程pdf下载,android studio教程pdf下
  19. 【Avro二】Avro RPC框架
  20. C语言学习笔记(十五)

热门文章

  1. 位图+布隆过滤器-待续
  2. 【背包dp】自然数拆分Lunatic版
  3. python期末试题汇总
  4. 2019年总结和展望
  5. 6691. 【2020.06.05省选模拟】六道剑「一念无量劫」
  6. 家用计算机的普及英语作文,优秀高二英语作文:计算机
  7. 认识JavaScript
  8. 一起来找茬:下面这段代码是让计算机在屏幕上输出“hi”。其中有三个错误,快来改正吧
  9. 笔记:图解系统(小林coding)
  10. 查看FILEZILLA的快速连接密码