安卓小游戏:贪吃蛇
安卓小游戏:贪吃蛇
前言
这个是通过自定义View实现小游戏的第二篇,实际上第一篇做起来麻烦点,后面的基本就是照葫芦画瓢了,只要设计下游戏逻辑就行了,技术上不难,想法比较重要。
需求
贪吃蛇,太经典了,小时候在诺基亚上玩了不知道多少回,游戏也很简单,就两个逻辑,一个是吃东西变长,一个是吃到自己死亡。核心思想如下:
- 1,载入配置,读取游戏信息及掩图
- 2,启动游戏控制逻辑
- 3,手势控制切换方向
效果图
这里就稍微演示了一下,就这速度,要演示到死亡估计得一分钟以上了,掩图用的比较low,勉强凑合。
代码
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 = 0const val DIR_RIGHT = 1const val DIR_DOWN = 2const 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: Intprivate var rowDelta: Int = 0private val colNumb: Intprivate 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.LTGRAYstrokeWidth = 1fstyle = Paint.Style.STROKEflags = Paint.ANTI_ALIAS_FLAGtextAlign = Paint.Align.CENTERtextSize = 30f}// 上一个触摸点X、Y的坐标private var mLastX = 0fprivate var mLastY = 0finit {// 读取配置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 nulltypedArray.recycle()}private fun drawableToBitmap(drawable: Drawable): Bitmap? {val w = drawable.intrinsicWidthval h = drawable.intrinsicHeightval config = Bitmap.Config.ARGB_8888val 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 / rowNumbcolDelta = w / colNumb// 开始游戏load()}// 加载private fun load() {mGameController.removeMessages(0)// 设置贪吃蛇的位置mHead.posX = colNumb / 2mHead.posY = rowNumb / 2mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}// 重新加载private fun reload() {mGameController.removeMessages(0)// 清空界面mFoodList.clear()mHead.posX = colNumb / 2mHead.posY = rowNumb / 2// 蛇体链表回收,让GC通过可达性分析去回收mHead.next = nullmGameController.isGameOver = falsemGameController.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? = mHeadwhile (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.xmLastY = event.y}MotionEvent.ACTION_MOVE -> {}MotionEvent.ACTION_UP -> {val lenX = event.x - mLastXval lenY = event.y - mLastYmHead.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 = falseoverride fun handleMessage(msg: Message) {mRef.get()?.let { gameView ->mSnarkCounter++if (mSnarkCounter == (SNARK_MOVE_TIME / GAME_FLUSH_TIME).toInt()) {// 移动蛇体var p: Snark? = gameView.mHeadvar dir = gameView.mHead.dirwhile (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.dirp.dir = dirdir = tempp = 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.showfood.blingCounter = 0}}// 食物被吃,添加一节蛇体到尾部if (food.posX == gameView.mHead.posX && food.posY == gameView.mHead.posY) {var x = gameView.mTail.posXvar 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按掩图的宽高来摆放,让掩图中心放在位置上,最后出来的效果就比较好看了。
相关文章:

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

CUDA中的图内存节点
CUDA中的图内存节点 文章目录CUDA中的图内存节点1. 简介2. 支持的架构和版本3. API基础知识3.1. 图节点 APIs3.2. 流捕获3.3. 在分配图之外访问和释放图内存3.4. cudaGraphInstantiateFlagAutoFreeOnLaunch4. 优化内存复用4.1. 解决图中的重用问题4.2. 物理内存管理和共享5. 性…...

你真的看好低代码开发吗?
低代码开发前景如何,大家真的看好低代码开发吗?之前有过很多关于低代码的内容,这篇就来梳理下国内外低代码开发平台发展现状及前景。 01、国外低代码开发平台现状 2014年,研究机构Forrester Research发表的报告中提到“面向客户…...

一篇带你MySQL运维
1. 日志 1.1 错误日志 错误日志是 MySQL 中 重要的日志之一,它记录了当 mysqld启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,建议首先查看此日志。 该日志是默认开启的&…...

《嵌入式 – GD32开发实战指南》第22章 SPI
开发环境: MDK:Keil 5.30 开发板:GD32F207I-EVAL MCU:GD32F207IK 22.1 SPI简介 SPI,是Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的…...

一个优质软件测试工程师的简历应该有的样子(答应我一定要收藏起来)
个人简历 基本信息 姓 名:xxx 性 别: 女 年 龄:24 现住 地址: 深圳 测试 经验:3年 学 历:本科 联系 电话:18xxxxxxxx 邮 箱:xxxxl163.com 求职意向 应聘岗位:软件…...

C++ 浅谈之 STL Deque
C 浅谈之 STL Deque HELLO,各位博友好,我是阿呆 🙈🙈🙈 这里是 C 浅谈系列,收录在专栏 C 语言中 😜😜😜 本系列阿呆将记录一些 C 语言重要的语法特性 🏃&a…...

