【问题标题】:Object Oriented application problems in game development游戏开发中的面向对象应用问题
【发布时间】:2010-12-30 01:03:05
【问题描述】:

对于这个问题,我会尽可能直截了当,因为结构化编程背景一定是我完全缺少的东西。

假设我有一个 Player 课程。这个 Player 类做一些事情,比如改变它在游戏世界中的位置。我将此方法称为 warp(),它以一个 Position 类实例作为参数来修改 Player 的内部位置。这在 OO 方面对我来说完全有意义,因为我要求玩家“做”一些事情。

当我除了修改球员位置之外还需要做其他事情时,问题就出现了。例如,假设我需要将该扭曲事件发送给在线游戏中的其他玩家。该代码是否也应该在 Player 的 warp() 方法中?如果不是,那么我会想象在 Server 类中声明某种辅助方法,例如 warpPlayer(player, position)。这样做似乎会减少玩家对自己作为一系列 getter 和 setter 所做的一切,还是我在这里错了?这是完全正常的事情吗?我已经读过无数次了,将所有内容公开为一系列 getter/setter 的类表明抽象非常差(被用作数据结构而不是类)。

当您需要持久化数据并将其保存到文件时,也会出现同样的问题。由于将播放器“保存”到文件与 Player 类处于不同的抽象级别,因此在播放器类中具有 save() 方法是否有意义?如果没有,像 savePlayer(player) 这样在外部声明它意味着 savePlayer 方法需要一种方法来从 Player 类中获取它需要的每条数据,这最终会暴露该类的整个私有实现。

因为 OOP 是当今最常用的设计方法(我猜?),所以我在这些问题上一定缺少一些东西。我已经与同样从事轻量级开发的同行讨论过,他们也遇到了与 OOP 完全相同的问题。也许正是结构化编程背景使我们无法理解 OOP 的全部好处,而不仅仅是提供设置和获取私有数据的方法,以便从一个地方对其进行更改和检索。

在此先感谢您,希望我听起来不会太像个白痴。对于那些真正需要了解这种设计所涉及的语言的人来说,它是服务器端的 Java 和客户端的 ActionScript 3。

【问题讨论】:

  • StackOverflow 不太适合回答这类问题。正如您所观察到的,OO 编程非常成功,因此我建议您通过阅读几本书并编写一些代码来解决您的问题。
  • 我读过几本,包括 Code Complete、Clean Code、The Practice of Programming,还有一些我记不得的。如果其中任何一个直接涉及该主题,请告诉我。我已经看了好几遍了,没有看到任何实质性的东西。
  • 我读过 CoComp 和 TPOP(非常好)——干净的代码,我不会用马桶刷。但它们都不是真正关于 OO 的。现在找到一个不“获得” OO 优势的人是不寻常的——我自己的 codse 一直都是以 OO 风格编写的(我已经这样做了 30 年了)所以我可能不是正确的有人要求推荐书,但我一直很喜欢 Grady Booch 的东西。
  • 好吧,我还阅读了 Java 的面向对象编程,因此我了解 OO 实践背后的所有术语,甚至阅读了傻瓜设计模式。我明白了所有这些,但没有一个能解决我认为 OOP 的核心问题。

标签: oop object


【解决方案1】:

我可能会考虑拥有一个跟踪玩家对象的 Game 对象。所以你可以做类似 game.WarpPlayerTo(WarpLocations.Forest);如果有多个玩家,可能会传递一个玩家对象或 guid。我觉得您仍然可以保持 OO,并且我认为游戏对象可以解决您的大部分问题。

【讨论】:

  • 它仍然是 OO,但至少在我看来,玩家类最终还是会成为大量的 getter 和 setter。想象一下攻击一名球员。我必须有一个像 player.getVitals().setHP(int) 或 player.getVitals().damageHP(int) 这样的方法,并且仍然需要有另一个辅助方法来实际计算基于攻击者对受害者的伤害。这最终使 Player 仍然是一系列 getter/setter。或者可能这样做: player.attack(attackerPlayer) 在内部完成所有计算。在这种情况下,无法通过网络发送更新。
【解决方案2】:

您所描述的问题不仅属于游戏设计,还属于一般的软件架构。常见的方法是有一个依赖注入(DI)控制反转(IoC)机制。简而言之,您想要实现的是能够从您的对象访问各种本地 Service,以便例如传播某些事件(例如扭曲)、日志等。

控制反转简而言之,不是直接创建对象,而是告诉某个服务为您创建它们,该服务反过来使用依赖注入来通知对象它们所依赖的服务。

