我正在编写一个游戏,鼠标驱动的控制器对象点击一个玩家对象让它做某事。
有两种方法可以启动鼠标和播放器之间的交互:
我的困境在于,第一种选择似乎更直观,我想象现实世界中发生的情景,但第二种选择似乎更直观,适当的面向对象设计,因为它不需要查看另一个对象的属性,在某种程度上违反了封装(控制器必须调查播放器以读取其“可点击”属性)。此外,第二个选项似乎与“控制器”设计模式一致。
这对我来说总是很困难 - 我是否违反了适当的面向对象设计(例如选项1),还是使用了与现实世界相似的实现(例如选项2)?
我希望我有一些中间立场。
答案 0 :(得分:8)
这对我来说总是很困难 - 我是否违反了适当的面向对象设计(例如选项1),还是使用了与现实世界相似的实现(例如选项2)?
我不认为面向对象的目的是模拟现实世界。
我认为OO模型经常遵循现实世界模型的原因是现实世界没有太大变化:因此选择现实世界作为模型意味着软件不会有太大变化,即维护成本低廉。
对现实世界的忠诚本身并不是设计目标:相反,您应该尝试最大化其他指标的设计,例如:简单。
“面向对象”中的“对象”是软件对象,不一定是真实世界的对象。
答案 1 :(得分:5)
为什么不选择选项3,这与选项1类似?
答案 2 :(得分:4)
MVC概念通常是一种很好的做法,并且仍然应该在游戏设计中用作一般原则。但是,通过游戏UI的交互性,您还应该研究基于事件的架构。如果仔细设计,MVC与基于事件的体系结构不矛盾。 (以PureMVC为例。)
我建议你在所有显示对象上使用可观察的模式,这样他们就可以全部监听/触发事件。这将为您以后节省很多麻烦。当您的代码库变得更复杂时,您最终将需要使用更多的解耦技术,例如您的选项2描述。同样mediator pattern也会有所帮助。
编辑:
Mediator Pattern通常是组织应用程序级事件的好方法。
这是一篇关于在游戏编程中使用MVC,事件和媒介的博客:
答案 3 :(得分:4)
第二种方法是明确更加惯用的Flash处理方式。 AS3将事件模型构建到EventDispatcher
并且所有DisplayObjects
都继承自它。这意味着任何Bitmap
,Sprite
或MovieClip
都会立即知道是否点击了它。
将Flash Player视为您的控制器。当我在Flash中执行MVC时,我几乎从不编写控制器,因为Flash Player会为您执行此操作。您正在浪费周期来确定Flash Player已经知道的点击内容。
var s:Sprite = new Sprite();
s.addEventListener(MouseEvent.CLICK, handleMouseClick);
function handleMouseClick(event:MouseEvent):void
{
// do what you want when s is clicked
}
我可能不会直接从精灵内部访问控制器(可能在视图中)。相反,我会派遣一个事件(可能是一个与环境相匹配的特定自定义事件)。决定每帧发生一次的次数。响应用户交互(例如鼠标点击)通常可以让您自由地不用担心事件系统中的开销。
最后 - 我认为这与OOD或OOP的某些Ivory Tower概念无关。这些原则可以帮助您不限制您。当涉及到语用学问题时,请使用最简单的解决方案,这不会让您头疼。有时这意味着做OOP,有时它意味着功能,有时它意味着必要。
答案 4 :(得分:2)
根据Applying UML and Patterns(Craig Larman),用户界面(您的鼠标事件)不应该与您的应用程序类交互,也就是说,用户界面永远不应该直接驱动业务逻辑。
相反,应该定义一个或多个控制器作为用户界面的中间层,因此选项1确实遵循良好的面向对象方法。
如果您考虑一下,为了使业务逻辑尽可能独立于UI,将尽可能少的类耦合到UI是有意义的。
答案 5 :(得分:2)
这对我来说总是很困难 - 我是否违反了适当的面向对象设计(例如选项1),还是使用了与现实世界相似的实现(例如选项2)?
现实可能是塑造或改进设计的良好起点,但将OO设计建模为现实总是错误的。
OO设计是关于接口,实现它们的对象,以及这些对象之间的交互(它们在它们之间传递的消息)。接口是两个组件,模块或软件子系统之间的合同协议。 OO设计有许多特性,但对我来说最重要的品质是替换。如果我有一个接口,那么实现代码会更好地遵循它。但更重要的是,如果实施交换,那么新的实施更好地遵守它。最后,如果实现是多态的,那么多态实现的各种策略和状态会更好地遵循它。
示例1
在数学中,正方形是矩形。从类Rectangle继承类Square是个好主意。你这样做会导致毁灭。为什么?因为违反了客户的期望或信念。宽度和高度可以有所不同,但Square违反了该合同。我有一个维度矩形(10,10),我将宽度设置为20.现在我认为我有一个维度矩形(20,10),但实际的实例是一个带有维度的方形实例(20,20)和我,客户,我正在为真正的大惊喜。所以现在我们违反了最少惊喜原则。
现在你有了错误行为,导致客户端代码变得复杂,好像需要使用语句来解决错误行为。你也可以找到你的客户端代码,要求RTTI通过测试conrete类型来解决bug的行为(我有一个对Rectange的引用,但我必须检查它是否真的是一个Square实例)。
示例2
在现实生活中,动物可以是食肉动物或食草动物。在现实生活中,肉类和蔬菜是食物类型。因此,您可能认为将Animal类作为不同动物类型的父类是一个好主意。您还认为为类Meat和类Vegetable创建一个FoodType父类是个好主意。最后,你有一个类动物运动叫做 eat(),,它接受一个FoodType作为正式参数。
Everything编译,传递静态分析和链接。你运行你的程序。当一个子类型的动物(比如草食动物)收到一个作为Meat类实例的FoodType时,在运行时会发生什么?欢迎来到协变和逆反的世界。这是许多编程语言的问题。对于语言设计师来说,这也是一个有趣且具有挑战性的问题。
结论......
那你做什么?您可以从问题域,用户故事,用例和要求开始。让他们开车设计。让他们帮助您发现在类和接口中建模所需的实体。当你这样做时,你会发现最终结果不是基于现实。
由Martin Fowler查看Analysis Patterns。在那里你会看到是什么推动了他的面向对象设计。它主要基于他的客户(医疗人员,财务人员等)如何执行日常任务。它与现实重叠,但它不是以现实为基础或由现实驱动的。
答案 6 :(得分:1)
这往往是首选。逻辑游戏设计经常与良好的OOP设计不一致。我倾向于在游戏世界的范围内有意义,但这个问题绝对没有正确答案,每个问题都应该根据自己的优点进行。
这有点类似于争论骆驼套管的利弊。
答案 7 :(得分:1)
我认为,从应用程序逻辑中分离输入逻辑非常重要...一种方法是将输入事件(无论是用户输入,还是通过套接字/本地连接等到达的某些数据)转换为应用程序事件(事件中的事件)这个词的抽象意义)...这个转换是由我称之为“前控制器”的,因为缺乏一个更好的术语......
所有这些前端控制器只是转换事件,因此完全独立于应用程序逻辑如何响应特定事件...另一方面,应用程序逻辑与这些前端控制器分离......“协议”是一组预定义的事件...当涉及通知机制时,由您决定是否使用AS3事件调度从前端控制器获取事件到应用程序控制器,或者是否针对某些指定的接口构建它们,会打电话给...
人们倾向于将应用程序逻辑写入按钮的点击处理程序......有时甚至可以手动触发这些处理程序,因为他们不想整理东西......我还没有看到......
所以是的,它肯定是版本1 ...在这种情况下,鼠标输入的前端控制器只需要知道显示列表,并且有关于何时发送什么事件的逻辑......并且应用控制器需要是在这种情况下能够处理某种PlayerEvent.SELECT
事件...(如果你以后决定,有某种教程模式,或者其他什么,你可以简单地移动假鼠标并在案件中发送此事件假的点击,或者你可以简单地重复某些重播的东西,或者你可以用它来记录宏,当它不是关于游戏时...只是指出这种分离有用的一些场景)
希望这有助于......;)
答案 8 :(得分:1)
我不得不同意你对这两种选择的评估。选项2是更糟糕的OO,因为它将Player对象紧密耦合到特定的用户界面实现。当你想在屏幕外或使用无鼠标界面的地方重新使用你的Player类时会发生什么?
但仍然可以改进选项1。早期使用iClickable接口或Clickable超类的建议是一个巨大的改进,因为它允许你实现多种类型的可点击对象(不仅仅是播放器),而不必给控制器一个巨大的列表“这个对象是这个类吗?是那班吗?“经历。
您对选项1的主要反对意见似乎是它检查了玩家的“可点击”属性,您认为该属性违反了封装。它没有。它检查一个属性,该属性被定义为Player的公共接口的一部分。这与从公共接口调用方法没什么不同。
是的,我意识到,此时,“可点击”属性恰好被实现,这是一个简单的getter,除了查询Player的内部状态之外什么也没做,但它没有。明天可以重新定义属性,以完全不同的方式确定其返回值,而不引用内部状态,只要它仍然返回一个布尔值(即公共接口保持不变),使用Player.clickable的代码将仍然工作得很好。这是财产与内部国家直接检查之间的区别 - 这是一个巨大的巨大差异。
如果这仍然让你感到不安,那么很容易消除让控制器检查Player.clickable:只需将click事件发送到鼠标下的每个对象,该对象实现iClickable /从Clickable下降。如果对象在收到点击时处于不可点击状态,则可以忽略它。
答案 9 :(得分:1)
在任何OOP语言中,当有可能遵循现实生活模拟方法时,遵循惯用方法几乎总是正确的做法。所以要回答你的问题,第二种方法几乎肯定是更好的,或者可以证明是更好的,因为你深入研究设计或后来感觉需要改变或添加它。
这不应该阻止你找到其他解决方案。但要尽量保持语言习语。 OOP不会以1比1的关系转化为现实生活,也不会特别擅长模仿它。一个说明性的例子是您可能已经知道的经典矩形和方形对象关系。在现实生活中,正方形是一个矩形。在OOP中(或至少在适当的OOP中),这种关系不能很好地转化为基础派生关系。所以你觉得有必要摆脱现实生活中的模仿,因为语言成语更高。当你开始认真地实现矩形和方形,或者后来希望对它们进行更改时,它或者是一个痛苦的世界。
答案 10 :(得分:1)
另一种方法是创建IClickHandler接口。注册点击的所有对象都是通过将IClickHandler交给控制器来实现的。单击对象时,控制器会调用已注册的IClickHandler上的clicked()方法。 IClickHandler可以转发或不转发对其注册的方法的调用。现在,你的控制器和你的对象都没有决定是否真正点击了所述对象。
也可以根据其他标准选择IClickHandler(IOW,对象在注册时不选择IClickHandler本身,其他一些算法选择它)。一个很好的例子是所有NPC对象都有一个转发点击的IClickHandler,而所有树都有一个不转发点击的IClickHandler。
至少你可以有3个实现界面的处理程序:AlwaysClickable,NeverClickable,ToggledClickable
请记住,上面的内容确实更具活动性,而且性能也很小,但它为您提供了很大的灵活性(由您决定灵活性是否值得增加复杂性)。
另请注意,最好不要简单地遵循任何类型的原则。在编写代码时,最好根据情况做什么。如果你正在写一个俄罗斯方块克隆,那么选项1“违反”OOP原则的事实是完全无关紧要的,你永远不会意识到严格的OOP对一个简单项目的好处。