Koa2-项目中的基本应用
文章目录安装配置koa2配置nodemon,热更新我们的项目中间件什么是中间件👻洋葱模型路由中间件连接数据库 - mysql后端允许跨域处理请求getpostputdelete后续会继续更新安装配置koa2 👻安装 koa2 npm i koa2 -s👻在package.json 配置,当然是在…...
Flask入门(2):配置
目录2.Flask配置2.1 直接写入主脚本2.2 系统环境变量2.3 单独的配置文件2.4 多个配置类2.5 Flask内置配置2.Flask配置 我们都知道,Flask应用程序肯定是需要各种各样的配置。来满足我们不同的需求的,这样可以使我们的应用程序更加灵活。比如可以根据需要…...

Linux--fork
一、fork入门知识 fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。可以简单地说fork()的作用就是创建一…...

计算机组成原理(一)
1.了解计算机硬件的发展和软件的发展历程; 硬件: 电子管时代(1946-1959):电子管、声汞延迟线、磁鼓 晶体管时代(1959-1964):晶体管、磁芯 中、小规模集成电路时代&#…...
【SpringBoot】实现Async异步任务
1. 环境准备 在 Spring Boot 入口类上配置 EnableAsync 注解开启异步处理。 创建任务抽象类 AbstractTask,并分别配置三个任务方法 doTaskOne(),doTaskTwo(),doTaskThree()。 public abstract class AbstractTask {private static Random r…...

Node =>Express学习
1.Express 能做什么 能快速构建web网站的服务器 或 Api接口的服务期 Web网站服务器,专门对外提供Web网页资源的服务器Api接口服务器:专门对外提供API接口的服务器 2.安装 在项目所处的目录中,运行以下命令,简装到项目中了 npm …...

QT基础入门【布局篇】消除控件之间的间隔
一、相关参数 layoutLeftMargin: layout内的布局距离边框左端的距离。 layoutTopMargin: layout内的布局距离边框顶端的距离。 layoutRightMargin: layout内的布局距离边框右端的距离。 layoutBottomMargin: layout内的布局距离边框底端的距离。 layoutHorizontalSpacing: layo…...

vue脚手架 element-ui spring boot 实现图片上传阿里云 并保存到数据库
一.阿里云 注册登陆就不讲了,登陆进去后如下操作 1. 进入对象存储OSS 创建一个新的Bucket 随后点击新建的bucket 2.去访问RAM 前往RAM控制台 3.去创建用户 4.创建密匙 5.随后返回RAM控制台 给用户增加权限,文件上传所需权限,需要带含有…...

【FPGA】Verilog:组合电路 | 3—8译码器 | 编码器 | 74LS148
前言:本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例:编码/译码器的应用 功能特性: 采用 Xilinx Artix-7 XC7A35T芯片 配置方式:USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度 存储器&…...

GLP-1类药物研发进展-销售数据-上市药品前景分析
据一项2021 年的报告发现,当 GLP-1 类似物用于治疗 2 型糖尿病时,全因死亡率降低了 12%,它们不仅降糖效果显著,同时还兼具减重、降压、改善血脂谱等作用。近几年,随着GLP-1R激动剂类药物市场规模不断增长,美…...
C++远程监控系统接收端- RevPlayMDIChildWnd.cpp
void CRevPlayWnd::InitMultiSock() { int RevBuf; int status; BOOL bFlag; CString ErrMsg; SOCKADDR_IN stLocalAddr; SOCKADDR_IN stDestAddr; SOCKET hNewSock; int RevLensizeof(RevBuf); //创建一个IP组播套接字 MultiSock W…...

QT之OpenGL深度测试
QT之OpenGL深度测试1. 深度测试概述1. 1 提前深度测试1.2 深度测试相关函数2. 深度测试精度2.1 深度冲突3. Demo4. 参考1. 深度测试概述 在OpenGL中深度测试(Depth Testing)是关闭的,此时在渲染图形时会产生一种现象后渲染的会把最先渲染的遮挡住。而在启用深度测试…...

用LCR测试仪测试无线充电系统中的线圈
宽阻抗范围用来表征电感和质量因数– 高精度 DCR 测量– 制造环节快速测量– 大量夹具可供选择智能终端上不断增加新功能,电池寿命成为用户最头痛的问题之一。相比便携式电源和电缆供电而言,无线充电技术因其方便性和多功能性获得了很大的关注࿰…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...

大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...

华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...

HDFS分布式存储 zookeeper
hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架,允许使用简单的变成模型跨计算机对大型集群进行分布式处理(1.海量的数据存储 2.海量数据的计算)Hadoop核心组件 hdfs(分布式文件存储系统)&a…...

毫米波雷达基础理论(3D+4D)
3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文: 一文入门汽车毫米波雷达基本原理 :https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...

Ubuntu Cursor升级成v1.0
0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开,快捷键也不好用,当看到 Cursor 升级后,还是蛮高兴的 1. 下载 Cursor 下载地址:https://www.cursor.com/cn/downloads 点击下载 Linux (x64) ,…...

Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...

Vue3 PC端 UI组件库我更推荐Naive UI
一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用,前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率,还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库(Naive UI、Element …...