安卓小游戏:贪吃蛇

news2025/5/25 20:48:22

安卓小游戏:贪吃蛇

前言

这个是通过自定义View实现小游戏的第二篇,实际上第一篇做起来麻烦点,后面的基本就是照葫芦画瓢了,只要设计下游戏逻辑就行了,技术上不难,想法比较重要。

需求

贪吃蛇,太经典了,小时候在诺基亚上玩了不知道多少回,游戏也很简单,就两个逻辑,一个是吃东西变长,一个是吃到自己死亡。核心思想如下:

  • 1,载入配置,读取游戏信息及掩图
  • 2,启动游戏控制逻辑
  • 3,手势控制切换方向

效果图

这里就稍微演示了一下,就这速度,要演示到死亡估计得一分钟以上了,掩图用的比较low,勉强凑合。

snark

代码

import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.silencefly96.module_views.R
import java.lang.ref.WeakReference
import kotlin.math.abs

/**
 * 贪吃蛇游戏view
 *
 * @author silence
 * @date 2023-02-07
 */
class SnarkGameView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attributeSet, defStyleAttr) {
    companion object{
        // 四个方向
        const val DIR_UP = 0
        const val DIR_RIGHT = 1
        const val DIR_DOWN = 2
        const val DIR_LEFT = 3

        // 游戏更新间隔,一秒5次
        const val GAME_FLUSH_TIME = 200L
        // 蛇体移动频率
        const val SNARK_MOVE_TIME = 600L
        // 食物添加间隔时间
        const val FOOD_ADD_TIME = 5000L
        // 食物存活时间
        const val FOOD_ALIVE_TIME = 10000L
        // 食物闪烁时间,要比存货时间长
        const val FOOD_BLING_TIME = 3000L
        // 食物闪烁间隔
        const val FOOD_BLING_FREQ = 300L
    }

    // 屏幕划分数量及等分长度
    private val rowNumb: Int
    private var rowDelta: Int = 0
    private val colNumb: Int
    private var colDelta: Int = 0

    // 节点掩图
    private val mNodeMask: Bitmap?

    // 头节点
    private val mHead = Snark(0, 0, DIR_DOWN, null)

    // 尾节点
    private var mTail = mHead

    // 食物数组
    private val mFoodList = ArrayList<Food>()

    // 游戏控制器
    private val mGameController = GameController(this)

    // 画笔
    private val mPaint = Paint().apply {
        color = Color.LTGRAY
        strokeWidth = 1f
        style = Paint.Style.STROKE
        flags = Paint.ANTI_ALIAS_FLAG
        textAlign = Paint.Align.CENTER
        textSize = 30f
    }

    // 上一个触摸点X、Y的坐标
    private var mLastX = 0f
    private var mLastY = 0f

    init {
        // 读取配置
        val typedArray =
            context.obtainStyledAttributes(attributeSet, R.styleable.SnarkGameView)

        // 横竖划分
        rowNumb = typedArray.getInteger(R.styleable.SnarkGameView_rowNumb, 30)
        colNumb = typedArray.getInteger(R.styleable.SnarkGameView_colNumb, 20)

        // 节点掩图
        val drawable = typedArray.getDrawable(R.styleable.SnarkGameView_node)
        mNodeMask = if (drawable != null) drawableToBitmap(drawable) else null

        typedArray.recycle()
    }

    private fun drawableToBitmap(drawable: Drawable): Bitmap? {
        val w = drawable.intrinsicWidth
        val h = drawable.intrinsicHeight
        val config = Bitmap.Config.ARGB_8888
        val bitmap = Bitmap.createBitmap(w, h, config)
        //注意,下面三行代码要用到,否则在View或者SurfaceView里的canvas.drawBitmap会看不到图
        val canvas = Canvas(bitmap)
        drawable.setBounds(0, 0, w, h)
        drawable.draw(canvas)
        return bitmap
    }

