面向对象应用在游戏开发中的问题

10

考虑到我的结构化编程背景,我会尽可能直接地谈论这个问题。比如说我有一个Player类,它可以在游戏世界中改变自己的位置。我调用了warp()方法,并将一个Position类实例作为参数传入,以修改Player的内部位置。从面向对象的角度来看,这对我来说是完全合理的,因为我要求玩家“去执行”某些操作。

问题出现在我需要除了修改玩家位置之外做其他事情时。例如,假设我需要向在线游戏中的其他玩家发送warp事件。那么这段代码是否也应该包含在Player的warp()方法中呢?如果不是,那么我想象一下,在Server类中声明某种次要方法,例如warpPlayer(player, position)。这样做似乎将玩家所做的一切都简化为一系列获取器和设置器,或者我错了吗?这种情况是完全正常的吗?我已经无数次阅读过,将所有内容都公开为一系列getter / setter表示抽象性很差(被用作数据结构而不是类)。

当你需要将数据持久化,将其保存到文件时,同样的问题也会出现。由于将玩家“保存”到文件的抽象级别与Player类不同,所以在Player类中定义save()方法是否合理呢?如果不是,那么在外部声明savePlayer(player)就意味着savePlayer方法需要一种方式从Player类中获取它所需的每个数据,这最终会公开该类的整个私有实现。

因为面向对象编程是当今最常用的设计方法(我想?),所以在这些问题上肯定有些我没有理解到的东西。我已经与我那些轻度开发的同事讨论过这个问题,他们也遇到了与面向对象编程相同的问题。也许只是我们结构化编程背景使我们无法理解OOP的全部好处,将其视为提供方法以设置和获取私有数据,以便从一个地方更改和检索数据。

提前感谢并希望我听起来不太像个白痴。需要了解这个设计所涉及的语言的人,服务器端是Java,客户端是ActionScript 3。


StackOverflow不太适合回答这种类型的问题。正如您所观察到的,面向对象编程已经取得了很大的成功,因此我建议通过阅读一些书籍和编写一些代码来解决您的问题。 - anon
我已经阅读了几本书,包括《代码大全》、《代码整洁之道》、《编程实践》等等,还有一些我记不清的。如果其中任何一本直接涉及到这个话题,请告诉我。我已经多次查阅它们,但没有看到任何实质性的内容。 - suinswofi
我已经读过CoComp和TPOP(非常好) - Clean Code我不会碰。但它们都不是真正关于面向对象编程的。现在很少能找到一个不理解面向对象编程优势的人了 - 我自己的代码一直都是用面向对象的风格编写的(我已经这样做了30年了),所以我可能不是寻求书籍推荐的合适人选,但我一直喜欢Grady Booch的东西。 - anon
我也读过Java面向对象编程,所以我理解所有与OO实践相关的术语,甚至阅读了《设计模式入门》。我都明白,但它们都没有解决我认为是OOP核心问题的问题。 - suinswofi
7个回答

4
我建议您不要害怕玩家将成为获取器和设置器的类。那么对象到底是什么?它是属性和行为的编译。实际上,您的类越简单,您在开发过程中获得的面向对象编程的好处就越多。
我会将您的任务/功能分解为以下类:
Player:
- 具有hitpoints属性 - 具有位置属性 - 可以走到(position),触发“walk”事件 - 可以治愈(hitpoints) - 可以受伤(hitpoints),触发“isHurt”事件 - 可以检查是否仍然存活,例如isAlive()方法
Fighter扩展了Player(需要时应该能够将Player转换为Fighter):
- 具有力量和其他战斗参数来计算伤害 - 可以攻击()触发“attack”事件
World跟踪所有玩家:
- 监听“walk”事件(并防止非法移动) - 监听“isHurt”事件(并检查他们是否仍然存活)
Battle处理两个战斗机之间的战斗:
- 构造函数带有两个战斗机作为参数(您只想在真正相互战斗的玩家之间构造战斗) - 听取来自两个玩家的“attack”事件,计算伤害,并执行被攻击玩家的takeDamage方法
PlayerPersister扩展了AbstractPersister:
- 将玩家状态保存在数据库中 - 从数据库中恢复玩家状态
当然,您的游戏分解将会更加复杂,但我希望这可以帮助您开始以“更面向对象”的方式思考问题 :)

