七. 使用ts写一个贪吃蛇小游戏

之前学习了几篇的ts基础,今天我们就使用ts来完成一个贪吃蛇的小游戏。

游戏拆解

我们将我们的任务进行简单拆解分析。

  1. 首先我们应该有一个窗口,我们叫做屏幕。让蛇在里面移动,所有我们应该想到要设计一个大盒子当作地图。考虑到食物以及蛇的绘制我们可以使用canvas来实现。
  2. 其次我们还会在地图随机投放食物(这里还可以考虑食物该不该出现在蛇的身体节点上,本文不做考虑),所以我们大概率会创建一个类,这个类用来诞生一个随机方块,也就是食物。
  3. 接着我们考虑蛇,蛇在最开始应该也是一个随机方块,然后通过移动吃到食物长长。

代码实现

接下来我们根据上面的拆解做详细的需求梳理以及代码实现。
屏幕的实现
屏幕的实现是最为简单的,我们决定了使用canvas来绘制食物与蛇,那么我们直接创建一个canvas标签当作屏幕即可。

<canvas width="500" height="500"></canvas>

食物的实现

  • 接下来我们思考食物应该如何实现。既然决定在canvas绘制食物,那么最简单的方式就是把食物绘制会一个矩形。而矩形的绘制需要四个参数,分别是起始点坐标以及宽高,食物的宽高我们就设定为10,所以不确定的也就至于起始点的坐标了。这个坐标决定了他会出现在屏幕的哪个位置。

  • 还需要注意的是他的起始位置一定要在蛇的移动路径上,例如我们蛇的宽度为10,如果你的食物起始点在(11,11)这个坐标上,那么他就无法一次吃掉这个食物。
    在这里插入图片描述
    所以食物的坐标应该是10的倍数,且不能超过屏幕的边界。

  • 我们还要思考到食物被吃掉后应该就会自动消失,所以蛇这个类还应该有个清除方法可以清除掉自己。

代码展示

class Drop {width: number = 10height: number = 10x: numbery: numbercolor: stringconstructor(x: number = Math.floor(Math.random() * 49) * 10,y: number = Math.floor(Math.random() * 49) * 10,color: string = 'black') {this.color = colorthis.x = xthis.y = y}del() {const ctx: CanvasRenderingContext2D = canvasEle.getContext('2d')!ctx.clearRect(this.x, this.y, this.width, this.height)}
}

