放假期间闲着地无事就又写了一个小游戏消遣一下,为什么说”又“呢?是因为去年春节期间也写了一款,只是这回省去了上网找图片资源的时间直接改用代码手绘图像,虽然画面不如之前的那个华丽,但加入几个关键的数学算法,使游戏中的精灵移动更加合理。
传统的游戏开发是需要用到游戏引擎的,早期游戏引擎的最大作用是发挥硬件的机能,不过移动设备经过这几年的发展已经非常成熟了,不用游戏引擎照样能开发游戏,所以现在的游戏引擎更多的是方便跨平台跨硬件的开发罢了。
我写这篇教程的目的主要是从应用开发的角度讲解游戏的制作,所以为了便于大家更好地学习和研究,我直接把项目源码放在了Github上以供参考也希望得到各位高人的指教。
Github源码:https://github.com/greentea107/BoxSpaceGame
游戏所涉及的技术栈如下:Kotlin 协程 SurfaceView MediaPlayer SoundPool LiveEventBus
操控和玩法
操作很简单,十字键控制玩家的移动,可以往八个方向移动,在玩家按住“开火”按钮的时候可以不断地发射子弹,而且在开火期间玩家虽然可以移动但方向锁定的。“瞬移”按钮可以让玩家瞬间移动一段距离。
游戏的玩法是采用通关模式,第一关有五架敌机,全部打掉后就算过关,每过一关就增加一架敌机并增加敌机的HP值,以此类推看玩家能闯过多少关。
数学算法类MathUtils
2D游戏中实现角色的垂直和水平方向的移动是很简单的,就是单纯的对X轴或Y轴的加减运算,但要实现夹角方向的移动时要怎么做?是不是同时对X轴和Y轴作加减计算吗?
如果真这么做话,当我们用十字键或摇杆转圈的话会发现角色在屏幕上的移动轨迹呈现的是矩形,而非圆形,尤其是当角色做夹角方向运动时会明显的发现角度移动的比垂直或水平方向的快,所以同时对X轴和Y轴做加减运算的办法是有问题的。
由于我去年做的飞机大战游戏就有这个问题,为此今年我重新改进了算法,角色移动的坐标将根据角度和移动的距离获得。并将个算法封装成了工具类。
MathUtils.kt
import android.graphics.PointF
import android.graphics.RectF
import android.util.LruCache
import androidx.annotation.NonNull
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
object MathUtils {
/**
* 判断两个矩形是否相交
*/
fun cross(r1: RectF, r2: RectF): Boolean {
val zx = abs(r1.left r1.right - r2.left - r2.right)
val x = abs(r1.left - r1.right) abs(r2.left - r2.right)
val zy = abs(r1.top r1.bottom - r2.top - r2.bottom)
val y = abs(r1.top - r1.bottom) abs(r2.top - r2.bottom)
return zx <= x && zy <= y
}
/**
* 对COS和SIN的结果进行缓存,如果某个角度已经计算过则直接取缓存,不用每次都计算
* 缓存的KEY为保留两位小数的角度值,所以从缓存取值有精度不准的问题
*/
private val cosCache = LruCache<String, Double>(360)
private val sinCache = LruCache<String, Double>(360)
/**
* @param isCache 是否使用缓存
*/
private fun getCosByRadians(@NonNull angle: Double, isCache: Boolean = true): Double {
//将Double类型的角度值保留两位小数后转String类型,用于缓存的KEY值
val keyCache = String.format("%.2f", angle)
return if (isCache) {
// 有缓存取缓存,没缓存就计算后再缓存
if (cosCache[keyCache] != null)
cosCache[keyCache]
else {
val result = cos(Math.toRadians(angle))
cosCache.put(keyCache, result)
result
}
} else {
// 不用缓存直接计算
cos(Math.toRadians(angle))
}
}
/**
* @param isCache 是否使用缓存
*/
private fun getSinByRadians(@NonNull angle: Double, isCache: Boolean = true): Double {
//将Double类型的角度值保留两位小数后转String类型,用于缓存的KEY值
val keyCache = String.format("%.2f", angle)
return if (isCache) {
// 有缓存取缓存,没缓存就计算后再缓存
if (sinCache[keyCache] != null)
sinCache[keyCache]
else {
val result = sin(Math.toRadians(angle))
sinCache.put(keyCache, result)
result
}
} else {
// 不用缓存直接计算
sin(Math.toRadians(angle))
}
}
/**
* 根据半径、角度计算对应的坐标
* 角度以三点钟方向为0度,顺时针方向增加
* @param ptOrgini 原点坐标,默认为0,0
*/
fun getCoordsByAngle(radius: Float, angle: Double, ptOrgini: PointF = PointF()): PointF {
val x = ptOrgini.x radius * getCosByRadians(angle)
val y = ptOrgini.y radius * getSinByRadians(angle)
return PointF(x.toFloat(), y.toFloat())
}
/**
* 根据两个坐标获取角度
* @param x1 纬度1
* @param y1 经度1
* @param x2 纬度2
* @param y2 经度2
* @return
*/
fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double {
val p1 = PointF(x1, y1)
val p2 = PointF(x2, y2)
val angle = atan2((p2.y - p1.y), (p2.x - p1.x))
return angle * (180 / Math.PI)
}
}
这个数学工具类就是这个游戏的核心算法了。由于用到了浮点计算,且这种计算比较耗资源,所以使用了LruCache对已经计算过的值进行缓存以便于下回可以快速取值。这个工具类的用法会在后续的代码中用到。下一篇我们就开始组装游戏的各种”精灵“了。
如果对我的文章感兴趣可以加我的Q群聊或公众号:口袋里的安卓
,