是的,这很类似于我现在正在做的事情。我的 Player 类只是一堆 get/set,然后我在 Game 类中有另一个方法 warpPlayer(player, newposition)。但这并不像应该是最好的解决方案。我无法解释。 - suinswofi
不,以我的例子为例,World类根本无法移动Player。Player通过walk()方法自己移动(你是从UI调用walk()方法而不是从World)。而且World只能通过侦听Player的“行走”事件来防止玩家行走到非法位置(这只是一个例子,你可以实现自己的逻辑,让玩家的行走与世界互动)。 - Anton N

1
不要过于担心Player类是一堆setter和getter。 Player类是一个模型类,而模型类往往是这样的。重要的是您的模型类要小而干净,因为它们将在整个程序中被重复使用。
我认为你应该使用你提出的warpPlayer(player, position)方法。它使Player类保持干净。如果您不想将玩家传递到函数中,也许您可以有一个PlayerController类,其中包含一个Player对象和一个warp(Position p)方法。这样,您可以将事件发布添加到控制器中,并将其排除在模型之外。
至于保存玩家,我会通过使Player实现某种序列化接口来实现。玩家类负责对自身进行序列化和反序列化,而其他一些类则负责将序列化数据写入/从文件中读取。

例如,可能有一个名为serialize的方法,它返回一个键值对,其中包含类的状态,然后可以使用另一个方法或构造函数接受该键值对来恢复。我担心在整个程序中到处都是这些序列化内容,但没有考虑只将序列化部分放在其中,然后在其他地方完成实际的文件/数据库输出。谢谢你的提示。 - suinswofi

0
我会考虑创建一个游戏对象来跟踪玩家对象。这样你就可以像这样做:game.WarpPlayerTo(WarpLocations.Forest); 如果有多个玩家,可能需要传递一个玩家对象或GUID。我认为你仍然可以保持面向对象的思路,而且一个游戏对象可以解决大部分问题。

它仍然是面向对象的,但至少在我看来,玩家类最终只会成为一系列巨大的getter和setter。想象一下攻击一个玩家。我必须有一个像player.getVitals().setHP(int)或player.getVitals().damageHP(int)这样的方法,并且仍然需要另一个次要方法来根据攻击者对受害者造成的伤害进行实际计算。这最终使Player仍然是一系列getter/setter。或者可能这样做:player.attack(attackerPlayer),其中所有计算都在内部完成。在这种情况下,无法通过网络发送更新。 - suinswofi

0
你所描述的问题不仅仅属于游戏设计,而是普遍存在于软件架构中。常见的方法是使用“依赖注入(DI)”和“控制反转(IoC)”机制。简而言之,你想要实现的是能够从对象中访问本地“服务”,以便例如传播某些事件(如warp)、记录日志等。
控制反转简而言之就是,你告诉某个服务为你创建对象,而不是直接创建它们,该服务再使用依赖注入来通知对象它们所依赖的服务。

在这种情况下,无论是服务还是未实例化这些对象(例如:Player),Player对象仍然需要引用服务(假设它不是静态的),以便可以调用那些函数。那么这是否会将Player对象紧密耦合到服务对象上呢?在这种情况下,例如warp()方法将与service.sendWarpEvent()方法紧密耦合。这是否是可接受的面向对象编程实践呢? - suinswofi
通过让您的对象使用服务接口,并独立地实例化实际的服务对象,您可以实现解耦。因为您可以随时替换实际的服务实现。当然,接口需要保持不变,但耦合是根据实现而不是接口进行衡量的。 - Aviad P.

0
如果您正在为多人游戏在不同的电脑之间共享数据,那么程序的核心功能就是在这些电脑之间保持和同步状态。如果您将这些值散布在许多不同的类中,将很难进行同步。
在这种情况下,我建议您设计需要在所有客户端之间同步的数据,并将其存储在一个单独的类中(例如GameState)。该对象将处理不同PC之间的所有同步,以及允许本地代码请求更改数据。然后,它将从自己的状态“驱动”游戏对象(Player、EnemyTank等)。[编辑:原因是尽可能将此状态保持小并在客户端之间高效传输将是您设计的关键部分。通过将其全部放在一个地方,这样做会变得更加容易,并鼓励您仅将绝对必要的内容放入该类中,以使通信不会因不必要的数据而变得臃肿]

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

