Libgdx游戏开发(2)——接水滴游戏实现
本文使用Kotlin语言开发
通过本文的学习可以初步了解以下基础知识的使用:
- Basic file access
 - Clearing the screen
 - Drawing images
 - Using a camera
 - Basic input processing
 - Playing sound effects
 
游戏玩法
游戏的主要玩法有以下5点:
- 使用桶接水滴
 - 桶只能左右移动
 - 水滴会从顶部并加速下落
 - 玩家可以通过鼠标或键盘来移动桶
 - 游戏没有结束一说,可以一直玩
 
预览动图:

步骤
1.创建项目

由于我是要使用Kotlin开发,所以勾选了Kotlin开发的选项

实际上,创建出来的项目,还是Java文件写的,所以,为了方便,我用了Android Studio把Java文件转为了Kotlin文件

2.添加资源文件
之后,我们需要添加该有的素材文件,总共有四个文件
drop.wav水滴掉落在桶里的声音rain.mp3雨声(背景声)bucket.png桶图片drop.png水滴图片
都放在assets资源文件夹中

资源文件下载可以点击下载 蓝奏云下载
3.设置游戏配置
找到desktop文件夹目录下的代码文件,进行代码的修改,调整游戏窗口大小为800*480,并开启垂直同步
//设置游戏窗口大小为800*480
config.setWindowedMode(800, 480)
//设置开启垂直同步
config.useVsync(true)

4.加载资源文件
我们进入到core目录下的CatchWater文件,可以看到具体的代码结构

这里可以看到我们的类是继承ApplicationAdapter,从名字上就可以让我们猜测到是使用的设计模式中的适配器模式来兼容不同平台(没深入验证,仅是猜测)
ApplicationAdapter是抽象类方法,提供了几个需要重写的方法,感觉和Android开发中的Activity差不多,应该就是Libgdx游戏的生命周期方法了,这里先不深入扩展了

因为在游戏开始前,我们得先加载上述我们复制到项目的一些图片和音乐的资源文件,所以我们选择在create()方法中进行初始化我们的资源文件
添加以下代码:
lateinit var dropImage: Texture
lateinit var bucketImage: Texture
lateinit var dropSound: Sound
lateinit var rainMusic: Music
override fun create() {
    // load the images for the droplet and the bucket, 64x64 pixels each
    dropImage = Texture(Gdx.files.internal("drop.png"))
    bucketImage = Texture(Gdx.files.internal("bucket.png"))
    // load the drop sound effect and the rain background "music"
    dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"))
    rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"))
}
这里需要注意下,我们两张图片(水滴和桶)分辨率都是64*64
我们使用了Gdx.files.internal()方法来获取assets文件夹里的内容,之后游戏如果是运行在Android平台上,这个方法也是通用的
如果是assets文件夹里还有一层文件夹,可以这样写:
Gdx.files.internal("iamge/myimg.png")
稍微讲解下用到的几个类,具体知识得后面再新开文章进行研究
- Texture 这个英文翻译是纹理,但其实可以看做成图片的内存对象,类似Android开发里的Bitmap
 - Sound 比较短的那种音效文件
 - Music 时长较长的音频文件
 
5.播放背景音乐
之后我们可以实现播放背景音乐了,这个我们也直接在资源文件加载完毕之后播放吧
//设置循环播放背景音乐
rainMusic.setLooping(true)
rainMusic.play()
这个时候,我们可以进入到desktop里的那个文件,点击箭头运行游戏

游戏界面是黑屏的,但是已经开始播放音乐了,这里就不放图了
6.绘制图形
上述加载资源文件,我们已经得到了两个Texture对象,这个时候我们需要将其画出来,可以通过SpriteBatch这个类来实现
我们直接新建一个全局变量,然后也是在create()方法初始化即可