    // 完成测量开始游戏
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        rowDelta = h / rowNumb
        colDelta = w / colNumb
        // 开始游戏
        load()
    }

    // 加载
    private fun load() {
        mGameController.removeMessages(0)
        // 设置贪吃蛇的位置
        mHead.posX = colNumb / 2
        mHead.posY = rowNumb / 2
        mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)
    }

    // 重新加载
    private fun reload() {
        mGameController.removeMessages(0)
        // 清空界面
        mFoodList.clear()
        mHead.posX = colNumb / 2
        mHead.posY = rowNumb / 2
        // 蛇体链表回收,让GC通过可达性分析去回收
        mHead.next = null
        mGameController.isGameOver = false
        mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
            getDefaultSize(0, heightMeasureSpec))
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // 绘制网格
        for (i in 0..rowNumb) {
            canvas.drawLine(0f, rowDelta * i.toFloat(),
                width.toFloat(), rowDelta * i.toFloat(), mPaint)
        }
        for (i in 0..colNumb) {
            canvas.drawLine(colDelta * i.toFloat(), 0f,
                colDelta * i.toFloat(), height.toFloat(), mPaint)
        }

        // 绘制食物
        for (food in mFoodList) {
            if (food.show) canvas.drawBitmap(mNodeMask!!,
                (food.posX + 0.5f) * colDelta - mNodeMask.width / 2,
                (food.posY + 0.5f) * rowDelta - mNodeMask.height / 2, mPaint)
        }

        // 绘制蛇体
        var p: Snark? = mHead
        while (p != null) {
            canvas.drawBitmap(mNodeMask!!,
                (p.posX + 0.5f) * colDelta - mNodeMask.width / 2,
                (p.posY + 0.5f) * rowDelta - mNodeMask.height / 2, mPaint)
            p = p.next
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when(event.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastX = event.x
                mLastY = event.y
            }
            MotionEvent.ACTION_MOVE -> {}
            MotionEvent.ACTION_UP -> {
                val lenX = event.x - mLastX
                val lenY = event.y - mLastY

                mHead.dir = if (abs(lenX) > abs(lenY)) {
                    if (lenX >= 0) DIR_RIGHT else DIR_LEFT
                }else {
                    if (lenY >= 0) DIR_DOWN else DIR_UP
                }

                invalidate()
            }
        }
        return true
    }

    private fun gameOver() {
        AlertDialog.Builder(context)
            .setTitle("继续游戏")
            .setMessage("请点击确认继续游戏")
            .setPositiveButton("确认") { _, _ -> reload() }
            .setNegativeButton("取消", null)
            .create()
            .show()
    }

    // kotlin自动编译为Java静态类,控件引用使用弱引用
    class GameController(view: SnarkGameView): Handler(Looper.getMainLooper()){
        // 控件引用
        private val mRef: WeakReference<SnarkGameView> = WeakReference(view)
        // 蛇体移动控制
        private var mSnarkCounter = 0
        // 食物闪烁控制
        private var mFoodCounter = 0
        // 游戏结束标志
        internal var isGameOver = false

        override fun handleMessage(msg: Message) {
            mRef.get()?.let { gameView ->
                mSnarkCounter++
                if (mSnarkCounter == (SNARK_MOVE_TIME / GAME_FLUSH_TIME).toInt()) {
                    // 移动蛇体
                    var p: Snark? = gameView.mHead
                    var dir = gameView.mHead.dir
                    while (p != null) {
                        // 移动逻辑,会穿过屏幕边界
                        when(p.dir) {
                            DIR_UP -> {
                                p.posY--
                                if (p.posY < 0)  {
                                    p.posY = gameView.rowNumb - 1
                                }
                            }
                            DIR_RIGHT -> {
                                p.posX++
                                if (p.posX >= gameView.colNumb) {
                                    p.posX = 0
                                }
                            }
                            DIR_DOWN -> {
                                p.posY++
                                if (p.posY >= gameView.rowNumb)  {
                                    p.posY = 0
                                }
                            }
                            DIR_LEFT -> {
                                p.posX--
                                if (p.posX < 0) {
                                    p.posX = gameView.colNumb - 1
                                }
                            }
                        }

                        // 死亡逻辑,蛇头撞到身体了
                        if (p != gameView.mHead &&
                            p.posX == gameView.mHead.posX && p.posY == gameView.mHead.posY) {
                            isGameOver = true
                        }

                        // 移动修改方向为上一节的方
                        val temp = p.dir
                        p.dir = dir
                        dir = temp

                        p = p.next
                    }

                    mSnarkCounter = 0
                }

                // 食物控制
                val iterator = gameView.mFoodList.iterator()
                while (iterator.hasNext()) {
                    val food = iterator.next()
                    food.counter++

                    // 食物消失
                    if (food.counter >= (FOOD_ALIVE_TIME / GAME_FLUSH_TIME)) {
                        iterator.remove()
                        continue
                    }

                    // 食物闪烁
                    if (food.counter >= ((FOOD_ALIVE_TIME - FOOD_BLING_TIME) / GAME_FLUSH_TIME)) {
                        food.blingCounter++
                        if (food.blingCounter >= (FOOD_BLING_FREQ / GAME_FLUSH_TIME)) {
                            food.show = !food.show
                            food.blingCounter = 0
                        }
                    }

                    // 食物被吃,添加一节蛇体到尾部
                    if (food.posX == gameView.mHead.posX && food.posY == gameView.mHead.posY) {
                        var x = gameView.mTail.posX
                        var y = gameView.mTail.posY
                        // 在尾部添加
                        when(gameView.mTail.dir) {
                            DIR_UP -> y++
                            DIR_RIGHT -> x--
                            DIR_DOWN -> y--
                            DIR_LEFT -> x++
                        }
                        gameView.mTail.next = Snark(x, y, gameView.mTail.dir,null)
                        gameView.mTail = gameView.mTail.next!!

                        // 移除被吃食物
                        iterator.remove()
                    }
                }

                mFoodCounter++
                if (mFoodCounter == (FOOD_ADD_TIME / GAME_FLUSH_TIME).toInt()) {
                    // 生成食物
                    val x = (Math.random() * gameView.colNumb).toInt()
                    val y = (Math.random() * gameView.rowNumb).toInt()
                    gameView.mFoodList.add(Food(x, y, 0, 0,true))
                    mFoodCounter = 0
                }

                // 循环发送消息,刷新页面
                gameView.invalidate()
                if (!isGameOver) {
                    gameView.mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)
                }else {
                    gameView.gameOver()
                }
            }
        }
    }

    data class Food(var posX: Int, var posY: Int, var counter: Int, var blingCounter: Int, var show: Boolean)
    data class Snark(var posX: Int, var posY: Int, var dir: Int, var next: Snark? = null)
}