【讨论】:

  • 在这种情况下,无论它是否是服务,实际上都在实例化这些对象(例如:播放器),播放器对象仍然需要对服务的引用(假设它不是静态的),以便它可以调用这些函数。这不是将 Player 对象与服务对象紧密耦合吗?在这种情况下,例如 warp() 将与 service.sendWarpEvent() 方法紧密耦合。这甚至是可接受的 OO 实践吗?
  • 通过让您的对象使用服务的接口,并独立实例化实际的服务对象,您可以实现解耦。因为您可以随时替换实际的服务实现。当然,接口需要保持不变,但耦合是根据实现而不是接口来衡量的。
【解决方案3】:

如果您在多人游戏的不同 PC 之间共享数据,那么程序的核心功能是在 PC 之间保持和同步该状态。如果您将这些值分散在许多不同的类中,将很难同步。

在这种情况下,我建议您设计需要在所有客户端之间同步的数据,并将其存储在单个类中(例如 GameState)。该对象将处理不同 PC 之间的所有同步,并允许您的本地代码请求更改数据。然后它将从自己的状态“驱动”游戏对象(玩家、EnemyTank 等)。 [编辑:这样做的原因是保持此状态尽可能小并在客户端之间有效地传输它将是您设计的关键部分。通过将所有内容保存在一个位置,可以更轻松地执行此操作,并鼓励您只将绝对必要的内容放在该类中,这样您的通讯就不会因不必要的数据而变得臃肿]

如果您不进行多人游戏,并且您发现更改玩家的位置需要更新多个对象(例如,您希望相机知道玩家已经移动以便它可以跟随他),那么一个好的方法是让玩家对自己的位置负责,但引发其他对象可以订阅/收听的事件/消息,以便知道玩家的位置何时发生变化。所以你移动玩家,相机会收到一个回调,告诉它玩家的位置已经更新。

另一种方法是相机在每一帧都读取玩家的位置以更新自身 - 但这不像使用事件那样松散耦合和灵活。

【讨论】:

  • 在您的示例中,将所有与游戏相关的数据存储在单个类 GameState 中,我仍然需要在该类中定义函数来执行我需要对我的对象(如 Player)进行的所有处理。这仍然会创建重复的函数,并且我的所有类最终都是数据结构。从我在您的帖子中可以看出,您正在从该类中定义一个函数 API 来操作所有数据,对吗?
  • 是的。您可能仍将数据存储在单个对象中,但无论何时您想要移动 Player,您都会要求 GameState 更新它,而不是直接询问玩家。您还可以通过将 Player 方法设为私有但 GameState 的朋友来强制执行此用法,因此它是唯一可以更改它们的类。
【解决方案4】:

有时,OOP 的诀窍是了解什么是对象,以及对象的功能是什么。我认为我们通常很容易在概念上将诸如 Player、Monster、Item 等对象作为系统中的“对象”锁定,然后我们需要创建诸如 Environment、Transporter 等对象来将这些对象链接在一起,根据概念如何协同工作以及我们需要完成的工作,它可能会失控。

过去与我共事过的真正优秀的工程师有一种将系统视为对象集合的方式。有时在一个系统中它们是业务对象(如项目、发票等),有时它们是封装处理逻辑的对象(DyeInjectionProcessor、PersistanceManager),这些逻辑跨越系统中的多个操作和“对象”。在这两种情况下,隐喻都适用于该特定系统,并使整个过程更易于实施、描述和维护。

OOP 的真正威力在于让事情在大型复杂系统中更易于表达和管理。这些是面向对象的 OOP 原则,不必担心它是否适合严格的对象层次结构。

我没有从事过游戏设计工作,所以也许这个建议不会奏效,在我工作和开发的系统中,从简化和封装的角度考虑 OOP 而不是真正的 1 是一个非常有益的改变1 个 OOP 类的世界对象。

【讨论】:

  • 我会将此与 Code Complete 中的“上帝”类联系起来。这几乎就是我在原始帖子中为后一种解决方案所做的情况,这需要重复的方法定义。基本上 Player 的 warp() 函数只是整个过程的一部分,God 类中的辅助方法调用 Player warp 方法,以及其他方法来执行诸如将 warp 更新发送给其他玩家并可能输出的事情给玩家一条消息说“你被扭曲了”或其他什么。这仍然使 Player 基本上是一类 getter 和 setter。 ://
  • 实际上玩家只是“游戏对象”,没有玩家、岩石或怪物的概念,它只是一个游戏对象。如果那是它的预期功能,那么您的设计中的“包”类就没有真正的问题。这些与上帝类不同,因为它们本身并不与更高阶的控制类相关,而是与低阶的管道类相关。
  • 拥有像 GameObject、GamePlayer、GameNPC 等这样的继承层次结构仍然与此问题无关。我将这样的层次结构用于我的 2D 图形引擎,其中 OO 实践完美无缺。当我有“某事”需要完成时,它会在多个抽象领域产生涟漪效应。