之后,在render()方法里,将我们的图片绘制出来
override fun render() {
    //设置屏幕背景色
    ScreenUtils.clear(0f, 0f, 0.2f, 1f)
    //绘制图片
    batch.begin()
    batch.draw(bucketImage, 400f, 400f)
    batch.end()
}
效果如下图所示:

这里需要注意一个问题:
在Libgdx中,默认坐标系是以左下角为原点,也就是常规的数学坐标系,当然,也可以通过Camare进行修改,具体如何修改,这里先不研究
上述代码,我们将图片对象的左下角,绘制到屏幕的(400,400)坐标位置上
上面虽然我们成功绘制了一个图片,但是考虑到下述几个问题:
- 雨滴掉落需要改变y坐标,但是y坐标如何存储呢?
 - 桶左右移动,需要改变x坐标,x坐标应该如何存储呢?
 - 如何确认雨滴和桶的边界关系呢?
 
考虑到上述的问题,单纯的坐标点记录会使后续的流程代码变得十分的复杂,这个时候我们可以引入一个范围来记录相关的坐标点信息
这里我们可以选用矩形Rectangle来存储我们的图片绘制位置
PS:实际上,不只有
Rectangle矩形,还有其他的形状,但具体的使用,还是留在后面教程再进行补充吧
我们通过定义矩形的左上角(x,y)坐标点和宽高,就可以确认一个Rectangle矩形范围了,如下代码所示:
val bucket = Rectangle().apply {
    //桶放中间
    x = (800 / 2 - 64 / 2).toFloat()
    y = 20.toFloat()
    width = 64.toFloat()
    height = 64.toFloat()
}
由于此矩形区域是我们用来找回进行边界关系的比对,所以将其宽高都设置为与图片图像的分辨率一样(64*64)
这样一来,我们使用batch.draw()方法绘制的时候,能将图片对象刚好覆盖到矩形范围里
batch.begin()
batch.draw(bucketImage, bucket.x, bucket.y)
batch.end()
7.雨滴下落实现
按照上述的逻辑,我们也创建一个雨滴的矩形范围,并将其绘制出来
雨滴默认在最上面,由于绘制图片的时候以左下角来绘制的,所以,最大y坐标减去64,即是雨滴开始的固定高度
然后雨滴的x坐标是随机的,但是最大范围为800减去宽度64
MathUtils是Libgdx提供的随机数工具类
val rainDrop = Rectangle().apply {
    //x坐标随机
    x = MathUtils.random(0, 800 - 64).toFloat()
    y = (480 - 64).toFloat()
    width = 64.toFloat()
    height = 64.toFloat()
}
override fun render() {
    batch.projectionMatrix = camera.combined
    batch.begin()
    batch.draw(dropImage, rainDrop.x, rainDrop.y)
    batch.end()
}
效果如下所示:

接下来,我们需要实现雨滴的下落功能,这里,我们可以采用时间作为变量,随着时间的变长来改rainDrop对象的y坐标数值
override fun render() {
    batch.begin()
    batch.draw(dropImage, rainDrop.x, rainDrop.y)
    batch.end()
    rainDrop.y -= 200 * Gdx.graphics.deltaTime
}
效果如下:

可以看到,下落效果实现了,但是似乎出现了重复的东西,其实就是我们在开始的没清除掉上次绘制的图像,加上清除的代码即可:
override fun render() {
     //清除并设置屏幕背景色
    ScreenUtils.clear(0f, 0f, 0.2f, 1f)
    batch.begin()
    batch.draw(dropImage, rainDrop.x, rainDrop.y)
    batch.end()
    //每帧的时间,高度减少200
    rainDrop.y -= 200 * Gdx.graphics.deltaTime
}