对应style配置

res -> values -> snark_game_view_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name ="SnarkGameView">
        <attr name="rowNumb" format="integer"/>
        <attr name="colNumb" format="integer"/>
        <attr name="node" format="reference"/>
    </declare-styleable>
</resources>

蛇体掩图也给一下吧,当然你找点好看的图片代替下会更好!

res -> drawable -> ic_node.xml

<vector android:height="24dp" android:tint="#6F6A6A"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
</vector>

主要问题

下面简单讲讲吧,大部分还是和上一篇的飞机大战类似,这里就讲讲不一样的或者做点补充吧。

资源加载

资源加载就是从styleable配置里面读取设置,这里贪吃蛇是完全网格化的游戏,这里读取了行数和列数,后面把屏幕等分,获取到了行高和列长,转换逻辑得注意下。蛇的掩图逻辑和上一篇博文一致,不细说了。

蛇体移动

蛇体的移动实际就要有一个方向,这里每一截蛇都是一个节点,构成了一个链表,每个节点的方向都是移动前上一个节点的方向,这样移动起来就有效果了。当然方向的获取也简单,在onTouchEvent中监听DOWN和UP事件就行了,比较起点和终点,看看往哪边滑动的,更改蛇头方向就行,后面会向后传递。

这里还有个穿墙的问题要更改下,从一边出去会从另一边出来,这里改下节点的方向和位置就行了。

食物闪烁

食物的控制是通过counter对游戏刷新频率计数实现的,超过计数数量就移除食物,到达闪烁时间food内部的blingCounter进行计数,在我的设置里是0.5秒反转一下show,这样就出来了闪烁效果。

位置摆放问题

这里用的坐标都是中心坐标,所以和掩图的宽高有关,在生成位置的时候按中心位置去生成,在onDraw按掩图的宽高来摆放,让掩图中心放在位置上,最后出来的效果就比较好看了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/335039.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决:ChatGPT too many requests in 1 hour.Try again later 怎么办?OpenAI 提示

