IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 贪吃蛇 -> 正文阅读

[移动开发]Android 贪吃蛇

先上个最后完成品的效果图
在这里插入图片描述

一、绘制页面

最终视图
在这里插入图片描述

界面绘制

  1. activity_main.xml 中的控件一共分为两个,我将按键单独提取出复合成一个控件,背景、蛇、食物自定义成一个 view
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.yunyan.snake.widget.BackgroundView
        android:id="@+id/backgroundView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.yunyan.snake.widget.KeyView
        android:id="@+id/controlView"
        android:layout_width="match_parent"
        android:layout_height="210dp"
        android:layout_margin="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 先来看一下 按键 的复合控件 view_key.xml 视图

用 Button 按钮来当 上下左右 与 开始 / 暂停 按键

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_gravity="center_vertical"
    android:layout_height="210dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="210dp"
        android:layout_height="210dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <Button
            android:id="@+id/keyView_btn_up"
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:background="@drawable/select_up"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/keyView_btn_left"
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:background="@drawable/select_left"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/keyView_btn_up" />

        <Button
            android:id="@+id/keyView_btn_right"
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:background="@drawable/select_right"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/keyView_btn_up" />

        <Button
            android:id="@+id/keyView_btn_down"
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:background="@drawable/select_down"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/keyView_btn_left" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <Button
        android:id="@+id/keyView_btn_switch"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@drawable/select_pause"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 为了让按键视觉效果更好 定义 selector 设置 每个按钮图片的 state_pressed 以实现按压效果

这里用按键 上 的文件来举例

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="true" android:drawable="@drawable/ic_up_pressed_white"/>

    <item android:state_pressed="false" android:drawable="@drawable/ic_up_white"/>

</selector>
  1. 定义 KeyView 继承 FrameLayout并实现 View.OnClickListener 接口

初始化相关的代码这里就不贴了,详细的可以点击文章尾部的 Github 源码

class KeyView(context: Context, attributeSet: AttributeSet) :

	private lateinit var mBtnUp: Button
	......
	private lateinit var mBtnSwitch: Button
	
	init {
        init()
    }
     private fun init() {
        val inflate = inflate(context, R.layout.view_key, this)
        mBtnUp = inflate.findViewById(R.id.keyView_btn_up)
        ......
        mBtnUp.setOnClickListener(this)
        ......
    }
    FrameLayout(context, attributeSet), View.OnClickListener {
       override fun onClick(v: View?) {
       }
    }

静态蛇绘制

  1. 按键视图完成现在来进行 蛇、食物的绘制

新建 BackgroundView 继承 View

class BackgroundView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
}
  1. 既然要绘制肯定需要画笔

定义三个全局变量画笔,进行延迟初始化

private lateinit var mPaintHead: Paint
private lateinit var mPaintBody: Paint
private lateinit var mPaintFood: Paint
  1. 蛇的身体可以看做是一个数组

所以我们定义两个数组存储 蛇 的x,y坐标,在定义一个变量蛇的默认长度
随机食物使用 float 类型变量存储坐标

private val mSnakeX = FloatArray(800)
private val mSnakeY = FloatArray(800)

private var mFoodX = 0f
private var mFoodY = 0f

private var mSnakeLength = 2
  1. 变量设置完成还需要进行初始化
	init {
        init()
	}

    private fun init() {
        mPaintHead = Paint() 
        mPaintHead.isAntiAlias = true
        mPaintHead.color = resources.getColor(R.color.head, null)
        ......
        mSnakeX[0] = 750f
        mSnakeY[0] = 500f
        mFoodX = 25 * Random().nextInt(15).toFloat()
        mFoodY = 25 * Random().nextInt(15).toFloat()
    }
  1. 开始绘制蛇身与食物,蛇的每一截长宽为 25,食物也为25
//  绘制蛇身
 for (i in mSnakeLength downTo 1) {
     mSnakeX[i] = mSnakeX[i - 1]
     mSnakeY[i] = mSnakeY[i - 1]
     canvas?.drawOval(mSnakeX[i],mSnakeY[i],mSnakeX[i] + 25,mSnakeY[i] + 25,mPaintBody)
}
//  绘制蛇头
canvas?.drawRect(mSnakeX[0], mSnakeY[0], mSnakeX[0] + 25, mSnakeY[0] + 25, mPaintHead)
//  绘制食物
canvas?.drawOval(mFoodX, mFoodY, mFoodX + 25, mFoodY + 25, mPaintFood)

二、让蛇动起来

  1. 定义枚举

DirectionStateEnum 用来记录方向
GameStateEnum 用来记录游戏状态

enum class DirectionStateEnum {
    UP,
    DOWN,
    LEFT,
    RIGHT
}
enum class GameStateEnum {
    START,
    PAUSE,
    STOP
}
  1. BackgroundView 中设置方向变量默认向右 ,游戏状态为停止
private var mDirectionEnum = DirectionStateEnum.RIGHT
private var mGameState = GameStateEnum.STOP
  1. 定义一个 定时器 与 handler

定时器每个 0.1s 向 handler 发送消息并调用 invalidate() 重新绘制

    private val mTimer = Timer().schedule(object : TimerTask() {
        override fun run() {
            val message = Message()
            message.what = 99
            mHandler.sendMessage(message)
        }

    }, 0, 100)
