本章内容源自于:Visualization
第一章中我们建立一个大致的游戏框架并在屏幕上显示了一艘飞船,但还没有使用实体系统。尽管我们写好了实体系统,但是还没有真正用到。这一章我们将真正地使用实体系统,我发誓。
敌人
没有入侵者的入侵者游戏的确不是一款入侵者游戏,因此你需要再次打开你的Blender给敌人制作一个模型。当然你也可以从google drive(译者:第一章中的网盘已经包含了所有资源,请国内读者找找看)获取我已经做好的模型。我把它称为:BasicInvader。(译者:我们目前做的这款游戏叫《太空入侵者》,玩法类似于红白机上的《小蜜蜂》。我们将操作“星际战舰”消灭这些“入侵者”)
现在我们使用实体系统来存储大量的入侵者实体和一个星际战舰实体。我们需要做什么才能让太空战舰和入侵者们显示到屏幕上?需要位置和模型,然后根据位置把模型设置到屏幕上。通常情况下,此时你会创建各种类对象,这些对象继承入侵者类或太空战舰类等等。或许你还会滥用Spatial类的userData存储一些游戏逻辑,然后把事情搞得一团糟,像意大利面一样。至少在我身上就发生过这种事情。为此,把图像显示这部分从游戏逻辑中分离出去就是一个非常聪明的主意。实体系统把这种想法发挥得淋漓尽致。使用实体系统,我们可以在任何时候扩展飞船的任何功能而无需触碰已经写好的那部分代码。甚至属性和方法也都是分离的——这完全违反了面向对象编程。因此如果你是个十足的面向对象编程人员,接下来的代码可能会使你感到困惑。但请耐心看完教程、耐心理解。
我的位置坐标是什么?
我们的飞船和敌人都需要位置,位置组件则是可以为他们提供位置信息的类:
你可以看到,组件类中只有纯粹的数据,并且不可更改。这种机制可以帮助我们在多线程情况下不必考虑线程同步的问题。我们将在教程进阶篇——局域网开发中用到这一特性。
我长什么样子?
为了告诉系统应该给对象加载哪个模型,我们应该给对象添加模型组件。如同创建位置组件类那样,模型组件也是只有属性并且不可修改:
为避免敲错名字,我在模型组件类中添加了两个静态常量字符串:SpaceShip和BasickInvader。现在,我们得瞧瞧它们该怎么用。
创建实体
最后我们用实体系统存储太空战舰与入侵者的数据。实体说白了就是ID。你得把组件和实体绑定到一起,这样实体才有意义。然后你就可以通过组件追查到实体,例如通过位置组件和模型组件查询太空战舰和入侵者实体。就像是使用数据库那样。现在我们需要一个运行游戏的AppState,它可以在游戏开始时创建敌人,敌人被消灭完后又会重新刷一波新敌人等等。我们先写个超级简单的,以后再慢慢地丰富它的功能:
当前我们只是显示太空战舰和入侵者,这就够了。这足以说明实体系统是怎么运行的了。
显示它们
本章重点来了。第一次用实体系统显示入侵者和我们的太空战舰。请用下列代码替换我们之前写的VisualAppState:
上述代码使用了一种实体系统中的设计模式,你将会经常使用这种设计模式,至少在客户端中是这样。
细节讲解
所以我们在显示系统类中究竟做了些什么?首先我们定义了一个我们要操作的实体集合:
我们要操作所有具有位置和模型的实体。这段代码就是把同时所有具有位置和模型的实体找出来,也就是入侵者与星际战舰这两种实体。
接下来是更新循环,所有游戏的核心所在。JME会给每个AppState提供一个update()方法:
我们在更新循环中检查实体集合中是否有实体的实体组件发生了改变、有了新的同时拥有位置和模型的实体、原实体集合中有实体不再拥有位置组件或模型组件了。
所有的Spatial都通过成员变量HashMap与实体相关联:
这样我们稍后就能移除掉已经添加到视图结点上的实体模型了。让我看一下移除实体方法:
Spatial被储存在HashMap中。当有实体被移除时,我们可以通过HashMap找到对应的Spatial并把它从视图结点中移除掉。当然,移除实体的前提是添加实体。添加实体的方法在addModels()方法中:
你可以看到,我们把所有的Spatial都储存在HashMap中。并且为了能看到,我们把Spatial关联到了rootNode上。
updateModels()方法十分简单:就是通过id找到Spatial,然后更新Spatial的位置。
最后,修改主类的构造方法,使它看起来像是这样:
大功告成,你的第一个由实体系统驱动的软件。