ChatGPT 提示&#xff1a; Too many requests in 1 hour. Try again later. 如下图&#xff0c;我多次访问也出现同样的问题。中文意思是太多的请求数量在当前 1 个小时内&#xff0c;请稍后重试。那怎么办&#xff1f;怎么解决&#xff1f; 一、问题现象 我基本试了半个小时&…

二分查找基本原理

二分查找基本原理1.二分查找1.1 基本概念1.2 二分查找查找步骤1.2.1 中间索引不能整除&#xff0c;取整数作为中间索引1.2.2 索引不能整除&#xff0c;整数1作为中间索引1.3 二分查找大O记法表示2. 二分查找代码实现1.二分查找 1.1 基本概念 二分法(折半查找&#xff09;是一…

【第37天】斐波那契数列与爬楼梯 | 迭代的鼻祖,递推与记忆化

本文已收录于专栏&#x1f338;《Java入门一百例》&#x1f338;学习指引序、专栏前言一、递推与记忆化二、【例题1】1、题目描述2、解题思路3、模板代码4、代码解析5.原题链接三、【例题1】1、题目描述2.解题思路3、模板代码4、代码解析5、原题链接三、推荐专栏四、课后习题序…

数据库原理及应用基础知识点

数据库原理基础知识点大全数据库原理及应用1、数据库系统概述1.1 基本概念1.2 数据模型1.3 数据库系统的结构2、实体 -- 联系模型2.1 基本概念2.2 实体-联系图2.3 弱实体集3、关系数据模型3.1 关系数据库的结构3.2 从ER模型到关系模型3.3 关系操作、完整性约束、关系代数4、关系…

Nacos安装配置(二)

目录 一、概述 二、Nacos 安装 A&#xff09;Debian11 1&#xff09;软件环境 2&#xff09;下载源码或者安装包 3&#xff09;mysql配置 4&#xff09;启动服务器 B) Debian11 1) 安装JDK 2) 安装Maven 3) 安装Nacos2 4) 修改访问参数&#xff08;/conf/applicati…

GEE:下载研究区同一天的Landsat影像

本文记录了下载Landsat逐日数据的代码,包装成了函数。直接输入数据集合就可以直接使用。 并在下文中应用了该函数,以下载2022年逐日地表温度LST数据,和下载研究区多波段影像为例。 结果如图所示 文章目录 一、调用方法二、Landsat 逐日下载函数三、应用示例1——下载2022年研…

RNN循环神经网络原理理解

一、基础 正常的神经网络 一般情况下&#xff0c;输入层提供数据&#xff0c;全连接进入隐藏层&#xff0c;隐藏层可以是多层&#xff0c;层与层之间是全连接&#xff0c;最后输出到输出层&#xff1b;通过不断的调整权重参数和偏置参数实现训练的效果。深度学习的网络都是水…

【安全知识】——对Linux密码文件的处理

作者名&#xff1a;白昼安全主页面链接&#xff1a; 主页传送门创作初心&#xff1a; 一切为了她座右铭&#xff1a; 不要让时代的悲哀成为你的悲哀专研方向&#xff1a; web安全&#xff0c;后渗透技术每日emo&#xff1a;他既乐观又悲观&#xff0c;生活也一无是处昨天在挖掘…

mycat2使用

安装部署下载1&#xff1a;mycat2-install-template-1.21.zip下载2&#xff1a;mycat2-1.21-release-jar-with-dependencies.jar解压mycat2-install-template-1.21.zipunzip mycat2-install-template-1.21.zip把mycat2-1.21-release-jar-with-dependencies.jar放在mycat/lib中修…

神码ospfv3配置.docx

一.配置各设备的ip地址 sw1(config)#ipv6 enable sw1(config)#vlan 1000 sw1(config-vlan1000)#swi int eth1/0/3 Set the port Ethernet1/0/3 access vlan 1000 successfully sw1(config)#int vlan 1000 sw1(config-if-vlan1000)#ipv6 address aa::aa/64 sw1(config-if-vla…