另一种方法是相机每帧简单地读取玩家的位置以更新自身 - 但这不如使用事件松耦合和灵活。


在您的例子中,将所有与游戏相关的数据存储在单个类GameState中,我仍然需要在该类中定义函数来执行我需要对Player等对象进行的所有处理。这仍然会创建重复函数并导致我的所有类最终成为数据结构。根据我从您的帖子中获得的信息,您正在定义一个API函数,从而从该类中操作所有数据,是吗? - suinswofi
是的。您仍然可以将数据存储在各个对象中,但每当您想移动玩家时,您都会要求GameState进行更新,而不是直接询问玩家。您还可以通过使Player方法为私有,但是GameState的友元来强制执行此使用方式,因此它是唯一可以更改它们的类。 - Jason Williams

0
有时候,理解什么是对象以及对象的功能是掌握面向对象编程(OOP)的关键。我们通常可以将Player、Monster、Item等概念上看作系统中的“对象”,然后需要创建Environment、Transporter等对象来将这些对象链接在一起。这取决于概念如何协同工作以及我们需要完成什么任务,这种方式可能会失控。
我曾经与一些非常优秀的工程师合作过,他们能够将系统视为对象的集合。有时候,在一个系统中,它们可能是业务对象(如item、invoice等),有时候它们可能是封装处理逻辑的对象(如DyeInjectionProcessor、PersistanceManager),这些对象跨越了系统中的多个操作和“对象”。在这两种情况下,这些隐喻都适用于特定的系统,并使整个过程更容易实现、描述和维护。
OOP的真正威力在于使大型复杂系统更易于表达和管理。这些是要重点考虑的OOP原则,而不是担心是否符合严格的对象层次结构。

我没有从事过游戏设计,所以也许这些建议不太适用。但在我所从事和开发的系统中,将面向对象编程(OOP)视为简化和封装而非一个真实世界对象对应一个OOP类,是一种非常有益的改变。


我会将此与《代码大全》中的“上帝类”相关联。这基本上是我在原始帖子中提出后者解决方案的情况,需要重复方法定义。其中,玩家的warp()函数只是整个过程的一部分,而上帝类中的辅助方法调用了玩家的warp方法,以及其他方法来执行诸如向其他玩家发送warp更新并可能向玩家输出“您已被传送”等消息。这仍然使Player基本上成为一个getters和setters类。 :/ - suinswofi
实际上,玩家只是一个“游戏对象”,没有它是玩家、岩石还是怪物的概念,它只是一个游戏对象。如果你的设计意图是这样的,那么“背包”类并没有什么实际问题。这些与上帝类不同,因为它们本身与更高级别的控制类没有关系,而是与更低级别的管道类有关。 - GrayWizardx
拥有像GameObject、GamePlayer、GameNPC等继承结构仍然与此问题无关。我在我的2D图形引擎中使用这样的继承结构,其中面向对象的实践一直运作得很好。但当我有“某些事情”需要在多个抽象层面上产生连锁反应时,就会出现问题。 - suinswofi

0
我想进一步扩展GrayWizardx的最后一段话,说并非所有对象都需要具有相同的复杂性。你的设计可能非常适合拥有一些简单的集合get/set属性的对象。另一方面,重要的是要记住,对象可以代表任务或任务的集合,而不是真实世界的实体。
例如,玩家对象可能不负责移动玩家,而是表示其位置和当前状态。PlayerMovement对象可能包含更改玩家在屏幕上或游戏世界中位置的逻辑。
在我开始简单地重复已经说过的内容之前,我将指向面向对象设计(Aviad P.已经提到了其中两个)的SOLID原则。它们可能为创建良好的游戏对象模型提供一些高级指南。

感谢您的评论。我认为您所说的基本上正是我的问题所在。在某些情况下,似乎要在封装和耦合之间权衡取舍。在我上面的示例中,拥有一个轻度耦合的Player对象会打破封装性,因为它变成了一系列的getter和setter。或者选择一个良好封装的Player对象,该对象引用其他服务对象或静态引用全局方法来执行其他需要完成的任务,这将使该类与其服务对象/静态引用的全局方法紧密耦合。 - suinswofi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接