【解决方案5】:

我想扩展 GrayWizardx 的最后一段,说并非所有对象都需要具有相同级别的复杂性。拥有简单的 get/set 属性集合的对象可能非常适合您的设计。另一方面,重要的是要记住对象可以表示任务或任务的集合,而不是现实世界的实体。

例如,玩家对象可能不负责移动玩家,而是代表其位置和当前状态。 PlayerMovement 对象可能包含用于更改玩家在屏幕上或游戏世界中的位置的逻辑。

在我开始简单地重复已经说过的内容之前,我将指向 OOP 设计的SOLID principles(Aviad P. 已经提到了其中两个)。它们可能会提供一些高级指南来为游戏创建良好的对象模型。

【讨论】:

  • 感谢 cmets。我认为你所说的几乎完全符合我的问题。在我看来,在某些情况下,它是封装和耦合之间的一种杂耍。在我上面的示例中,拥有一个轻度耦合的 Player 对象使其成为一系列破坏 imo 封装的 getter 和 setter。或者使用一个很好的封装 Player 对象,该对象引用其他服务对象或静态引用来完成其他需要完成的事情,从而使该类与其服务对象/静态引用的全局方法紧密耦合。
【解决方案6】:

我建议你不要害怕这样一个事实,玩家将是一类 getter 和 setter。到底什么是对象?它是属性和行为的汇编。事实上,您的类越简单,您在开发过程中获得的 OOP 好处就越多。

我会将你的任务/功能分解成这样的类:

玩家:

  • 有生命值属性
  • 有位置属性
  • 可以 walkTo(position),触发“walk”事件
  • 可以治愈(生命值)
  • 可以承受伤害(生命值),触发“isHurt”事件
  • 可以检查是否还活着,比如 isAlive() 方法

Fighter 扩展了 Player(您应该能够在需要时将 Player 转换为 Fighter):

  • 有力量和其他战斗参数来计算伤害
  • 可以通过attack() 触发“攻击”事件

世界跟踪所有玩家:

  • 监听“步行”事件(并防止非法移动)
  • 监听“isHurt”事件(并检查它们是否还活着)

Battle 处理两个战士之间的战斗:

  • 以两个战士为参数的构造函数(您只想构造真正互相战斗的玩家之间的战斗)
  • 监听双方玩家的“攻击”事件,计算伤害,并执行防御玩家的 takeDamage 方法

PlayerPersister 扩展了 AbstractPersister:

  • 将玩家的状态保存在数据库中
  • 从数据库中恢复玩家的状态

当然,你的游戏分解会复杂得多,但我希望这能帮助你开始以“更多 OOP”的方式思考问题:)

【讨论】:

  • 是的,这与我现在正在做的类似。我的 Player 类只是一堆获取/设置,然后我在 Game 类中有另一个方法,它具有 warpPlayer(player, newposition)。它只是不觉得这应该是最好的解决方案。我无法解释。
  • 不,在我的示例中,World 类根本无法移动 Player。玩家使用 walk() 方法移动自己(您不是从 World 调用 walk() 方法,而是从 UI 调用)。而 World 只能通过监听 Player 的“walk”事件来阻止玩家走到非法位置(仅举例来说,您可以实现自己的玩家行走与世界交互的逻辑)。
【解决方案7】:

不要太担心Player 类是一堆setter 和getter。 Player 类是模型类,模型类往往是这样的。重要的是您的模型类要小而干净,因为它们将在整个程序中重复使用。

我认为您应该使用您建议的warpPlayer(player, position) 方法。它使Player 类保持清洁。如果您不想将播放器传递给函数,也许您可​​以拥有一个包含Player 对象和warp(Position p) 方法的PlayerController 类。这样您就可以将事件发布添加到控制器,并将其排除在模型之外。

至于保存播放器,我会通过让Player 实现某种序列化接口来实现。播放器类负责对其自身进行序列化和反序列化,而其他一些类将负责将序列化的数据写入文件或从文件中写入。

【讨论】:

  • 例如,可能有一个名为 serialize 的方法,它返回一个包含类状态的键值对,然后可以使用另一个方法或构造函数接收键值对来恢复该状态。我担心在程序中到处都有所有这些序列化的东西,但没有考虑只在其中包含序列化部分,然后将实际输出到文件/数据库的实际输出在其他地方完成。感谢您的提示。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-23
相关资源
最近更新 更多