分享微信商城小程序搭建步骤_微信公众号商城小程序怎么做

如何搭建好一个微信商城&#xff1f;这三个功能要会用&#xff01; 1.定期低价秒杀&#xff0c;提高商城流量 除了通过私域流量裂变&#xff0c;低价秒杀是为商城引流提高打开率的良好手段。 以不同节日作为嘘头&#xff0c;在情人节、38妇女节、中秋国庆、七夕节等日子&…

Node=>Express中间件 学习3

1.概念&#xff1a; 例&#xff1a;在处理污水的时候&#xff0c;一般都要经过三个处理环节&#xff0c;从而保证处理过后的废水&#xff0c;达到排放标准 处理污水的这三个中间处理环节&#xff0c;就可以叫中间件 2.中间件调用流程 当一个请求到达Express的服务器之后&#x…

大数据---Hadoop安装jdk简易版

编写自动安装的shell脚本 完整流程: 大数据—Hadoop安装教程&#xff08;一&#xff09; 文章目录编写自动安装的shell脚本上传压缩包编写shell脚本vim autoinstall.sh解压更名添加环境运行上传压缩包 在opt目录下创建连个目录install和soft 将压缩包上传到install目录下 …

Google杀入AI聊天机器人领域,暴跌千亿?错哪了?

大家好&#xff0c;ChatGPT 现在被大家玩坏了&#xff0c;甚至在用户的不断逼问之下&#xff0c;露出了鸡脚&#xff0c;原来 ChatGPT 也是小黑子ChatGPT 太火了&#xff0c;火的谷歌都坐不住了。为了应对爆火的ChatGPT&#xff0c;谷歌推出的Bard&#xff0c;但是谷歌翻车了&a…

Python Web 框架要点

Python Web 框架要点 1. Web应用程序处理流程 2. Web程序框架的意义 用于搭建Web应用程序免去不同Web应用相同代码部分的重复编写&#xff0c;只需关心Web应用核心的业务逻辑实现 3. Web应用程序的本质 接收并解析HTTP请求&#xff0c;获取具体的请求信息处理本次HTTP请求&a…

三大基础排序算法——冒泡排序、选择排序、插入排序

目录前言一、排序简介二、冒泡排序三、选择排序四、插入排序五、对比References前言 在此之前&#xff0c;我们已经介绍了十大排序算法中的&#xff1a;归并排序、快速排序、堆排序&#xff08;还不知道的小伙伴们可以参考我的 「数据结构与算法」 专栏&#xff09;&#xff0…

【内网安全】——数据库提权姿势

作者名&#xff1a;白昼安全主页面链接&#xff1a;主页传送门创作初心&#xff1a; 一切为了她座右铭&#xff1a; 不要让时代的悲哀成为你的悲哀专研方向&#xff1a; web安全&#xff0c;后渗透技术每日emo&#xff1a; 在哪能找到解救我的办法模拟环境我们拿到了一个普通用…

java开发-用户注册-MD5工具加密密码

加密方式介绍 对称加密&#xff1a;加密和解密使用的相同的密钥&#xff0c;常见的对称加密算法有:DES、3DES非对称加密&#xff1a;加密和解密使用的密钥不同&#xff0c;常见的非对称加密算法有:RSA 加密&#xff1a;使用私钥加密解密&#xff1a;使用公钥解密 消息摘要: 消…

vcs仿真教程

VCS是在linux下面用来进行仿真看波形的工具&#xff0c;类似于windows下面的modelsim以及questasim等工具&#xff0c;以及quartus、vivado仿真的操作。 1.vcs的基本指令 vcs的常见指令后缀 sim常见指令 2.使用vcs的实例 采用的是全加器的官方教程&#xff0c;首先介绍不使用…

Netty(IO模型/零拷贝技术/IO复用之select、poll、epoll模型)

目录 IO模型 阻塞IO和非阻塞IO 阻塞IO 非阻塞IO IO复用模型 异步IO mmap IO复用之select、poll、epoll模型 select poll epoll IO模型 阻塞IO和非阻塞IO 阻塞IO 所谓阻塞IO就是当应用B发起读取数据申请时&#xff0c;在内核数据没有准备好之前&#xff0c;应用…