第十三章:控制游戏逻辑
导读:《入门:游戏主循环》
【转载】入门:游戏主循环
引言
主循环是一款游戏或者框架的核心以及基础,它会让游戏以及动画看起来是在做实时的运行。几乎所有游戏(除了回合制等几种类型以外)都要基于主循环以及精确的时间控制。
下面就是一个最基本的主循环示例代码:
先定义一个简单的游戏引擎接口,声明游戏的基本生命周期。
package net.jmecn.logic;
/**
* 一个简单的游戏引擎接口
*
* @author yanmaoyuan
*
*/
public interface IGameEngine {
// 初始化
void init();
// 主循环
void update();
// 渲染
void render();
// 清理
void clear();
}
然后定义一个抽象的游戏Engine
,用于驱动游戏主循环。
package net.jmecn.logic;
/**
* 抽象游戏引擎,实现了游戏的主循环。
*
* @author yanmaoyuan
*
*/
public abstract class Engine implements IGameEngine {
protected boolean running = false;
protected boolean pause = false;
// 开始游戏
public void start() {
if (running)
return;
running = true;
pause = false;
// 启动主循环
loop();
}
// 暂停游戏
public void pause(boolean pause) {
this.pause = pause;
}
// 主循环
private void loop() {
init(); // 初始化整个体系,框架、图形、声音等等
while(running) {
// 暂停游戏
if (pause) {
return;
}
update();
render();
}
clear(); // 清理资源、关闭各种接口等
}
// 停止游戏
public void stop() {
running = false;
}
}
主循环每次执行的时候,都会调用指定好的函数来执行相应的工作,比如在上面代码中我们设置 update()
来处理游戏逻辑,设置 render()
来绘制游戏当前的画面。比如下面这个例子
package net.jmecn.logic;
/**
* 实现游戏逻辑
*
* @author yanmaoyuan
*
*/
public class MyGame extends Engine {
float x = 0;
@Override
public void init() {}
@Override
public void update() {
x += 1;
}
@Override
public void render() {
// TODO 根据x坐标绘制玩家
}
@Override
public void clear() {}
public static void main(String[] args) {
MyGame game = new MyGame();
game.start();
}
}
每一次游戏逻辑函数被触发的时候,都会将玩家角色的水平 x
位置加 1,这样,经过相应的 render()
方法处理,就会看到角色在横向运动。
但是,上面这个主循环有着明显的问题,那就是:主循环能够被执行的次数是取决于机器配置的,越快的机器,主循环执行的次数越多,那么角色也就运动得越快。
当然,如果我们知道运行游戏的硬件系统是一致的,比如说都运行在某种主机平台上,那么,还是可以直接使用这样的主循环的。
那么,既然这种主循环不是很合理,我们希望游戏在任何系统上都保持一致的速度,那就需要引入基于时间的主循环了。
基于时间的主循环
下面这个主循环例子基于度过的时间,那么,会在不同机器上表现达到一致:
首先,接口中的update()
方法变成了update(float deltatime)
package net.jmecn.logic;
/**
* 一个简单的游戏引擎接口
*
* @author yanmaoyuan
*
*/
public interface IGameEngine {
// 初始化
void init();
/**
* 主循环
* @param deltatime 间隔时间
*/
void update(float deltatime);
// 渲染
void render();
// 清理
void clear();
}
其次,主循环中计算了执行update(float deltatime)
方的的时间间隔(单位:秒):
// 主循环
private void loop() {
long currentTime = System.nanoTime();
long lastTime = 0;
long deltaTime = 0;
init(); // 初始化整个体系,框架、图形、声音等等
while(running) {
lastTime = currentTime;
currentTime = System.nanoTime();
deltaTime = currentTime - lastTime;
// 暂停游戏
if (pause) {
return;
}
update(deltaTime * 0.0000000001f);
render();
}
clear(); // 清理资源、关闭各种接口等
}
在实现类中,不再直接+1
,而是加上时间差:
@Override
public void update(float deltatime) {
x += deltatime;
}
我们通过 System.nanoTime()
取得程序运行的时间(单位:纳秒),每一次主循环执行的时候我们都取得时间差,然后通过时间差来决定更新距离。
我们用 lastTime
来记录上一次的时间,然后通过 System.nanoTime()
取得当前的时间,然后减去上一次的时间,就得到了 deltaTime
——时间差。
lastTime = currentTime;
currentTime = System.nanoTime();
deltaTime = currentTime - lastTime;
deltaTime的时间单位是纳秒,游戏中一般使用秒作为时间单位,因此需要对齐进行转换deltaTime * 0.0000000001f
。
基于时钟的主循环
有一些语言或者框架提供了按照时钟触发的方式,可以设定固定的触发时间间隔,比如下面的例子:
package net.jmecn.logic;
import java.util.Timer;
import java.util.TimerTask;
/**
* 抽象游戏引擎,实现了游戏的主循环。
*
* @author yanmaoyuan
*
*/
public abstract class Engine implements IGameEngine {
protected boolean pause = false;
Timer timer;
TimerTask task;
// 开始游戏
public void start() {
init(); // 初始化整个体系,框架、图形、声音等等
// 定时器
timer = new Timer();
task = new TimerTask() {
public void run() {
// 暂停游戏
if (pause) {
return;
}
update();
render();
}
};
// 设定 1/30 秒触发一次,执行 主循环
long period = (long) (1000 / 30f);
timer.scheduleAtFixedRate(task, 0, period);
}
// 暂停游戏
public void pause(boolean pause) {
this.pause = pause;
}
// 停止游戏
public void stop() {
task.cancel();// 退出任务
task = null;
timer.cancel();
timer = null;
clear(); // 清理资源、关闭各种接口等
}
}
游戏逻辑可以按固定速率执行:
@Override
public void update() {
x += 1;
}
可见,这种方式可以通过系统时钟来不断的触发主循环 update()
,可以精确的控制游戏的运行状态。它可以保证游戏在任何机器上都以同样的速度运行。现在很多主流的游戏框架都支持通过时钟来触发事件。
jME3的主循环
jME3使用的是基于时间的主循环,jme3-core.jar
中的com.jme3.system.NanoTimer
类就是是专门用来计算时间的。
主循环做了什么?
当你继承SimpleApplication类后,就自动获得了它提供的主循环,利用它我们可以实现自己的游戏逻辑,比如遥控NPC、处理游戏事件、响应用户输入等。
SimpleApplication在后台做了很多事情,我们曾在本教程第二章“jME3基本概念”的生命周期章节简单介绍过:
- 执行
initialize()
方法,初始化显示系统
、场景图
、摄像机
、输入系统
、音效系统
、资源管理系统
、应用状态机
等一大堆重要的内容。 - initialize() 方法只在程序启动时执行一次。
- 在 initialize() 方法的最后,会执行我们重载的
simpleInitApp()
方法。 - 由于 initialize() 方法已经把该准备的东西都准备好了,所以我们在 simpleInitApp() 方法中才可以直接使用assetManager、inputManager等重要对象。
- 循环执行
update(float tpf)
方法 - 1-响应用户输入,执行所有事件监听器中的代码。见第九章:用户交互
- 2-更新游戏状态
- 2-1 更新全局游戏状态(执行所有 AppState#update() 方法);
- 2-2 更新用户自定义游戏逻辑(执行 simpleUpdate() 方法);
- 2-3 更新场景图状态(执行所有Spatial中的 Control#update() 方法。)。
- 3-渲染音频和视频
- 3-1 渲染全局应用状态中的场景(执行所有 AppState#render() 方法);
- 3-2 渲染游戏主场景(执行 renderManager#render() 方法);
- 3-3 执行用户自定义渲染代码(执行 simpleRender() 方法)。
- 重复上述循环。
- 退出游戏。当 stop() 方法被调用后,执行所有 cleanup() 方法和 distory() 方法,然后关闭jME3窗口、终止主循环。
主循环的用法
在继承SimpleApplication类之后,我们的游戏逻辑主要通过重载 simpleUpdate(float tpf)
方法来实现,游戏主循环会自动调用它。
下面是一个例子:
package net.jmecn.logic;
import com.jme3.app.SimpleApplication;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
/**
* 主循环
*
* @author yanmaoyuan
*
*/
public class HelloLoop extends SimpleApplication {
// 旋转的物体
private Spatial spatial;
// 旋转速度:每秒180°
private float rotateSpeed = FastMath.PI;
@Override
public void simpleInitApp() {
cam.setLocation(new Vector3f(3.3435764f, 3.7595856f, 6.611723f));
cam.setRotation(new Quaternion(-0.05573249f, 0.9440857f, -0.23910178f, -0.22006002f));
// 创建一个方块
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
spatial = new Geometry("Box", new Box(1, 1, 1));
spatial.setMaterial(mat);
rootNode.attachChild(spatial);
// 添加光源
rootNode.addLight(new DirectionalLight(new Vector3f(-1, -2, -3)));
}
@Override
public void simpleUpdate(float tpf) {
// 绕Y轴以固定速率旋转
spatial.rotate(0, tpf * rotateSpeed, 0);
}
public static void main(String[] args) {
// 启动
HelloLoop app = new HelloLoop();
app.start();
}
}
这个程序的作用,是创建一个方块,并让它以每秒180°的速度绕Y轴旋转。
扩展主循环
一般来说,我们会在 simpleInitApp()
方法中初始化程序所需的资源,然后在 simpleUpdate(float tpf)
方法中编写代码逻辑。直到本章为止,教程中几乎所有的例子都是这样写的。
这种写法并不好。对于一个稍微复杂一点的游戏来说,这都将导致单一类中的代码超级长,难以阅读,而且难以维护。我们并不需要在游戏启动时加载所有资源,也不需要在主循环中执行所有逻辑代码。
最好的做法是将游戏模块化,把 simpleInitApp() 和 simpleUpdate(float tpf) 方法中的代码分解到不同的Java类中。jME3提供了两个接口(AppState和Control),利用它们可以把系统的功能模块分解成若干个子模块。
AppState
用于将全局游戏机制模块化。Control
用于将单个游戏对象的行为模块化。
诸如天气、光照、音效、物理等模块的代码,都可以搬到AppState中。如果游戏中有几个不同的地图,可以分解成多个子场景,并在不同的AppState中并分别初始化。当玩家进入某个地图就加载某个AppState,离开时再把它移除。
如果要控制单个游戏对象的行为,就可以用Control来做。如果程序设计得合理,就可以得到很多可复用的小组件。比如让一个物体旋转、让NPC寻找到达目的地的最佳路线、让摄像机始终跟随玩家、让一个物体始终正面朝向玩家。
下面我们分别介绍 AppState 和 Control 的用法。
AppState
介绍
AppState是jME3的一个重要接口,主要用于处理全局的游戏机制。合理应用AppState,可以让你的程序结构清晰、灵活,而且代码可复用性更高。当 AppState 和 Control 结合使用时,还可以实现更加复杂的功能。
应用场景
举几个在游戏开发中经常遇到的用例:
-
当用户处于不同的界面时,键盘和鼠标的输入需要有不同的处理。比如在开始界面、在创建角色界面、在游戏场景内...不同的场景下,输入的处理方式是不一样的。能否根据不同的场景,分别激活或者禁用不同的输入处理代码?
-
我的程序包含“开始菜单”、“游玩模式”、“角色编辑模式”、“展览模式”,我能否在这些场景之间自由切换?
-
我在 simpleUpdate() 方法中有一段超级复杂的逻辑代码(例如:人工智能),能不能把这段代码独立出去,单独做成一个功能模块?
这些都可以通过AppState完成。每个AppState都是应用程序的一个子集(或扩展),可以访问到jME3中的全局对象,诸如:
- AssetManager
- InputManager
- AppStateManager
- ViewPort
- Camera
- RootNode
- GuiNode
- 等等..
有了这些全局对象,AppState 就能做到原本你在 SimpleApplication 中做的任何事情。
生命周期
AppState的生命周期有三个主要阶段:初始化(initialize)、主循环(update)、清理(cleanup)。
AppStateManager
用于管理所有的 AppState 实例,通过 下面两行代码,可以把一个 AppState 实例添加到系统的处理队列中,或者移除一个 AppState对象。
stateManager.attach(AppState appState);// 添加AppState
stateManager.detach(AppState appState);// 移除AppState
AppState的生命历程是这样的:
- 初始化:当一个 AppState 实例被添加到 AppStateManager 之后,它的生命周期就开始了,随后 initialize() 方法将会执行一次。
initialze()
方法类似于simpleInitApp()
,用于初始化游戏资源、输入处理、GUI等内容。 - 主循环:每个 AppState 都有自己的
update(float tpf)
方法,类似于simpleUpdate(float tpf)
。AppState 的主循环会被 SimpleApplication 的主循环驱动运行。 - 激活:调用
setEnabled(true)
将激活 AppState 的主循环。我们应该在 AppState 激活的同时把子场景添加到主场景中,这样运行时才能看到子场景。 - 运行:当
isEnabled()
的返回值为true
时,AppState 的update(float tpf)
方法会循环运行。此时就可以更新游戏状态、修改场景图、处理用户输入了。 - 暂停:调用
setEnabled(false)
将暂停 AppState 的主循环。此时应该吧子场景从主场景中移除,直到 AppState 再次被激活。 - 清理:当一个 AppState 实例被从 AppStateManager 中移除后,它的生命周期即将走到终点。此时
cleanup()
方法将会执行一次,用于清理 AppState 中的各种资源,比如删除子场景,移除输入监听器,关闭GUI等等。
用法
对于一个已经定义好的 AppState,只需要把 AppState 对象交给 AppStateManager 即可。例如,jME3中集成了Bullet物理引擎,使用方法是这样的:
@Override
public void simpleInitApp() {
stateManager.attach(new BulletAppState());
...
}
自定义 AppState 通常遵循下列步骤:
- 创建一个AppState的实现类。
- 实现 AppState 中的抽象方法。
- 在initialize()方法中进行初始化,在update()方法中实现游戏逻辑,在cleanup()方法中进行清理工作。
- 你可以使用构造方法来给这个AppState传参。
- 分别定义当 isEnabled() 为 true 和 false 时的行为,比如开灯/关灯。
- 创建一个 AppState 对象,将其添加到AppStateManager中(
stateManager.attach(appState);
)。 - 在你需要的时候,激活/暂停 AppState 的运行状态(
appState.setEnabled(true);
)。 - 移除一个AppState对象(
stateManager.detach(appState);
),并进行清理。
关于AppState的具体使用,有这么几点需要注意的地方:
- 当你的程序中有多个 AppState 时,它们运行的顺序与添加到AppStateManager中的顺序一致。
- AppState 的设计应遵循高内聚、低耦合原则,两个 AppState 之间最好不要存在依赖关系。否则当你移除一个AppState时,当心导致其他AppState发生异常。
- 永远不要忘记清理 AppState 中的资源。
实例:子场景管理
上图中的场景我们已经看过很多次了,场景中是一个红色的方块,使用Lighting.j3md材质。为了让你能够看清它,场景中还加入了光源。
现在我要使用AppState来实现这个程序。
- VisualAppState 管理子场景,控制方块的显示和隐藏。
- LightAppState 管理光照,控制开灯、关灯功能。
- InputAppState 管理用户输入。
- 按下空格 键,调用 LightAppState 来开关灯;
- 按下Tab 键,调用 VisualAppState 来显示/隐藏场景。
HelloAppState
HelloAppState.java 是我们的主类,现在它的作用仅仅是启动程序,并把上述3个 AppState 交给 AppStateManager 管理。
package net.jmecn;
import com.jme3.app.SimpleApplication;
import net.jmecn.logic.InputAppState;
import net.jmecn.logic.LightAppState;
import net.jmecn.logic.VisualAppState;
/**
* 演示AppState的作用
*
* @author yanmaoyuan
*
*/
public class HelloAppState extends SimpleApplication {
public static void main(String[] args) {
HelloAppState app = new HelloAppState();
app.start();
}
@Override
public void simpleInitApp() {
stateManager.attach(new LightAppState());
stateManager.attach(new VisualAppState());
stateManager.attach(new InputAppState());
// 初始化摄像机
cam.setLocation(new Vector3f(2.4611378f, 2.8119917f, 9.150583f));
cam.setRotation(new Quaternion(-0.020502187f, 0.97873497f, -0.16252096f, -0.1234684f));
}
}
比起我们以前写的代码,是不是整洁多了?
除了在 simpleInitApp() 方法中初始化AppState以外,还有另一种更优雅的写法:调用父类构造方法,设置所需的 AppState。
package net.jmecn;
import com.jme3.app.DebugKeysAppState;
import com.jme3.app.FlyCamAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.audio.AudioListenerState;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import net.jmecn.logic.InputAppState;
import net.jmecn.logic.LightAppState;
import net.jmecn.logic.VisualAppState;
/**
* SimpleApplication的最佳形式
*
* @author yanmaoyuan
*
*/
public class HelloAppState2 extends SimpleApplication {
public static void main(String[] args) {
HelloAppState2 app = new HelloAppState2();
app.start();
}
/**
* 在构造方法中初始化AppState
*/
public HelloAppState2() {
super(new StatsAppState(), new FlyCamAppState(), new AudioListenerState(), new DebugKeysAppState(),
new LightAppState(), new VisualAppState(), new InputAppState());
}
@Override
public void simpleInitApp() {
// 初始化摄像机
cam.setLocation(new Vector3f(2.4611378f, 2.8119917f, 9.150583f));
cam.setRotation(new Quaternion(-0.020502187f, 0.97873497f, -0.16252096f, -0.1234684f));
}
}
这里比上面多了4个AppState,它们是jME3系统自带的AppState:
- StatsAppState 它的功能就是显示屏幕左下角的状态信息。
- FlyCamAppState 它的功能就是jME3自带的第一人称摄像机。
- AudioListenerState 它的作用是渲染3D音效。
- DebugKeysAppState 它的作用就是按 C 显示摄像机位置,按 M 显示内存使用情况。
VisualAppState
下面是 VisualAppState.java 的代码。initialize() 方法用于初始化场景,update(float tpf) 方法用于控制方块旋转。
注意,在 VisualAppState 中,我没有直接使用 SimpleApplication 中的 rootNode,而是又定义了一个 sceneNode。子场景中的所有物体都被添加到 sceneNode中,这样只需要一行代码就可以把整个子场景添加到 rootNode 中,同样也可以用一行代码就把整个子场景移除掉。
package net.jmecn.logic;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
/**
* 管理自场景的AppState
*
* @author yanmaoyuan
*
*/
public class VisualAppState implements AppState {
private boolean initialized = false;
private boolean enabled = true;
/**
* 创建一个独立的根节点,便于管理子场景。
*/
private Node sceneNode = new Node("MyScene");
private Geometry cube = null;
/**
* 对于那些我们用得上的系统对象,保存一份对象的引用。
*/
private SimpleApplication simpleApp;
private AssetManager assetManager;
@Override
public void stateAttached(AppStateManager stateManager) {}
@Override
public void initialize(AppStateManager stateManager, Application app) {
this.simpleApp = (SimpleApplication) app;
this.assetManager = app.getAssetManager();
// 创建一个方块
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
mat.setColor("Diffuse", ColorRGBA.Red);
mat.setColor("Ambient", ColorRGBA.Red);
mat.setColor("Specular", ColorRGBA.Black);
mat.setFloat("Shininess", 1);
mat.setBoolean("UseMaterialColors", true);
Mesh mesh = new Box(1, 1, 1);
cube = new Geometry("Cube", mesh);
cube.setMaterial(mat);
// 将方块添加到我们这个场景中。
sceneNode.attachChild(cube);
// 初始化完毕
initialized = true;
if (enabled)
simpleApp.getRootNode().attachChild(sceneNode);
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void setEnabled(boolean active) {
if ( this.enabled == active )
return;
this.enabled = active;
if (!initialized)
return;
if (enabled) {
simpleApp.getRootNode().attachChild(sceneNode);
} else {
sceneNode.removeFromParent();
}
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public void update(float tpf) {
cube.rotate(0, tpf * FastMath.PI, 0);
}
@Override
public void render(RenderManager rm) {}
@Override
public void postRender() {}
@Override
public void stateDetached(AppStateManager stateManager) {}
@Override
public void cleanup() {
if (enabled)
sceneNode.removeFromParent();
initialized = false;
}
}
LightAppState
下面是 LightAppState.java 的代码。仅仅是在 initialize() 中初始化了光源,没有什么特别的。
package net.jmecn.logic;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.light.AmbientLight;
import com.jme3.light.PointLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Node;
public class LightAppState implements AppState {
private boolean initialized = false;
private boolean enabled = true;
// 点光源
private Vector3f lightPos;
private ColorRGBA pointLightColor;
private PointLight pointLight;
// 环境光
private ColorRGBA ambientLightColor = new ColorRGBA(0.2f, 0.2f, 0.2f, 1f);
private AmbientLight ambientLight;
// 背景色
private ColorRGBA bgColor = new ColorRGBA(0.7f, 0.8f, 0.85f, 1f);
/**
* 灯光应该引用到整个场景中,所以需要保存SimpleApplication中的根节点。
*/
private Node rootNode;
@Override
public void initialize(AppStateManager stateManager, Application app) {
SimpleApplication simpleApp = (SimpleApplication) app;
this.rootNode = simpleApp.getRootNode();
// 创建光源
lightPos = new Vector3f(1, 2, 3);
pointLightColor = new ColorRGBA(0.8f, 0.8f, 0.0f, 1f);
pointLight = new PointLight(lightPos, pointLightColor);
ambientLightColor = new ColorRGBA(0.2f, 0.2f, 0.2f, 1f);
ambientLight = new AmbientLight(ambientLightColor);
// 设置背景色,在关灯之后你依然能看到场景中漆黑的物体。
app.getViewPort().setBackgroundColor(bgColor);
// 初始化完毕
initialized = true;
if (enabled)
turnOn();
}
// 开灯
public void turnOn() {
rootNode.addLight(pointLight);
rootNode.addLight(ambientLight);
}
// 关灯
public void turnOff() {
rootNode.removeLight(pointLight);
rootNode.removeLight(ambientLight);
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void setEnabled(boolean active) {
if ( this.enabled == active )
return;
this.enabled = active;
if (!initialized)
return;
if (enabled) {
turnOn();
} else {
turnOff();
}
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public void stateAttached(AppStateManager stateManager) {}
@Override
public void stateDetached(AppStateManager stateManager) {}
@Override
public void update(float tpf) {}
@Override
public void render(RenderManager rm) {}
@Override
public void postRender() {}
@Override
public void cleanup() {
if (enabled)
turnOff();
initialized = false;
}
}
InputAppState
下面是 InputAppState.java 的代码。
package net.jmecn.logic;
import com.jme3.app.Application;
import com.jme3.app.state.AppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.renderer.RenderManager;
/**
* 输入模块
* @author yanmoayuan
*
*/
public class InputAppState implements AppState, ActionListener {
// 电灯开关
public final static String SWITCH_LIGHT = "switch_light";
public final static Trigger TRIGGER_KEY_SPACE = new KeyTrigger(KeyInput.KEY_SPACE);
// 显示/隐藏子场景
public final static String TOGGLE_SUBSCENE = "toggle_subscene";
public final static Trigger TRIGGER_KEY_TAB = new KeyTrigger(KeyInput.KEY_TAB);
private boolean initialized = false;
private boolean enabled = true;
/**
* 保存我们所需要的系统对象
*/
private InputManager inputManager;
private AppStateManager stateManager;
@Override
public void initialize(AppStateManager stateManager, Application app) {
this.stateManager = stateManager;
this.inputManager = app.getInputManager();
initialized = true;
if (enabled)
addInputs();
}
/**
* 添加输入
*/
public void addInputs() {
inputManager.addMapping(SWITCH_LIGHT, TRIGGER_KEY_SPACE);
inputManager.addMapping(TOGGLE_SUBSCENE, TRIGGER_KEY_TAB);
inputManager.addListener(this, SWITCH_LIGHT, TOGGLE_SUBSCENE);
}
/**
* 移除输入
*/
public void removeInputs() {
inputManager.deleteTrigger(SWITCH_LIGHT, TRIGGER_KEY_SPACE);
inputManager.deleteTrigger(TOGGLE_SUBSCENE, TRIGGER_KEY_TAB);
inputManager.removeListener(this);
}
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if (isPressed) {
if (SWITCH_LIGHT.equals(name)) {
// 开关灯
LightAppState light = stateManager.getState(LightAppState.class);
if (light != null)
light.setEnabled(!light.isEnabled());
} else if (TOGGLE_SUBSCENE.equals(name)) {
// 显示/隐藏场景
VisualAppState visual = stateManager.getState(VisualAppState.class);
if (visual != null)
visual.setEnabled(!visual.isEnabled());
}
}
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void setEnabled(boolean active) {
if ( this.enabled == active )
return;
this.enabled = active;
if (!initialized)
return;
if (enabled) {
addInputs();
} else {
removeInputs();
}
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public void stateAttached(AppStateManager stateManager) {}
@Override
public void stateDetached(AppStateManager stateManager) {}
@Override
public void update(float tpf) {}
@Override
public void render(RenderManager rm) {}
@Override
public void postRender() {}
@Override
public void cleanup() {
if (enabled)
removeInputs();
initialized = false;
}
}
最佳实践
使用抽象类
你并不需要总是去直接实现AppState接口,这个接口中需要重载的方法太多,而且每次都去重新写一遍几乎相同的 setEnabled()
方法也太无聊了。
jME3提供了2个抽象类,它们各自有一些基本实现。如果你懒得把 AppState 接口中的方法都实现一遍,可以尝试继承下面2个类。
BaseAppState是最好用的一个,而AbstractAppState更加简单粗暴。我不解释为什么,你应该在实际使用后根据自己的理解来决定怎么做。
下面是我常用的一个AxisAppState,基于BaseAppState实现。它的作用是在场景的中央显示一个参考坐标系,按F4 可以显示/隐藏坐标系。
package net.jmecn.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.debug.Arrow;
import com.jme3.scene.debug.Grid;
/**
* 坐标系
*
* @author yanmaoyuan
*
*/
public class AxisAppState extends BaseAppState implements ActionListener {
public final static String TOGGLE_AXIS = "toggle_axis";
private AssetManager assetManager;
private Node rootNode = new Node("AxisRoot");
@Override
protected void initialize(Application app) {
assetManager = app.getAssetManager();
// 网格
Geometry grid = new Geometry("Grid", new Grid(21, 21, 1));
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.DarkGray);
grid.setMaterial(mat);
grid.center().move(0, 0, 0);
grid.setShadowMode(ShadowMode.Off);
rootNode.attachChild(grid);
// 坐标
createArrow("X", Vector3f.UNIT_X.mult(10), ColorRGBA.Red);
createArrow("Y", Vector3f.UNIT_Y.mult(10), ColorRGBA.Green);
createArrow("Z", Vector3f.UNIT_Z.mult(10), ColorRGBA.Blue);
toggleAxis();
}
@Override
protected void cleanup(Application app) {
}
@Override
protected void onEnable() {
SimpleApplication simpleApp = (SimpleApplication) getApplication();
simpleApp.getRootNode().attachChild(rootNode);
// 注册按键
InputManager inputManager = getApplication().getInputManager();
inputManager.addMapping(TOGGLE_AXIS, new KeyTrigger(KeyInput.KEY_F4));
inputManager.addListener(this, TOGGLE_AXIS);
}
@Override
protected void onDisable() {
rootNode.removeFromParent();
// 移除按键
InputManager inputManager = getApplication().getInputManager();
inputManager.removeListener(this);
inputManager.deleteMapping(TOGGLE_AXIS);
}
@Override
public void onAction(String name, boolean keyPressed, float tpf) {
if (name.equals(TOGGLE_AXIS) && keyPressed) {
toggleAxis();
}
}
/**
* 坐标轴开/关
*
* @return
*/
public boolean toggleAxis() {
SimpleApplication simpleApp = (SimpleApplication) getApplication();
if (simpleApp.getRootNode().hasChild(rootNode)) {
simpleApp.getRootNode().detachChild(rootNode);
return false;
} else {
simpleApp.getRootNode().attachChild(rootNode);
return true;
}
}
/**
* 创建一个箭头
*
* @param vec3
* 箭头向量
* @param color
* 箭头颜色
*/
private void createArrow(String name, Vector3f vec3, ColorRGBA color) {
// 创建材质,设定箭头的颜色
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color);
mat.getAdditionalRenderState().setLineWidth(3f);
mat.getAdditionalRenderState().setWireframe(true);
// 创建几何物体,应用箭头网格。
Geometry geom = new Geometry("Axis_" + name, new Arrow(vec3));
geom.setMaterial(mat);
geom.setShadowMode(ShadowMode.Off);
// 添加到场景中
rootNode.attachChild(geom);
}
}
效果:
保存对全局对象的引用
你可以在AppState中保存一份对全局对象的引用,这样一来编码比较方便,另外编码的习惯会与在SimpleApplication中一致。
public class MyAppState extends AbstractAppState {
private SimpleApplication simpleApp;
private AssetManager assetManager;
private AppStateManager stateManager;
private InputManager inputManager;
private ViewPort viewPort;
private Camera camera;
@Override
public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app);
this.simpleApp = (SimpleApplication) app;
this.assetManager = app.getAssetManager();
this.stateManager = app.getStateManager();
this.inputManager = app.getInputManager();
this.viewPort = app.getViewPort();
this.camera = app.getCamera();
}
}
AppState之间的通信
虽说我们希望 AppState 的设计要符合高内聚低耦合原则,做开发时总是避免不了要与其他AppState交互。比较推荐的方式,是通过 AppStateManager 进行通信。
例如在InputAppState中,需要调用LightAppState中的方法,是这样做的:
// 开关灯
LightAppState light = stateManager.getState(LightAppState.class);
if (light != null) {
light.setEnabled(!light.isEnabled());
}
AppStateManager的 T getState(Class<T> stateClass)
方法利用了Java的泛型机制,通过 AppState 的类型来查找实例。如果存在相同类型的的 AppState,就会返回这个对象,没有的话就会返回 null。
如果不顾虑返回值为空的情况,你甚至可以直接这么做:
// 关灯
stateManager.getState(LightAppState.class).turnOff();
由于这个机制,AppStateManager 中相同类型的 AppState 对象只能有一个。
如果想判断AppStateManager中是否有某个 AppState 对象,可以这么做:
stateManager.has(myAppState);
界面或场景切换
经常有人问这个问题:我该怎么切换主界面和游戏界面?
首先把主界面和游戏界面分别写到两个不同的AppState中,各自管理自己的子场景、输入处理等内容。例如:
- MainMenuAppState
- InGameAppState
然后,在主类中实例化这些 AppState,把主界面添加到AppStateManager中,作为游戏开始时的界面。
public MyGame extends SimpleApplication {
MainMenuAppState mainMenu;
InGameAppState inGame;
public void simpleInitApp() {
mainMenu = new MainMenuAppState();
inGame = new InGameAppState();
stateManager.attach(mainMenu);
}
}
你想切换到游戏界面?写一个方法进行切换就好了。
// 切换到游戏界面
public void switchToInGame() {
stateManager.detach(mainMenu);
stateManager.attach(inGame);
}
如果你不希望因为用户频繁切换界面导致 AppState 总是重复初始化,也可以一次性把2个 AppState都添加到 AppStateManager 中,然后把不需要显示的 AppState 先禁用掉。
public void simpleInitApp() {
stateManager.attachAll(new MainMenuAppState(),
new InGameAppState());
stateManager.getState(InGameAppState.class).setEnabled(false);
}
切换界面时使用 setEnable() 方法来代替 attach/detach。
// 切换到游戏界面
public void switchToInGame() {
stateManager.getState(MainMenuAppState.class).setEnabled(false);
stateManager.getState(InGameAppState.class).setEnabled(true);
}
jME3中的那些AppState
有4个AppState前面我们已经介绍过了,它们是:
- StatsAppState
- FlyCamAppState
- AudioListenerState
- DebugKeysAppState
除了这4个自动加载的 AppState 以外,jME3还提供了一些具有特殊功能的 AppState。
com.jme3.app.state.ScreenshotAppState
它的作用是按 Print Screen 键截屏com.jme3.app.BasicProfilerState
它的作用是按 F6 键,以图形方式显示渲染效率。com.jme3.app.ChaseCameraAppState
它是一个第三人称摄像机,激活时将主动禁用FlyCamAppState。com.jme3.cinematic.Cinematic
它的作用是根据脚本播放剧情动画。com.jme3.app.state.VideoRecorderAppState
录制游戏画面,保存为avi文件。位于jme3-desktop模块中,渣机慎用。com.jme3.bullet.BulletAppState
集成Bullet物理引擎。位于jme3-bullet模块中。
Control
com.jme3.scene.control.Control
可以操纵单个游戏实体(Spatial)的行为,例如AnimControl就是用来控制模型动画的,MotionEvent可以遥控模型沿着固定轨迹移动。
每个Spatial可以绑定多个Control实例,每个Control控制一种特定的行为。通过一行代码,就可以把Control对象和Spatial绑定。
spatial.addControl(Control control);
举个例子来说 ,我们可以根据NPC的不同行为,分别实现多个Control,并把它们绑定到一个NPC身上:
- PhysicalControl 配合物理引擎使用,控制NPC的运动;
- AiControl 负责控制NPC的智能行为,例如3D寻路等;
- AnimationContorl 负责在NPC处于不同状态时,播放对应的3D动画。
- TalkControl 负责控制NPC如何说话,根据说话的内容播放音频。
- FollowControl 让NPC能够跟随某个目标一起运动。
- 等等...
实例:物体自旋
下面是一个简单的自定义控件,它可以让模型绕Y轴以固定速率旋转。注意update(float tpf)
方法,这是该控件内部的主循环。
package net.jmecn.logic;
import java.io.IOException;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.math.FastMath;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
/**
* 让模型绕Y轴以固定速率旋转
*
* @author yanmaoyuan
*
*/
public class RotateControl implements Control {
private Spatial spatial;
// 旋转速度:每秒180°
private float rotateSpeed = FastMath.PI;
public RotateControl() {
this.rotateSpeed = FastMath.PI;
}
public RotateControl(float rotateSpeed) {
this.rotateSpeed = rotateSpeed;
}
@Override
public void setSpatial(Spatial spatial) {
this.spatial = spatial;
}
@Override
public void update(float tpf) {
spatial.rotate(0, tpf * rotateSpeed, 0);
}
@Override
public void render(RenderManager rm, ViewPort vp) {
}
@Override
public void write(JmeExporter ex) throws IOException {
throw new IOException("暂不支持");
}
@Override
public void read(JmeImporter im) throws IOException {
throw new IOException("暂不支持");
}
@Override
public Control cloneForSpatial(Spatial spatial) {
RotateControl c = new RotateControl(rotateSpeed);
c.setSpatial(spatial);
return c;
}
}
下面这个代码,RotateControl让方块绕Y轴旋转。
package net.jmecn.logic;
import com.jme3.app.SimpleApplication;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
/**
* 主循环
*
* @author yanmaoyuan
*
*/
public class HelloControl extends SimpleApplication {
@Override
public void simpleInitApp() {
cam.setLocation(new Vector3f(3.3435764f, 3.7595856f, 6.611723f));
cam.setRotation(new Quaternion(-0.05573249f, 0.9440857f, -0.23910178f, -0.22006002f));
// 创建一个方块
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
Geometry spatial = new Geometry("Box", new Box(1, 1, 1));
spatial.setMaterial(mat);
// 添加控件
spatial.addControl(new RotateControl(FastMath.PI));
rootNode.attachChild(spatial);
// 添加光源
rootNode.addLight(new DirectionalLight(new Vector3f(-1, -2, -3)));
}
public static void main(String[] args) {
// 启动
HelloControl app = new HelloControl();
app.start();
}
}
AbstractControl
与 AppState 一样,你也没有必要直接实现 Control 接口。我们可以通过继承 AbstractControl 来节省代码。
com.jme3.scene.control.AbstractControl
AbstractControl 中做了很多基础工作,比如保存被绑定的Spatial引用,增加 setEnabled() 方法让你可以激活/禁用某个Control的功能。
比较特别是的,继承AbstractControl的话,我们的主循环就不能写在 update() 方法中了,而是改为写在 controlUpdate()
方法中。
实例:让物体上下浮动
下例的FloatControl基于AbstractControl,作用是让物体做上下往返运动。
package net.jmecn.logic;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;
/**
* 一个让目标上下浮动的控件
*
* @author yanmaoyuan
*
*/
public class FloatControl extends AbstractControl {
float tmp = 0;
boolean raise = true;
float dist;// 上下浮动的距离
float speed;// 浮动的速度
public FloatControl() {
this.dist = 0.5f;
this.speed = 1f;
}
public FloatControl(float dist, float speed) {
this.dist = dist;
this.speed = speed;
}
@Override
protected void controlUpdate(float tpf) {
if (speed == 0)
return;
float delta = tpf * speed;
if (tmp < dist && raise) {
tmp += delta;
spatial.move(0, delta, 0);
} else {
raise = false;
}
if (tmp > -dist && !raise) {
tmp -= delta;
spatial.move(0, -delta, 0);
} else {
raise = true;
}
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
}
};
代码量是不是比直接实现Control接口少多了?
实例:让物体朝目标移动
下例的MotionControl也不算很复杂,它的作用是让物体朝目标点做直线运动。
package net.jmecn.game;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
/**
* 这是一个运动控件,其作用是让模型朝目标点直线运动。
*
* @author yanmaoyuan
*
*/
public class MotionControl extends AbstractControl {
// 运动速度
private float walkSpeed = 1.0f;
private float speedFactor = 1.0f;
// 运动的方向向量
private Vector3f walkDir;
// 运动一步的向量
private Vector3f step;
// 当前位置
private Vector3f loc;
// 目标位置
private Vector3f target;
// 观察者
private Observer observer;
public MotionControl() {
this(1.0f);
}
public MotionControl(float walkSpeed) {
this.walkSpeed = walkSpeed;
walkDir = null;
target = null;
loc = new Vector3f();
step = new Vector3f();
}
/**
* 设置运动速度
* @param walkSpeed
*/
public void setWalkSpeed(float walkSpeed) {
this.speedFactor = walkSpeed;
}
/**
* 设置观察者
* @param observer
*/
public void setObserver(Observer observer) {
this.observer = observer;
}
/**
* 设置目标点
*
* @param target
*/
public void setTarget(Vector3f target) {
this.target = target;
if (target == null) {
walkDir = null;
return;
}
// 当模型面朝目标点
this.spatial.lookAt(target, Vector3f.UNIT_Y);
// 计算运动方向
walkDir = target.subtract(loc);
walkDir.normalizeLocal();
}
@Override
public void setSpatial(Spatial spatial) {
super.setSpatial(spatial);
// 初始化位置
loc = new Vector3f(spatial.getLocalTranslation());
}
/**
* 重写主循环,让这个模型向目标点移动。
*/
@Override
protected void controlUpdate(float tpf) {
if (walkDir != null) {
// 计算下一步的步长
float stepDist = walkSpeed * tpf * speedFactor;
if (stepDist == 0f) {
return;
}
// 计算离目标点的距离
float dist = loc.distance(target);
if (stepDist < dist) {
// 计算位移
walkDir.mult(stepDist, step);
loc.addLocal(step);
spatial.setLocalTranslation(loc);
} else {
// 可以到达目标点
walkDir = null;
spatial.setLocalTranslation(target);
target = null;
// 通知观察者,已经抵达目标点了。
if (observer != null) {
observer.onReachTarget();
}
}
}
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
}
}
与Control通信
Control是被绑定到Spatial上的,通过Spatial的 T getControl(Class<T> class)
方法可以查询Spatial对象身上的Control对象。
AnimContorl animControl = spatial.getControl(AnimContorl .class);
与 AppStateManager 中的 T getState(Class<T> class)
方法类似,getContorl() 方法同样使用了泛型机制。一旦查询到Spatial中绑定了相同类型的Control,就会直接返回Control的实例。
在不考虑返回值为null的情况下(并不是什么好习惯),我们可以直接调用Control的方法。基于这种特性,我们就可以在Control访问绑定到同一个Spatial的其他Control,并实现Control之间的通信。
spatial.getControl(MotionContorl.class).setTarget(new Vector3f(10f, 0f, 12f));
jME3中的Control
jME3有一些定义好的Control,各有用途。
com.jme3.scene.control.BillboardControl
这个Control能够让物体的正面始终对准摄像机。com.jme3.scene.control.CameraControl
让摄像机跟随某个物体,或者让物体跟随摄像机。com.jme3.scene.control.LightControl
让光源跟随某个物体,或者让物体跟随光源。com.jme3.scene.control.LodControl
根据视野距离自动调整模型的层次细节(Level Of Detail),用于性能优化。
多线程优化
为什么要用多线程
首先,从操作系统的角度来看,多线程并不比单线程运行得更快。因为CPU除了进行计算以外,还要额外负责线程的调度工作。
然而,CPU并不仅仅是在做计算。程序运行时,CPU绝大多数时间是在等待要计算的数据。打个比方,我(CPU)吃完一锅莲藕排骨汤只需要十几分钟,但是把汤熬好却要准备大半天。
多线程的目的是为了最大限度的利用CPU资源。这就像在等待排骨汤熬好的这段时间,我还可以煮个饭、炒个菜。
优化思路
-
图形渲染线程和逻辑线程并行
-
I/O线程与其他线程并行
-
逻辑线程并行(难)