8.判断雨滴是否掉落在桶里
这里,就是需要判断边界了,上面也说到,使用Rectangle矩形,就是方便我们判断是否水滴和桶接触了
Rectangle对象中,有个overlaps()方法,就是专门来判断两个矩形是否重叠了
//判断两个矩形的接触面积有重叠,即水滴掉落在桶里
if (rainDrop.overlaps(bucket)) {
    //播放音效
    dropSound.play()
}
9.键盘控制改变桶位置
上面的功能已经基本完成了,那么还差通过键盘来控制桶的位置就能实现了
我们判断是否按下方向键左或右来改变桶对应矩形的x坐标,这样就能改变桶的绘制位置了
override fun render() {
    //清除设置屏幕背景色
    ScreenUtils.clear(0f, 0f, 0.2f, 1f)
    batch.projectionMatrix = camera.combined
    batch.begin()
    batch.draw(bucketImage, bucket.x, bucket.y)
    batch.draw(dropImage, rainDrop.x, rainDrop.y)
    batch.end()
    rainDrop.y -= 200 * Gdx.graphics.deltaTime
    //键盘输入判断
    if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.deltaTime
    if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.deltaTime
    //判断两个矩形的接触面积有重叠,即水滴掉落在桶里
    if (rainDrop.overlaps(bucket)) {
        //播放音效
        dropSound.play()
        //模拟消失(让水滴图片消失在游戏屏幕内)
        rainDrop.y = -64f
    }
}
效果如下图:

10.随机雨滴
最后,上述完成的只是一个雨滴,那么,我们需要生成多个雨滴,可以使用一个List来存储雨滴的位置
- 当雨滴掉落在地上,将雨滴从列表中移除;
 - 当雨滴被桶接到,雨滴也从列表中移除;
 
class CatchWater : ApplicationAdapter() {
    lateinit var dropImage: Texture
    lateinit var bucketImage: Texture
    lateinit var dropSound: Sound
    lateinit var rainMusic: Music
    lateinit var camera: OrthographicCamera
    lateinit var batch: SpriteBatch
    //最后雨滴下落时间
    var lastDropTime = TimeUtils.millis()
    val bucket = Rectangle().apply {
        //桶放中间
        x = (800 / 2 - 64 / 2).toFloat()
        y = 20.toFloat()
        width = 64.toFloat()
        height = 64.toFloat()
    }
    val rainDropList = arrayListOf<Rectangle>()
    override fun create() {
        // load the images for the droplet and the bucket, 64x64 pixels each
        dropImage = Texture(Gdx.files.internal("drop.png"))
        bucketImage = Texture(Gdx.files.internal("bucket.png"))
        // load the drop sound effect and the rain background "music"
        dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"))
        rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"))
        // start the playback of the background music immediately
        rainMusic.setLooping(true)
        rainMusic.play()
        // create the camera and the SpriteBatch
        camera = OrthographicCamera()
        camera.setToOrtho(false, 800f, 480f)
        batch = SpriteBatch()
        //开始先默认生成一个雨滴
        generateRainDrop()
    }
    private fun generateRainDrop() {
        val rainDrop = Rectangle().apply {
            //x坐标随机
            x = MathUtils.random(0, 800 - 64).toFloat()
            y = (480 - 64).toFloat()
            width = 64.toFloat()
            height = 64.toFloat()
        }
        rainDropList.add(rainDrop)
    }
    override fun render() {
        //清除设置屏幕背景色
        ScreenUtils.clear(0f, 0f, 0.2f, 1f)
        // tell the camera to update its matrices.
        camera.update()
        // tell the SpriteBatch to render in the
        // coordinate system specified by the camera.
        batch.projectionMatrix = camera.combined
        batch.begin()
        batch.draw(bucketImage, bucket.x, bucket.y)
        //绘制雨滴列表
        rainDropList.forEach {
            batch.draw(dropImage, it.x, it.y)
        }
        batch.end()
        // 触摸(手机端的操作和鼠标操作)
        if (Gdx.input.isTouched) {
            val touchPos = Vector3()
            touchPos[Gdx.input.x.toFloat(), Gdx.input.y.toFloat()] = 0f
            bucket.x = touchPos.x - 64 / 2
            camera.unproject(touchPos)
        }
        //键盘操作
        if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.deltaTime
        if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.deltaTime
        //500ms生成一个雨滴
        if (TimeUtils.millis() - lastDropTime > 500) {
            generateRainDrop()
            lastDropTime = TimeUtils.millis()
        }
        val iter: MutableIterator<Rectangle> = rainDropList.iterator()
        while (iter.hasNext()) {
            val raindrop = iter.next()
            raindrop.y -= 200 * Gdx.graphics.deltaTime
            //如果雨滴掉落在地上
            if (raindrop.y + 64 < 0) iter.remove()
            //判断两个矩形的接触面积有重叠,即水滴掉落在桶里
            if (raindrop.overlaps(bucket)) {
                //播放音效
                dropSound.play()
                //将此雨滴移除列表
                iter.remove()
            }
        }
    }
    override fun dispose() {
        //资源释放
        dropImage.dispose();
        bucketImage.dispose();
        dropSound.dispose();
        rainMusic.dispose();
        batch.dispose();
    }
}
上面有个涉及到手机和鼠标的操作,因为和Camera对象一起使用,还没过于研究,之后看看再深入一下吧
打包
关于打包的操作,可以通过Gradle的Task来进行操作
Android Studio4.2之后版本,把task给隐藏掉了,所以需要通过设置开启出来

