在教程的第一部分中,我们完成了基本的初始化工作,建立了游戏的骨架,并在屏幕上显示了一个孤零零的太空船,但是依然与实体系统没什么关系。好吧,其实我们创建了一个关于ES的类,只是没有实际用上它。我们在这篇教程中会用到它的,我保证。
我们需要入侵者
一个“太空入侵者”游戏没有“入侵者”,是称不上“太空入侵者”游戏的。所以请启动你的Blender,做个“入侵者”模型吧。当然,如果你不想浪费时间,我在google drive为你分享了一个我做的模型,将其命名为"BasicInvader"。
译注:无法访问google drive的同学,我已经分享到了百度网盘。
现在,朋友,我们要使用实体系统来保存一些“入侵者”和一艘太空船的基本数据了。我们需要哪些数据才能让它们在屏幕上正确显示出来呢?我们需要知道它们的位置(Position)和模型(Model)。
OK,一般来说,这时候你可能会根据传统面向对象编程(OOP)的思维来设计游戏,并采用继承、多态等特性来设计你的游戏系统,创建诸如SpaceShip、Invader之类的游戏对象。也许你还会使用JME3的Spatial.setUserData()来保存一些逻辑数据,并用Control来控制一部分游戏逻辑,结果是得到一个乱的像意大利面一样的程序架构。至少对我来说,这是经常发生的。
因此,把程序的逻辑和显示分层,是个非常不错的想法。使用实体系统时,这是自然而言的事情。在实体系统中,通过给太空船添加任何我们能够想到的组件,就可以在不修改原有代码的基础上,扩展太空船的功能。甚至,(实体的)数据和方法也是分离的,这与面向对象编程(OOP)思想完全对立(在OOP中,对象的数据和方法是一体的)。如果你有很深的OOP思想,那么本文描述的开发模式可能会让你十分困惑。但是请对我耐心点,也对自己耐心点。
我在哪
我们的太空船,以及入侵者,需要有自己的位置(Position),所以下面我们来定义一个Position组件。
package mygame;
import com.jme3.math.Vector3f;
import com.simsilica.es.EntityComponent;
public class Position implements EntityComponent {
private final Vector3f location;
public Position(Vector3f location) {
this.location = location;
}
public Vector3f getLocation() {
return location;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + location + "]";
}
}
注意,这个组件的值是不可改变的(final,且没有setLocation()方法),而且只有纯数据(没有逻辑方法)。这种设计有助于在多线程模式下运行,不必顾虑数据同步的问题。我们晚点会涉及到多线程。
我是谁
我们还需要知道每个实体应加载哪个模型,因此要定义一个Model组件。它看起来和上面的Position组件很像,也只有纯数据。
package mygame;
import com.simsilica.es.EntityComponent;
public class Model implements EntityComponent {
private final String name;
public final static String SpaceShip = "SpaceShip";
public final static String BasicInvader = "BasicInvader";
public Model(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Model[" + name + "]";
}
}
为了避免打错字,我定义了两个常量字符串,分别存储太空船和入侵者模型的名称。那么,这玩意该怎么用呢?
生成实体
现在,我们要使用实体系统(EntityData对象)来保存“入侵者”和“太空船”的数据,暂时还不显示出来。“实体”在这个框架中只有ID,别的什么都没有。你得把“组件”设置给“实体”,这个“实体”才有意义。然后你需要查询具有特定“组件”的实体集合,比如本例中定义的Position和Model。它(EntityData)就像是个数据库。
下面我们需要一个GameAppState类,用来建造游戏关卡。(这个类将控制游戏的主要过程,)从“开局”到“游戏”,从“游戏”到“死亡”,从“死亡”再到“重新开局”。但是,我们现在先不做地那么复杂,一点点丰满这个游戏比较好。
package mygame;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.math.Vector3f;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;
public class GameAppState extends AbstractAppState {
private EntityData ed;
private SimpleApplication app;
@Override
public void initialize(AppStateManager stateManager, Application app) {
this.app = (SimpleApplication) app;
this.app.setPauseOnLostFocus(true);
this.app.setDisplayStatView(true);
this.ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
// 创建太空船实体
EntityId ship = ed.createEntity();
this.ed.setComponents(ship,
new Position(new Vector3f(0, -20, 0)),
new Model(Model.SpaceShip));
// 创建入侵者实体
for (int x = -20; x < 20; x += 4) {
for (int y = 0; y < 20; y += 4) {
EntityId invader = ed.createEntity();
this.ed.setComponents(invader,
new Position(new Vector3f(x, y, 0)),
new Model(Model.BasicInvader));
}
}
}
@Override
public void cleanup() {
}
@Override
public void update(float tpf) {
}
}
在上面的代码中,我们只是创建了一些“太空船”和“入侵者”实体,这就够了。至少足够为你展示实体系统工作时的样子。
显示实体
重点来了。下面我们要显示实体系统生成的“入侵者”,哦还有“太空船”。把你当前的 VisualAppState ,改成下面的代码。
package mygame;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.light.DirectionalLight;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.simsilica.es.Entity;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;
import com.simsilica.es.EntitySet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class VisualAppState extends AbstractAppState {
private SimpleApplication app;
private EntityData ed;
private EntitySet entities;
private final Map<EntityId, Spatial> models;
private ModelFactory modelFactory;
public VisualAppState() {
this.models = new HashMap<EntityId, Spatial>();
}
@Override
public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app);
this.app = (SimpleApplication) app;
// 筛选用于显示的实体
ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
entities = ed.getEntities(Position.class, Model.class);
// 初始化摄像机,从Z轴正上方往下看。
app.getCamera().lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
app.getCamera().setLocation(new Vector3f(0, 0, 60));
// 添加定向光源
DirectionalLight light = new DirectionalLight();
light.setDirection(new Vector3f(1, 1, -1));
this.app.getRootNode().addLight(light);
// 加载太空船模型
modelFactory = new ModelFactory(this.app.getAssetManager());
}
@Override
public void cleanup() {
entities.release();
entities = null;
}
@Override
public void update(float tpf) {
if (entities.applyChanges()) {
removeModels(entities.getRemovedEntities());
addModels(entities.getAddedEntities());
updateModels(entities.getChangedEntities());
}
}
private void removeModels(Set<Entity> entities) {
for (Entity e : entities) {
Spatial s = models.remove(e.getId());
s.removeFromParent();
}
}
private void addModels(Set<Entity> entities) {
for (Entity e : entities) {
Spatial s = createVisual(e);
models.put(e.getId(), s);
updateModelSpatial(e, s);
this.app.getRootNode().attachChild(s);
}
}
private void updateModels(Set<Entity> entities) {
for (Entity e : entities) {
Spatial s = models.get(e.getId());
updateModelSpatial(e, s);
}
}
private void updateModelSpatial(Entity e, Spatial s) {
Position p = e.get(Position.class);
s.setLocalTranslation(p.getLocation());
}
private Spatial createVisual(Entity e) {
Model model = e.get(Model.class);
return modelFactory.create(model.getName());
}
}
上面的代码中使用了实体系统编程的一种设计模式,你几乎会在所有的游戏中使用这种模式,至少在客户端如此。
详细分析
这段代码具体做了什么呢?首先,我们筛选了需要显示的实体集合。
ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
entities = ed.getEntities(Position.class, Model.class);
在VisualAppState中,我们只关心那些拥有Position和Model的实体。这段代码会把拥有这2个组件的“太空船”和“入侵者”实体筛选出来。
然后是主循环,每个游戏的核心。JME3为每个AppState提供了update方法作为主循环。
public void update(float tpf) {
if (entities.applyChanges()) {
removeModels(entities.getRemovedEntities());
addModels(entities.getAddedEntities());
updateModels(entities.getChangedEntities());
}
}
在主循环中,我们检查筛选出来的实体集合是否更新,并处理那些消亡的、新生的以及产生了变化的实体。所有实体的模型及其对应的ID,都保存在一个“本地”HashMap对象中。
private final Map<EntityId, Spatial> models;
通过这种方式,我们就可以管理场景中的实体模型了。下面我们先直接看看 removeModels()
方法是怎么工作的。
private void removeModels(Set<Entity> entities) {
for (Entity e : entities) {
Spatial s = models.remove(e.getId());
s.removeFromParent();
}
}
实体的模型被保存在HashMap对象中,根据实体ID查询出对应的Spatial对象,然后把它从可视节点(visual node)中删除掉。这就搞定了。
好吧,当然,在删除之前得先添加点东西到场景中。添加实体模型由 addModels()
方法负责处理。
private void addModels(Set<Entity> entities) {
for (Entity e : entities) {
Spatial s = createVisual(e);
models.put(e.getId(), s);
updateModelSpatial(e, s);
this.app.getRootNode().attachChild(s);
}
}
在上面的代码中,我们把所有创建好的Spatial对象保存在HashMap中。同时,还把它们添加到场景图的根节点(root node)显示出来。
updateModel()
方法相当简单。通过实体ID,就能在HashMap中找到对应的Spatial,然后根据实体的Position组件来更新它的位置。
最后,你得在Main类中注册新写的AppState,代码如下。
public Main() {
super(new VisualAppState(),
new GameAppState(),
new EntityDataState());
}
运行程序,就能看到下面的结果。
就是这样,你的第一个基于实体系统驱动的软件模块。