蛇的实现
蛇的实现相对来说就要复杂很多。

  • 首先我们思考蛇的身体应该是怎样的,为了他的灵活转向,最简单的方式就是他的身体应该是一个一个的矩形拼接起来的。既然如此,我们就可以直接使用上面的食物类,这也是我将上面类的名字叫做Drop而不是Food的原因,并且我在类里面添加了颜色进行蛇与食物的区别。
  • 接下来我们想到蛇既然是多个矩形拼接起来的,那么应该有一个容器来有序的存放这些矩形,所以我们定义个数组list来进行存放身体的数据。
    class Snake {list: Array<Drop>constructor() {this.list = [new Drop(250, 250, 'red')]}}
    
    我们让他在地图中心点生成,并使用红色进行与食物进行区分。
  • 接下来我们思考移动方法。蛇在移动的时候首先需要确认方法,我们可以设定一个方向属性,初始化的时候默认一个方向值。接着就是朝着方向移动,如何移动呢?如果简单的使用平移会发现蛇好像并不能灵活的转向,蛇的身体也不会发生弯曲。这个时候我们就需要换一个思路。既然蛇是有一个个的矩形组成,那么我们只需要控制里面的矩形就行了。当然也不是控制里面的矩形平移,而是进行矩形的增加与删除操作。想象一下,当蛇向上走一格(这里我们设定基础格子就是10 x 10单位的)是不是意味着我们将这个矩形的起点坐标的y值减去10,所以我们直接创建一个蛇头部盒子的起点坐标y值减去10的盒子,然后在直接删除蛇的最后一个盒子,是不是就可以看作移动了一格。
    在这里插入图片描述 当然我们也要考虑到吃到食物的情况,这种情况下我们是不需要删除尾部矩形的。然后我们的类就补充成这样
    class Snake {list: Array<Drop>direction: stringconstructor(direction: string = 'ArrowUp', speed: number = 100) {this.list = [new Drop(250, 250, 'red')]this.direction = direction}move() {let newHeader = JSON.parse(JSON.stringify(this.list[0]))const { x: newHeaderX, y: newHeaderY } = newHeaderconst { x: foodX, y: foodY } = foodlet isEatFood: boolean = falseif (newHeaderX === foodX && foodY === newHeaderY) {isEatFood = true}switch (this.direction) {case 'ArrowUp':newHeader.y -= 10breakcase 'ArrowDown':newHeader.y += 10breakcase 'ArrowLeft':newHeader.x -= 10breakcase 'ArrowRight':newHeader.x += 10break}this.addHead(newHeader)// 判断是否吃到食物if (isEatFood) {food.del()food = new Drop()renderDorp(food)} else {this.delFooter()}}addHead(dorp: Drop) {this.list.unshift(dorp)}delFooter() {const endDrop: Drop = this.list.pop()!const { x, y, width, height } = endDropconst ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!ctx.clearRect(x, y, width, height)}
    }
    
  • 我还还应该考虑一些特殊情况,例如移动到屏幕边缘,会不会吃到自己得身体,我们新增一个状态属性来判断他是否出局,所以我们继续填充这个方法
    class Snake {list: Array<Drop>direction: stringisOut: booleanconstructor(direction: string = 'ArrowUp', speed: number = 100) {this.list = [new Drop(250, 250, 'red')]this.direction = directionthis.boolean = false}move() {let newHeader = JSON.parse(JSON.stringify(this.list[0]))const { x: newHeaderX, y: newHeaderY } = newHeaderconst { x: foodX, y: foodY } = foodlet isEatFood: boolean = falseif (newHeaderX === foodX && foodY === newHeaderY) {isEatFood = true}if (this.direction) {}switch (this.direction) {case 'ArrowUp':newHeader.y -= 10breakcase 'ArrowDown':newHeader.y += 10breakcase 'ArrowLeft':newHeader.x -= 10breakcase 'ArrowRight':newHeader.x += 10break}// 是否吃到自己const isEatSelf = this.list.some(({ x, y }) => {if (x === newHeader.x && y === newHeader.y) {return true}})if (isEatSelf) {alert('吃到自己了!')return }this.addHead(newHeader)// 判断是否吃到食物if (isEatFood) {food.del()food = new Drop()renderDorp(food)} else {this.delFooter()}// 判断是否达到边界if (newHeaderX > 500 ||newHeaderY > 500 ||newHeaderX < 0 ||newHeaderY < 0) {return alert('撞墙了!')}renderDorp(this.list)}addHead(dorp: Drop) {this.list.unshift(dorp)}delFooter() {const endDrop: Drop = this.list.pop()!const { x, y, width, height } = endDropconst ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!ctx.clearRect(x, y, width, height)}
    }
    

渲染蛇与食物
我们写了食物与蛇的类,但是还没有真正在canvas上进行绘制。接下来我们使用ts的重载进行渲染类的绘制。

// 创建渲染函数
function renderDorp(dorp: Drop): void
function renderDorp(dorps: Array<Drop>): void
function renderDorp(dorps: Drop | Array<Drop>) {if (Array.isArray(dorps)) {dorps.forEach((element: Drop) => {const { x, y, width, height, color } = elementconst ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!ctx.fillStyle = colorctx.fillRect(x, y, width, height)})} else {const { x, y, width, height, color } = dorpsconst ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!ctx.fillStyle = colorctx.fillRect(x, y, width, height)}
}

键盘监听
我么使用方向键来控制蛇的移动,那么就需要监听键盘事件。需要注意的是,我们在身体长度为1的时候通常是可以随意移动的,比如直接从右往左或者从上到下,但是当身体长度不为1的时候,我们的有了头尾的定义,就不应该在随意的上下或者左右移动了。毕竟他不像火车一样前后都有一个车头。

window.addEventListener('keydown', function (e) {const { code } = econst keys: string[] = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']if (keys.includes(code)) {if (snake.list.length === 1) {snake.direction = codereturn}if (snake.direction === 'ArrowUp' && code === 'ArrowDown') {return}if (snake.direction === 'ArrowDown' && code === 'ArrowUp') {return}if (snake.direction === 'ArrowLeft' && code === 'ArrowRight') {return}if (snake.direction === 'ArrowRight' && code === 'ArrowLeft') {return}snake.direction = code}
})

最后补充完整的实现代码

const canvasEle = document.querySelector('canvas')!
let food: Drop
let snake: Snake
class Drop {width: number = 10height: number = 10x: numbery: numbercolor: stringconstructor(x: number = Math.floor(Math.random() * 49) * 10,y: number = Math.floor(Math.random() * 49) * 10,color: string = 'black') {this.color = colorthis.x = xthis.y = y}del() {const ctx: CanvasRenderingContext2D = canvasEle.getContext('2d')!ctx.clearRect(this.x, this.y, this.width, this.height)}
}class Snake {list: Array<Drop>direction: stringisOut: booleanconstructor(direction: string = 'ArrowUp', speed: number = 100) {this.list = [new Drop(250, 250, 'red')]this.direction = directionthis.isOut = false}move() {let newHeader = JSON.parse(JSON.stringify(this.list[0]))const { x: newHeaderX, y: newHeaderY } = newHeaderconst { x: foodX, y: foodY } = foodlet isEatFood: boolean = falseif (newHeaderX === foodX && foodY === newHeaderY) {isEatFood = true}if (this.direction) {}switch (this.direction) {case 'ArrowUp':newHeader.y -= 10breakcase 'ArrowDown':newHeader.y += 10breakcase 'ArrowLeft':newHeader.x -= 10breakcase 'ArrowRight':newHeader.x += 10break}// 是否吃到自己const isEatSelf = this.list.some(({ x, y }) => {if (x === newHeader.x && y === newHeader.y) {return true}})if (isEatSelf) {this.isOut = truereturn alert('吃到自己了!')}this.addHead(newHeader)// 判断是否吃到食物if (isEatFood) {food.del()food = new Drop()renderDorp(food)} else {this.delFooter()}// 判断是否达到边界if (newHeaderX > 500 ||newHeaderY > 500 ||newHeaderX < 0 ||newHeaderY < 0) {this.isOut = truereturn alert('撞墙了!')}renderDorp(this.list)}addHead(dorp: Drop) {this.list.unshift(dorp)}delFooter() {const endDrop: Drop = this.list.pop()!const { x, y, width, height } = endDropconst ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!ctx.clearRect(x, y, width, height)}
}// 创建渲染函数
function renderDorp(dorp: Drop): void
function renderDorp(dorps: Array<Drop>): void
function renderDorp(dorps: Drop | Array<Drop>) {if (Array.isArray(dorps)) {dorps.forEach((element: Drop) => {const { x, y, width, height, color } = elementconst ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!ctx.fillStyle = colorctx.fillRect(x, y, width, height)})} else {const { x, y, width, height, color } = dorpsconst ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!ctx.fillStyle = colorctx.fillRect(x, y, width, height)}
};(function () {food = new Drop()snake = new Snake()renderDorp(food)let timer = setInterval(() => {snake.move()if (snake.isOut) {clearInterval(timer)}}, 100)window.addEventListener('keydown', function (e) {const { code } = econst keys: string[] = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']if (keys.includes(code)) {if (snake.list.length !== 1) {if (snake.direction === 'ArrowUp' && code === 'ArrowDown') {return}if (snake.direction === 'ArrowDown' && code === 'ArrowUp') {return}if (snake.direction === 'ArrowLeft' && code === 'ArrowRight') {return}if (snake.direction === 'ArrowRight' && code === 'ArrowLeft') {return}}snake.direction = code}})
})()

这只是一个简单版贪吃蛇效果,没有经过严格测试,肯定会有bug,希望可以留言交流!
再推一个自己插件element-ui的拓展组件库,还在不断完善,希望大家支持

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

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

相关文章

【LeetCode刷题笔记(7-1)】【Python】【四数之和】【哈希表】【中等】

文章目录 四数之和题目描述示例 1示例 2提示解决方案1&#xff1a;【四层遍历查找】解决方案2&#xff1a;【哈希表】【三层遍历】 结束语 四数之和 四数之和 题目描述 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件…

服务器一直掉线怎么回事?

随着网络的高速发展&#xff0c;不管是网站还是游戏&#xff0c;如果遇到服务器卡顿的情况&#xff0c;会造成用户访问网站或进游戏&#xff0c;网站页面长时间无法打开&#xff0c;游戏页面运行卡顿&#xff0c;这样就很容易会造成用户的流失&#xff0c;从而导致业务亏损极大…

可视化数据监控大屏网页界面,数据大屏模版PS资料(免费UI源文件)

数据大屏模板在大数据领域被广泛应用&#xff0c;其优势在于能够将复杂的数据通过图形、图表等方式呈现出来&#xff0c;使数据更易于理解。数据大屏模板可以用来进行数据分析。通过对数据的比较、趋势分析、异常检测等&#xff0c;可以发现数据中的规律和问题&#xff0c;为决…

Appium知多少

Appium我想大家都不陌生&#xff0c;这是主流的移动自动化工具&#xff0c;但你对它真的了解么&#xff1f;为什么很多同学搭建环境时碰到各种问题也而不知该如何解决。 appium为什么英语词典查不到中文含义&#xff1f; appium是一个合成词&#xff0c;分别取自“application…

51单片机项目(21)——基于51单片机的音乐流水灯

1.功能描述 本次所做设计&#xff0c;有流水灯的功能&#xff0c;使用了16颗LED灯&#xff0c;同时还可以播放音乐。单片机存储了三首音乐&#xff0c;通过声音检测模块触发其进行切换。&#xff08;仿真图里面使用一个按键来代码声音检测模块&#xff09; 此外&#xff0c;还…

四十七、Redis分片集群

目录 一、分片集群结构 二、散列插槽 1、Redis如何判断某个key应该在哪个实例&#xff1f; 2、如何将同一类数据固定的保存在同一个Redis实例&#xff1f; 三、集群伸缩 四、故障转移 1、当集群中有一个master宕机时 &#xff08;1&#xff09;自动转移 &#xff08;2&…

ORACLE 如何单机转RAC

生产库停监听 [oracleprimary backup1]$ lsnrctl stop 生产库启停数据库 shutdown immediate Startup mount 生产库全库备份&#xff1a; rmanbackup.sh export ORACLE_BASE/oracle/app export ORACLE_HOME/oracle/app/product/11.2.0/db_1 export ORACLE_SIDtest …

Linux 常用解压命令tar和zip(详细篇)

# tar 入门 介绍 在Linux平台上,tar 命令是主要的归档实用程序。了解各种 tar 命令选项将帮助您掌握归档文件操作。tar 代表磁带存档。 语法 tar [-ABcdgGhiklmMoOpPrRsStuUvwWxzZ][-C <目的目录>][-f <备份文件>][--delete][--totals][文件或目录...] 以前 un…

使用opencv的Laplacian算子实现图像边缘检测

1 边缘检测介绍 图像边缘检测技术是图像处理和计算机视觉等领域最基本的问题&#xff0c;也是经典的技术难题之一。如何快速、精确地提取图像边缘信息&#xff0c;一直是国内外的研究热点&#xff0c;同时边缘的检测也是图像处理中的一个难题。早期的经典算法包括边缘算子方法…

Linux的文件系统 内核结构

Linux的文件系统 Q1&#xff1a;什么是文件系统&#xff1f; A&#xff1a;在学术的角度下&#xff0c;文件系统指“操作系统用于明确存储设备组织文件的方法”&#xff0c;是“文件管理系统”的简称&#xff0c;本质也是代码&#xff0c;一段程序 Q2&#xff1a;文件系统&…

Java带缓存的单向链表-线程安全

开发原因 ArrayList太重&#xff0c;线程还不安全 在一些队列处理的时候&#xff0c;ArrayList有点麻烦&#xff0c;还比较占内存&#xff0c;还没缓存&#xff0c;又得再加个缓存队列 所以就开发了这个 轻量级&#xff0c;线程安全&#xff0c;带缓存&#xff01;nice!! 源码…

Elasticsearch:相关性工作台 - BM25 及 ELSER 的相关性比较

我们知道 Elastics Learned Sparse EncoderR (ELSER) 可以被用来做语义搜索。它是一个 out-of-domain 的语义搜索模型。无需训练&#xff0c;我们就可以得到很好的相关性。有关 ELSER 的更多知识&#xff0c;请参考文章 “Elastic Learned Sparse Encoder 简介&#xff1a;Elas…

Mac下ERROR: Cannot connect to the Docker daemon

解决Mac下ERROR: Cannot connect to the Docker daemon at unix:///Users/qq/.orbstack/run/docker.sock. Is the docker daemon running? 在Mac系统的中, 如果实际已经安装docker并且已经启动了. 但执行 docker info 时 报错: ERROR: Cannot connect to the Docker daemon …

SpringMVC映射请求数据

1、获取超链接的参数和值 <a href"vote/vote01?namelove">获取超链接的参数</a> //获取超链接的参数RequestMapping(value "/vote01")public String vote01(RequestParam(value "name", requiredfalse) String name){System.out…

4-Docker命令之docker cp

1.docker cp介绍 docker cp命令是用于在本地宿主机文件系统与docker容器(状态:运行中/停止)之间复制文件或文件夹 2.docker cp用法 docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH [root@centos79 ~]# docker …

如何使用PHP进行数据加密和解密?

在 PHP 中&#xff0c;你可以使用加密算法和相关的扩展库来进行数据加密和解密。以下是使用 PHP 进行数据加密和解密的基本示例&#xff1a; 使用 OpenSSL 扩展进行加密和解密&#xff1a; 加密&#xff1a; <?php $data "Hello, World!"; $key openssl_rando…

jenkins-Generic Webhook Trigger指定分支构建

文章目录 1 需求分析1.1 关键词 : 2、webhooks 是什么&#xff1f;3、配置步骤3.1 github 里需要的仓库配置&#xff1a;3.2 jenkins 的主要配置3.3 option filter配置用于匹配目标分支 实现指定分支构建 1 需求分析 一个项目一般会开多个分支进行开发&#xff0c;测试&#x…

网络入门---可变参数原理和日志模拟实现

目录标题 前言有关函数的几个性质介绍可变参数的用法介绍可变参数的一个注意事项可变参数的底层原理va_listva_endva_startva_arg_INTSIZEOF 可变参数的注意事项日志的实现日志的测试 前言 在上一篇文章中我们介绍了TCP协议有关的函数&#xff0c;大致就是服务端先通过listen函…

oracle 如何把数据库 date 日期格式 的数据 改成 2021-01-27

如果您要将日期"27-12月-29"更改为"2021-01-27"格式&#xff0c;您可以使用Oracle的日期格式化函数和字符串替换函数来实现。 以下是一个示例SQL语句&#xff0c;将日期"27-12月-29"更改为"2021-01-27"格式&#xff1a; sql UPDATE…

Springboot的火车票订票系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; Springboot的火车票订票系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#…