object : Handler() {
        override fun handleMessage(msg: Message) {
            if (msg.what == 99 && mGameState == GameStateEnum.START) {
                judgmentDirection()
                invalidate()
            }
        }

    }
	/**
     * 判断蛇头方向
     */
    private fun judgmentDirection() {
        when (mDirectionEnum) {
            DirectionStateEnum.UP -> {
                mSnakeY[0] = mSnakeY[0] - 25
                if (mSnakeY[0] < 25) mSnakeY[0] = measuredHeight.toFloat()
            }
            DirectionStateEnum.DOWN -> {
                mSnakeY[0] = mSnakeY[0] + 25
                if (mSnakeY[0] > measuredHeight) mSnakeY[0] = 25f
            }
            DirectionStateEnum.LEFT -> {
                mSnakeX[0] = mSnakeX[0] - 25
                if (mSnakeX[0] < 25) mSnakeX[0] = measuredWidth.toFloat()
            }
            DirectionStateEnum.RIGHT -> {
                mSnakeX[0] = mSnakeX[0] + 25
                if (mSnakeX[0] > measuredWidth) mSnakeX[0] = 25f
            }
        }
    }

三、按键控制蛇的方向与游戏状态

  1. 新建 IKeyData 接口用作数据传递,并在 MainActivity 中实现接口
interface IKeyData {

    /**
     * 获取游戏状态
     */
    fun gameState(gameState: GameStateEnum)

    /**
     * 获取蛇头方向
     */
    fun direction(directionState: DirectionStateEnum)

}
	override fun gameState(gameState: GameStateEnum) {
        if (gameState == GameStateEnum.STOP) {
            mKeyView.gameOver()
        } else {
            mBackgroundView.setGameState(gameState)
        }
    }

    override fun direction(directionState: DirectionStateEnum) {
        mBackgroundView.setDirection(directionState)
    }
  1. BackgroundView 中新建方法用作获取游戏状态与蛇头方向并判断蛇头方向是否与上一次方向相反
	 fun setGameState(gameStateEnum: GameStateEnum) {
        this.mGameState = gameStateEnum
    }

	/**
     * 设置蛇头方向
     */
    fun setDirection(directionState: DirectionStateEnum) {
        if (isDirectionContrary(directionState)) {
            mGameState = GameStateEnum.STOP
            gameOver()
            Toast.makeText(context, "方向相反,游戏失败!", Toast.LENGTH_SHORT).show()
        }
        if (mGameState == GameStateEnum.START) {
            this.mDirectionEnum = directionState
        }
    }
	/**
     * 判断按键方向是否与所前进方向相反
     */
    private fun isDirectionContrary(directionState: DirectionStateEnum): Boolean {
        when (directionState) {
            DirectionStateEnum.UP -> {
                if (this.mDirectionEnum == DirectionStateEnum.DOWN) return true
            }
            DirectionStateEnum.DOWN -> {
                if (this.mDirectionEnum == DirectionStateEnum.UP) return true
            }
            DirectionStateEnum.LEFT -> {
                if (this.mDirectionEnum == DirectionStateEnum.RIGHT) return true
            }
            DirectionStateEnum.RIGHT -> {
                if (this.mDirectionEnum == DirectionStateEnum.LEFT) return true
            }
        }
        return false
    }
  1. KeyView 中实现了 View.OnClickListener 接口用作点击监听,
    并将游戏状态与方向传递出去
 override fun onClick(v: View?) {
        val id = v?.id
        if (mGameState == GameStateEnum.START) {
            when (id) {
                R.id.keyView_btn_up -> {
                    mDirection = DirectionStateEnum.UP
                }
                R.id.keyView_btn_down -> {
                    mDirection = DirectionStateEnum.DOWN
                }
                R.id.keyView_btn_left -> {
                    mDirection = DirectionStateEnum.LEFT
                }
                R.id.keyView_btn_right -> {
                    mDirection = DirectionStateEnum.RIGHT
                }
            }
        }
        if (id == R.id.keyView_btn_switch) {
            if (mGameState == GameStateEnum.STOP || mGameState == GameStateEnum.PAUSE) {
                mGameState = GameStateEnum.START
                mBtnSwitch.setBackgroundResource(R.drawable.select_start)
            } else {
                mBtnSwitch.setBackgroundResource(R.drawable.select_pause)
                mGameState = GameStateEnum.PAUSE
            }
            //  更新游戏状态
            mIKeyData.gameState(mGameState)
        }
        //  将方向传到背景视图中
        mIKeyData.direction(mDirection)
    }

四、蛇吃食物长大

  1. BackgroundView 中的 onDraw 方法中判断是否吃到食物,吃到食物蛇身+1,食物坐标随机
			//  判断是否吃到食物
            if (mSnakeX[0] == mFoodX && mSnakeY[0] == mFoodY) {
                mFoodX = 25 * Random().nextInt(measuredWidth / 25).toFloat()
                mFoodY = 25 * Random().nextInt(measuredHeight / 25).toFloat()
                mSnakeLength++
            }

五、游戏失败

  1. 游戏失败蛇头坐标重新赋值,游戏状态设置 停止,蛇身长度设回默认长度,将游戏状态传递出去通知按键视图更改
	/**
     * 游戏失败
     */
    private fun gameOver() {
        mGameState = GameStateEnum.STOP
        mIKeyData.gameState(mGameState)
        mSnakeLength = 2
        mSnakeX[0] = 750f
        mSnakeY[0] = 500f
    }

小问题

代码写的比较菜,目前还有两个小问题

  • 当蛇头与食物碰撞时食物不消失但多碰撞几次就消失
  • 当蛇头超过屏幕边界时偶尔不会从正对方向出现

源码

完整代码:Github 源码

参考链接

自定义视图组件
Android invalidate()方法分析
[ 狂神说Java ] GUI编程入门到游戏实战

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-14 14:11:29  更:2021-08-14 14:13:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 22:40:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码