原文: Libgdx游戏开发(7)——开始游戏界面实现-Stars-One的杂货小窝
上篇文章也是讲解了如何实现暂停,但实际上,上篇的做法可能不够优雅
因为暂停和游戏界面我们可以分成2个Screen对象,这样只需要监听键盘输入,更改显示不同的Screen对象即可
本文的实现目标:
使用Screen来实现,进入游戏前,先显示一个游戏主界面,按下enter再开始游戏
前置知识
还记得之前例子,都是继承ApplicationAdapter
类,并在其的render
方法中实现我们的游戏逻辑
由于我们要使用Screen对象,所以我们得使用Game
对象类替换我们之前继承的ApplicationAdapter
对象
实际上,Game
对象和ApplicationAdapter
最终父类都是ApplicationListener
,只不过Game对象帮我们封装好了管理Screen的方法
Game对象提供了一个
setScreen()
方法来设置当前显示的Screen对象
基础使用
1.使用Game类作为入口
在我们启动方法中,使用Game作为启动的入口,下面的MyGame
即为Game对象
object DesktopLauncher { @JvmStatic fun main(arg: Array<String>) { val config = Lwjgl3ApplicationConfiguration() config.setForegroundFPS(60) //设置游戏窗口大小为800*480 config.setWindowedMode(800, 480) //设置开启垂直同步 config.useVsync(true) //Lwjgl3Application(CircleBallTest(), config) Lwjgl3Application(MyGame(), config) } }
MyGame代码
class MyGame : Game() { val batch: SpriteBatch by lazy { SpriteBatch() } val font: BitmapFont by lazy { BitmapFont() } val shape: ShapeRenderer by lazy { ShapeRenderer() } override fun create() { //这里调用下变量,实际相当于初始化了 batch font shape //注意这里,已经设置了首屏幕!! this.setScreen(MainScreen(this)) } override fun render() { super.render() } override fun dispose() { super.dispose() //释放资源 shape.dispose() font.dispose() batch.dispose() } }
之后,这个MyGame将作为全局单例对象来进行使用;
由于是单例对象,所以,我们可以在其创建的时候,进行相关资源的创建,比如绘制图片和文字等对象创建(这里不再赘述,若是类有些陌生可详见之前文章讲解),以及嘴硬的资源释放,避免出现内存溢出问题
而官方给出的代码示例中,是将此MyGame对象作为之后Screen的构造函数传入(因为需要调用Game对象对应方法来设置当前显示屏幕)
但我觉得可能在整个全局静态类直接调用可能会好点?但不确定是否是最优做法
所以下面还是先按照官方例子走一遍
2.创建对应的Screen
假设我们先简单些,有2个Screen,一个是主界面MainScreen
,另一个则是游戏运行界面GameScreen
和ApplicationAdapter类似,Screen接口也有一个
ScreenAdapter
空实现类
我们可以直接继承ScreenAdapter类,从而只重写我们需要的方法即可,代码更加清晰
MainScreen就简单绘制下游戏主界面的文字提示,代码如下:
class MainScreen(val game: MyGame) : ScreenAdapter() { override fun render(delta: Float) { game.apply { batch.begin(); font.draw(batch, "Welcome to Drop!!! ", 100f, 150f); font.draw(batch, "Tap anywhere to begin!", 100f, 100f) batch.end(); } //当鼠标点击则触发开始游戏,这里相信各位自己也能做些扩展,比如按下enter键来实现(前面文章也已经讲解过了) if (Gdx.input.isTouched()) { game.setScreen(GameScreen(game)) dispose(); } } }
而我们的GameScreen,则是之前我们的相关代码,只是绘制的时候使用的是全局对象Game里的相关对象进行绘制
class GameScreen() : ScreenAdapter() { val game by lazy { GloGame.game } val ball by lazy { Ball() } val line by lazy { MyBan() } val pauseInput by lazy { PauseInput() } override fun show() { Gdx.input.inputProcessor = pauseInput } override fun render(delta: Float) { if (pauseInput.handlePause { drawLogic() //绘制暂停的页面提示 GloGame.game.apply { Gdx.gl.glClearColor(0f, 0f, 0f, 0.8f); // 设置清屏颜色为透明度80%的黑色 batch.begin() font.draw(batch, "Pause", 100f, 150f) batch.end() } }) { return } drawLogic() updateXy() } private fun drawLogic() { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) val shape = game.shape line.draw(shape) ball.draw(shape) } fun updateXy() { //运动的逻辑 ball.gundon() line.control() ball.checkFz() //检测碰撞到数横条 ball.checkLineP(line) } }
这里我们封装了个简单的类实现游戏暂停(不过又觉得好像应该把暂停封装为一个Screen对象比较好)
class PauseInput() : InputAdapter() { var isPaused = false private var count = 0 override fun keyDown(keycode: Int): Boolean { if (keycode == Input.Keys.ESCAPE) { isPaused=isPaused.not() return true // 表示已经处理了按键事件 } return false; // 表示未处理按键事件 } fun handlePause(action: () -> Unit): Boolean { if (isPaused) { //保证当前帧和上一帧相同后,就不再绘制了 if (count <= 1) { action.invoke() count++ } } else { count = 0 } return isPaused } }
还有一些其他类,之前章节写的对应代码,为了方便实践,再贴一遍:
class MyBan { var width = 200f var height = 10f var x = 0f var y = height fun draw(shape: ShapeRenderer) { shape.begin(ShapeRenderer.ShapeType.Filled) //这里注意: x,y是指矩形的左上角 shape.rect(x, height, width, height) shape.end() } val spped = 400 fun control() { if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { x -= spped * Gdx.graphics.deltaTime } if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { x += spped * Gdx.graphics.deltaTime } //这里屏蔽y坐标改变,只给控制左右移动 return if (Gdx.input.isKeyPressed(Input.Keys.UP)) { y += spped * Gdx.graphics.deltaTime } if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { y -= spped * Gdx.graphics.deltaTime } } } class Ball { var size = 5f var x = 50f var y = 50f var speedX = 5f var speedY = 5f //与板子的碰撞检测 fun checkLineP(myB: MyBan) { val flag = x - size >= myB.x && x + size <= myB.x + myB.width if (y - size <= myB.y && flag) { speedY = speedY * -1 } } fun gundon() { x += speedX y += speedY } fun draw(shape: ShapeRenderer) { shape.begin(ShapeRenderer.ShapeType.Filled) shape.circle(x, y, size) shape.end() } fun checkFz() { //到达右边缘,x变反 if (x + size >= Gdx.graphics.width) { speedX = speedX * -1 } //到达下边缘,y变反 //todo 这个是判输条件! if (y - size <= 0) { //消失 //speedY = speedY * -1 } //到达上边缘,y变反 if (y + size >= Gdx.graphics.height) { speedY = speedY * -1 } //到达左边缘,x变反 if (x - size <= 0) { speedX = speedX * -1 } } }
3.最终效果
使用上的注意事项
- 切换到一个新的Screen的时候,如果之前的Screen不再使用,需要手动调用
Screen.dispose
方法,进行资源的释放 - 给Game对象设置Screen的时候,设置的新的那个Screen会调用
onShow()
方法,而之前的Screen会调用onHide()
方法 - 如果有需要的话,一般在onShow()方法,给当前Screen设置一个输入监听器
优化尝试 - 全局game对象
使用一个全局静态类来管理game对象,取消对应Screen构造方法传game对象,测试发现似乎没啥问题
object GloGame{ lateinit var game: MyGame } class MyGame : Game() { val batch: SpriteBatch by lazy { SpriteBatch() } val font: BitmapFont by lazy { BitmapFont() } val shape: ShapeRenderer by lazy { ShapeRenderer() } override fun create() { //这里调用下变量,实际相当于初始化了 batch font shape GloGame.game = this this.setScreen(MainScreen()) } override fun render() { super.render() } override fun dispose() { super.dispose() //释放资源 shape.dispose() font.dispose() batch.dispose() } }