之后Gradle重构当前项目,右侧的Gradle就会出现Task列表了

打包Android的和Android项目开发打包步骤一样的,这里不再赘述
如果是要打包的jar包,则可以点击右侧的Task任务,如下图所示:

生成的jar文件,在desktop\build\libs目录下
至于打包成exe,暂时还没研究,各位可以参考下这篇文章libGDX游戏开发之打包游戏(十二)_漫浅的博客-CSDN博客_libgdx开发的游戏
参考
- A Simple Game - libGDX
 - Quillraven/SimpleKtxGame: The LibGDX simple game with Kotlin and LibKTX using an assetmanager, pool and viewport
 - libGDX学习记录(三)接水滴_JS O_O的博客-CSDN博客
 
Libgdx游戏开发(2)——接水滴游戏实现的更多相关文章
- 【读书笔记《Android游戏编程之从零开始》】10.游戏开发基础(View 游戏框架)
		
对于玩家来说,游戏是动态的:对于游戏开发人员来说,游戏是静态的,只是不停地播放不通的画面,让玩家看到了动态的效果. 进入Android之前,首先要熟悉三个重要的类:View(视图).Canvas(画布 ...
 - C#游戏开发中快速的游戏循环
		
C#游戏开发中快速的游戏循环的实现.参考<精通C#游戏编程>一书. using System; using System.Collections.Generic; using System ...
 - Unity 2D游戏开发教程之2D游戏的运行效果
		
Unity 2D游戏开发教程之2D游戏的运行效果 2D游戏的运行效果 本章前前后后使用了很多节的篇幅,到底实现了怎样的一个游戏运行效果呢?或者说,游戏中的精灵会不会如我们所想的那样运行呢?关于这些疑问 ...
 - Unity 2D游戏开发教程之为游戏场景添加多个地面
		
Unity 2D游戏开发教程之为游戏场景添加多个地面 为游戏场景添加多个地面 显然,只有一个地面的游戏场景太小了,根本不够精灵四处活动的.那么,本节就来介绍一种简单的方法,可以为游戏场景添加多个地面. ...
 - 【Cocos2d-x游戏开发】浅谈游戏中的坐标系
		
无论是开发2D还是开发3D游戏,首先必须弄清楚坐标系的概念.在Cocos2d-x中,需要了解的有OpenGL坐标系.世界坐标系和节点坐标系. 1.UI坐标系 IOS/Android/Windows ...
 - 【读书笔记《Android游戏编程之从零开始》】11.游戏开发基础(SurfaceView 游戏框架、View 和 SurfaceView 的区别)
		
1. SurfaceView 游戏框架实例 实例效果:就是屏幕上的文本跟着点击的地方移动,效果图如下: 步骤: 新建项目“GameSurfaceView”,首先自定义一个类"MySurfac ...
 - OUYA游戏开发核心技术剖析OUYA游戏入门示例——StarterKit
		
第1章 OUYA游戏入门示例——StarterKit StarterKit是一个多场景的游戏示例,也是OUYA官方推荐给入门开发者分析的第一个完整游戏示例.本章会对StarterKit做详细介绍,包 ...
 - 《MFC游戏开发》笔记十 游戏中的碰撞检测进阶:地图类型&障碍物判定
		
本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9394465 作者:七十一雾央 新浪微博:http:// ...
 - 《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI
		
本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9374935 作者:七十一雾央 新浪微博:http:// ...
 
随机推荐
- Python下载网易云收藏
			
提前声明 仅作为个人学习使用,任何版权问题作者概不负责 本文的语言不会且不可能很严谨 博客园的编辑器有点BUG把我搞晕头了,所以本文可能有点鬼畜 前情 不知道各位有几个是对国内大厂的软件设计很满意的? ...
 - SpringCloud微服务实战——搭建企业级开发框架(四十五):【微服务监控告警实现方式二】使用Actuator(Micrometer)+Prometheus+Grafana实现完整的微服务监控
			
无论是使用SpringBootAdmin还是使用Prometheus+Grafana都离不开SpringBoot提供的核心组件Actuator.提到Actuator,又不得不提Micrometer ...
 - reduce累加实现
			
与map端的模式类似,map端要重写Mapper方法,reduce端也要重写Reduce方法,这里有一个泛型,我们先看参数类型 分别对应输入keyin,valuein,keyout,valueout. ...
 - iNeuOS工业互联网操作系统,设备运维业务和“低代码”表单开发工具
			
目 录 1. 概述... 2 2. 设备运维业务... 3 3. "低代码"表单开发工具... 6 1. 概述 iNeuOS工业互联网 ...
 - C#反射跟特性
			
一.什么是反射? 了解反射之前我们必须知道一个概念--元数据.有关程序和程序类型的信息叫做元数据,通俗的解释就是类里面的方法.属性.字段等. 而程序在运行的时候去查看其它程序集的行为就叫做反射.在我们 ...
 - PLC中增益和偏移
			
y=kx+b这个直线方程,那么增益就是指k这个斜率,而偏移就是指b. 模拟量转换时一般是不需要设置这两个参数的,只有当外部信号与模块接收的信号在值上有偏差的情况下才会去调整这个参数. 如果的模块信号是 ...
 - Dubbo源码(七) - 集群
			
前言 本文基于Dubbo2.6.x版本,中文注释版源码已上传github:xiaoguyu/dubbo 集群(cluster)就是一组计算机,它们作为一个总体向用户提供一组网络资源.这些单个的计算机系 ...
 - Apache DolphinScheduler 2.0.1 来了,备受期待的一键升级、插件化终于实现
			
✎ 编 者 按:好消息!Apache DolphinScheduler 2.0.1 版本正式发布! 本版本中,DolphinScheduler 经历了一场微内核+插件化的架构改进,70% 的代码被重构 ...
 - 使用docker简单编译k20pro内核
			
简介 本文将介绍一下如何使用docker编译红米k20pro的内核.作者当时尝试构建内核的原因是为了将3年前(好像是吧)购买的k20pro至尊版(已退役,12GB内存,512GB硬盘)制作成一个小的服 ...
 - Codeforces 1715E - Long Way Home
			
又是废掉的一个div2啊 第一次在学校熬夜打cf,开心还看到了自己最喜欢的斜率优化ohhh 链接 :E - Long Way Home 看到那个平方就可以靠感觉认为是斜率优化了.... 感觉似不似有点 ...