嘿,你好呀!是不是看着手机里那些小游戏,心里痒痒的,想着“我也能做出来”?别被那些复杂的 3D 引擎或者高大上的术语吓跑,今天我们就把“做游戏”这件事,拆解得像搭积木一样简单。我会带你用 Kotlin(现在的 Android 主流语言,比 Java 更简洁安全)一步步写出让朋友惊呼“哇,这是你做的?”的小游戏。
咱们不整那些虚的,直接上手。
第一步:别急着写代码,先想清楚你要做什么
很多新手最大的坑,就是上来就打开 Android Studio,然后迷茫地问:“我先画个圈,还是先定义个变量?”
在我教你怎么打开软件之前,咱们先花 5 分钟想清楚你要做什么。记住,第一款游戏,越小越好。
别想什么《王者荣耀》或者《原神》,哪怕只是“控制一个方块,躲开下落的石头”,那也是一个完整的游戏。
我们来定个靶子: 做一个叫《方块躲避球》的小游戏。
- 主角:屏幕底部的一个蓝色方块,左右移动。
- 敌人:从屏幕顶部随机掉落的红色小球。
- 目标:尽可能长时间地躲避,时间就是分数。
- 结束条件:碰到红色小球,游戏结束。
你看,这多简单?这就是我们今天的里程碑。
第二步:搭建你的“工地”(环境准备)
你需要一个地方来写代码,这个工地叫 Android Studio。
- 下载安装:去 google.cn/develop/android-studio 下载(如果你在国内,确保网络通畅)。
- 新建项目:
- 点击
New Project。 - 选择
Empty Views Activity(注意:选“Views”而不是“Compose”,因为 Views 更适合做游戏,逻辑更直观)。 - 语言:务必选择 Kotlin。
- Min SDK:选 API 21(Android 5.0),这样能兼容绝大多数老手机,也不用担心新功能用不了。
- 起个名字,比如
DodgeBall,选好保存位置,点击Finish。
- 点击
现在,你会看到一个让你眼花缭乱的项目结构。别慌,我们只需要关注两个核心文件:
AndroidManifest.xml:游戏的“身份证”,告诉系统游戏需要什么权限。MainActivity.kt:游戏的“大脑”,控制逻辑都在这里。activity_main.xml:游戏的“脸”,控制界面长什么样。
但等等,做游戏我们通常不直接用 XML 拖控件,因为游戏是动态画的,每一帧都在变。所以,我们要创建一个自定义 View。
第三步:创建游戏的“画布”
在 MainActivity.kt 的同级目录,新建一个 Kotlin 类,名字叫 GameView.kt。
这个类将成为我们游戏的画布。想象一下,你有一张白纸,每次游戏刷新,你就在这张纸上重新画一遍所有东西。
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
class GameView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// 定义画笔,就像画家用的刷子
private val playerPaint = Paint().apply {
color = Color.BLUE
style = Paint.Style.FILL
}
private val enemyPaint = Paint().apply {
color = Color.RED
style = Paint.Style.FILL
}
// 主角的位置和大小
private var playerX = 0f
private var playerY = 0f
private var playerSize = 100f
// 敌人数组,暂时为空,后面动态添加
private val enemies = mutableListOf<Enemy>()
// 游戏是否在进行中
private var isGameRunning = false
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 当屏幕大小改变时,初始化主角位置到底部中央
playerX = width / 2f - playerSize / 2
playerY = height - playerSize - 50
}
// 核心:每一帧,都在这里重绘
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 1. 清空画布(白色背景)
canvas.drawColor(Color.WHITE)
// 2. 画主角
canvas.drawRect(playerX, playerY, playerX + playerSize, playerY + playerSize, playerPaint)
// 3. 画所有敌人
for (enemy in enemies) {
enemy.update()
enemy.draw(canvas, enemyPaint)
}
// 如果游戏没开始,提示点击开始
if (!isGameRunning) {
canvas.drawText("点击屏幕开始游戏", width / 2f, height / 2f, Paint().apply {
color = Color.GRAY
textSize = 50f
textAlign = Paint.Align.CENTER
})
}
// 请求下一帧,让游戏“动”起来
invalidate()
}
// 处理触摸事件(左右移动)
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 点击开始游戏
if (!isGameRunning) {
startGame()
} else {
// 根据触摸位置移动主角
playerX = event.x - playerSize / 2
// 限制主角不跑出屏幕
playerX = playerX.coerceIn(0f, width - playerSize)
}
}
}
return true
}
private fun startGame() {
isGameRunning = true
enemies.clear()
// 启动一个线程来生成敌人
Thread {
while (isGameRunning) {
spawnEnemy()
Thread.sleep(500) // 每0.5秒生成一个敌人
}
}.start()
}
private fun spawnEnemy() {
// 简单的随机生成逻辑
val x = (Math.random() * (width - 50)).toFloat()
enemies.add(Enemy(x, -50f, 50f))
}
}
// 定义一个简单的 Enemy 类(内嵌在 GameView 里,方便理解)
class Enemy(var x: Float, var y: Float, var size: Float) {
val speed = 5f
fun update() {
y += speed
}
fun draw(canvas: Canvas, paint: Paint) {
canvas.drawCircle(x + size/2, y + size/2, size/2, paint)
}
}
这里有个新手大坑要注意:
很多人会在 onDraw 里写复杂的逻辑,或者创建对象。绝对不行! onDraw 每秒要调用 60 次,在里面创建 new Object() 会让手机内存爆炸,卡顿到怀疑人生。我们把敌人对象提前创建好,只在 onDraw 里画它。
接下来,我们需要把这个 GameView 放到我们的主界面里。打开 activity_main.xml,删掉里面默认的 TextView,换成我们的 GameView:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.dodgeball.GameView
android:id="@+id/gameView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
然后在 MainActivity.kt 里把它设为 setContentView:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(GameView(this))
}
}
现在,点击运行按钮。你会发现,屏幕上出现了一个蓝色方块,点击屏幕,它会移动,然后红色的球开始掉下来……等等,好像没结束?对,因为我们还没写碰撞检测。
第四步:让游戏有“输赢”——碰撞检测
这是游戏最核心的逻辑之一。就像两辆车撞在一起,我们需要判断两个图形是否重叠。
在矩形和圆形的碰撞检测中,有一个简化的方法:把圆形也看作一个矩形(bounding box)。如果两个矩形重叠,就算撞上了。
修改 GameView.kt,在 onDraw 方法中加入检测逻辑:
// 在 onDraw 方法中,画完敌人之后,加入这段代码
if (isGameRunning) {
checkCollisions()
// 移除飞出屏幕的敌人,防止内存堆积
enemies.removeAll { it.y > height }
}
然后添加这两个函数:
private fun checkCollisions() {
for (enemy in enemies) {
if (isColliding(playerX, playerY, playerSize, enemy.x, enemy.y, enemy.size)) {
gameOver()
return
}
}
}
// 简单的矩形碰撞检测算法
private fun isColliding(x1: Float, y1: Float, w1: Float, x2: Float, y2: Float, w2: Float): Boolean {
return x1 < x2 + w2 &&
x1 + w1 > x2 &&
y1 < y2 + w2 &&
y1 + w1 > y2
}
private fun gameOver() {
isGameRunning = false
// 绘制 Game Over 文字
val paint = Paint().apply {
color = Color.BLACK
textSize = 100f
textAlign = Paint.Align.CENTER
}
// 注意:这里不能直接调用 invalidate() 否则可能会死循环,
// 我们只是在下一帧绘制时画出文字即可,逻辑上已经停止更新敌人位置
canvas?.let {
it.drawColor(Color.WHITE) // 清空
canvas.drawText("游戏结束! 得分: ${enemies.size}", width / 2f, height / 2f, paint)
// 重新画主角(静态)
it.drawRect(playerX, playerY, playerX + playerSize, playerY + playerSize, playerPaint)
}
}
这里又是一个坑:
有些新手喜欢在 gameOver() 里直接 return,然后就不管了。但 onDraw 还在无限循环调用!你必须有一个标志位(如 isGameRunning)来控制逻辑不再更新,只负责绘制静态画面,否则游戏会无限循环播放“游戏结束”的动画。
第五步:增加分数和更流畅的控制
现在的游戏,敌人掉完就没了,也没有分数。我们来加点料。
- 分数系统:我们需要一个变量
score,每次移除飞出屏幕的敌人时,分数 +1。 - 更流畅的移动:现在的移动是“点击一下,跳一下”。我们要改成“按住不动,平滑移动”。
修改 onTouchEvent 和 GameView 的属性:
private var score = 0
// 在 onDraw 的末尾,游戏结束时绘制分数
if (isGameRunning) {
val scorePaint = Paint().apply {
color = Color.BLACK
textSize = 60f
}
canvas.drawText("分数: $score", 50f, 80f, scorePaint)
}
// 修改 onTouchEvent,支持滑动
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
if (isGameRunning) {
playerX = event.x - playerSize / 2
playerX = playerX.coerceIn(0f, width - playerSize)
}
}
MotionEvent.ACTION_DOWN -> {
if (!isGameRunning) {
score = 0
startGame()
}
}
}
return true
}
还有,刚才我们在 Thread 里生成敌人,这样写有点粗糙。更好的做法是使用 Handler 或者 Coroutine(协程),但对于新手,我们先保持简单。不过,你要知道,主线程(UI线程)是不能阻塞的,所以生成敌人的逻辑必须在子线程里,我们现在的做法是对的。
第六步:让游戏“活”起来——物理效果与视觉反馈
现在的游戏还是有点“干”。我们来加点“ Juice”(果汁,游戏设计术语,指让反馈更爽快的元素)。
- 主角跟随手指平滑移动:使用插值算法,让方块移动到手指位置时有一个延迟感,更自然。
- 敌人速度逐渐加快:随着分数增加,敌人下落速度变快,增加难度。
在 Enemy 类里添加一个静态速度基准,并根据分数动态调整:
class Enemy(var x: Float, var y: Float, var size: Float) {
companion object {
var baseSpeed = 5f
}
val speed = baseSpeed + (Math.random() * 2f) // 每个敌人速度略有不同
fun update(score: Int) {
// 每得10分,速度增加1
val speedMultiplier = 1f + (score / 10f) * 0.5f
y += speed * speedMultiplier
}
fun draw(canvas: Canvas, paint: Paint) {
canvas.drawCircle(x + size/2, y + size/2, size/2, paint)
}
}
在 GameView 的 onDraw 中调用时传入分数:
for (enemy in enemies) {
enemy.update(score) // 传入当前分数
enemy.draw(canvas, enemyPaint)
}
第七步:打包发布——把你的游戏给朋友玩
代码写完了,怎么让朋友装上?
- 点击 Android Studio 右上角的
Build->Build Bundle(s) / APK(s)->Build APK(s)。 - 等待编译完成,底部会提示
APK(s) generated successfully。 - 点击
locate,找到生成的.apk文件。 - 把这个文件发给朋友,或者传到自己的手机上安装。
新手大坑预警:
- 签名问题:第一次安装包可能无法安装,因为没签名。在
Build->Generate Signed Bundle / APK中,选择APK,创建一个新的密钥库(Keystore),设置密码和别名,一路下一步就能生成正式签名的 APK。 - 模拟器 vs 真机:模拟器运行游戏可能很卡,建议在真机上测试,尤其是触摸反馈和性能。
常见新手坑总结(避坑指南)
- 不要在
onDraw里new对象:这会引发 GC(垃圾回收)风暴,导致画面卡顿。把对象创建移到游戏初始化或单独的逻辑线程。 - 不要阻塞主线程:所有耗时的操作(如网络请求、大量计算)都要放在子线程。我们生成敌人的
Thread就是正确的做法。 - 坐标系统:Android 的坐标原点在左上角,X 向右增大,Y 向下增大。这和数学里的坐标系相反,新手容易搞反,导致物体“往上跑”而不是“往下掉”。
- 屏幕适配:不同手机屏幕大小不一样。我们现在的
width和height是动态获取的,所以没问题。但如果用了硬编码的像素值(如x = 100),在不同手机上位置会错位。 - 生命周期管理:当用户按下 Home 键,游戏应该暂停。我们需要重写
onPause()和onResume()方法,设置isGameRunning = false和重新赋值,避免后台运行时消耗电量和计算资源。
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
isGameRunning = false // 界面销毁时停止游戏
}
下一步:如何让它更专业?
现在,你已经有了一个能跑的、有逻辑的、有胜负的游戏。这已经超过 80% 的“想学游戏开发”的人了!
如果你想继续深入,可以:
- 添加音效:使用
SoundPool播放简单的音效。 - 更换素材:把蓝色方块换成你喜欢的精灵图(Sprite),把红色圆球换成小怪的图片。使用
BitmapFactory.decodeResource()加载图片。 - 使用 SurfaceView 或 TextureView:当游戏复杂度增加,
View的渲染性能可能不够,这时可以切换到SurfaceView,它是游戏开发的标准配置。 - 学习 libGDX 或 Unity:如果你想做更复杂的游戏(横版过关、RPG),现在的代码结构就不够用了,建议学习这些游戏引擎。
但记住,完成比完美更重要。你的第一款游戏,哪怕只是一个方块在屏幕上乱跳,也是你作为开发者的重要里程碑。
现在,打开 Android Studio,从头开始,打造你的第一个游戏吧!如果遇到报错,别慌,把错误信息贴到搜索引擎或 AI 助手那里,每个程序员都是靠“复制粘贴+理解”成长起来的。祝你玩得开心!