七. 使用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.hqwc.cn/news/278173.html

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

相关文章

华为数通——企业双出口冗余

目标&#xff1a;默认数据全部经过移动上网&#xff0c;联通低带宽。 R1 [ ]ip route-static 0.0.0.0 24 12.1.1.2 目的地址 掩码 下一条 [ ]ip route-static 0.0.0.0 24 13.1.1.3 preference 65 目的地址 掩码 下一条 设置优先级为65 R…

SaaS行业分析

文章目录 什么是SaaS ?SaaS的标准定义什么是软件即服务&#xff1f;SaaS与传统软件的区别 &#xff1f; SaaS行业分析你知道最赚钱的行业是什么&#xff1f;互联网带给企业的变化 SaaS与PaaS、IaaS的区别&#xff1f;IaaS&#xff08;Infrastructure as a Service&#xff09;…

关于“Python”的核心知识点整理大全22

目录 ​编辑 9.4.2 在一个模块中存储多个类 虽然同一个模块中的类之间应存在某种相关性&#xff0c;但可根据需要在一个模块中存储任意数量的 类。类Battery和ElectricCar都可帮助模拟汽车&#xff0c;因此下面将它们都加入模块car.py中&#xff1a; car.py my_electric_car…

【Linux】Redis 数据库安装教程(Ubuntu 22.04)

前言 Redis是一个开源的内存数据库&#xff0c;它可以用作键值存储、缓存和消息代理。它支持各种数据结构&#xff0c;包括字符串、哈希、列表、集合、有序集合等。Redis通常被用于构建高性能、可扩展的应用程序&#xff0c;特别是那些需要快速访问数据和实时数据处理的应用场…

高压电气是什么

高压电气 电工电气百科 文章目录 高压电气前言一、高压电气是什么二、高压电气的类别三、高压电气的作用原理总结前言 高压电气在电力系统中起着重要的作用,它能够将电能有效地输送和分配到各个用户,为社会和工业生产提供稳定可靠的电力供应。然而,高压电气系统也需要注意安…

【Qt开发流程】之网络编程:`HTTP`和`FTP`的高级网络操作

概述 Qt Network模块提供了可以编写TCP/IP客户端和服务器的类。它提供了较低层次的类&#xff0c;如QTcpSocket、QTcpServer和QUdpSocket&#xff0c;来代表低层次网络概念&#xff0c;以及高级层次类&#xff0c;如QNetworkRequest、QNetworkReply和QNetworkAccessManager&am…

什么是供应链安全及其工作原理?

6000公里长的丝绸之路将丝绸、谷物和其他货物从中国运送到帕尔米拉。尽管蒙古治下的和平保护丝绸之路免受海盗、强盗和内部盗窃的侵害&#xff0c;但商人仍然装备精良&#xff0c;并依赖于大型商队旅行和战略性放置的小型堡垒所提供的安全。 为什么供应链安全很重要&#xff1…

[蓝桥杯刷题]合并区间、最长不连续子序列、最长不重复数组长度

前言 ⭐Hello!这里是欧_aita的博客。 ⭐今日语录: 成功的关键在于对目标的持久追求。 ⭐个人主页&#xff1a;欧_aita ψ(._. )>⭐个人专栏&#xff1a; 数据结构与算法 数据库 文章目录 前言合并区间问题&#x1f4d5;现实应用大致思路代码实现代码讲解 最长不连续子序列&a…

机器学习支持向量机(SVM)

svm与logstic异同 svm支持向量机&#xff0c;因其英文名为support vector machine&#xff0c;故一般简称SVM&#xff0c;通俗来讲&#xff0c;它是一种二类分类模型&#xff0c;其基本模型定义为特征空间上的间隔最大的线性分类器&#xff0c;其学习策略便是间隔最大化&#x…

【深度学习】机器学习概述(二)优化算法之梯度下降法(批量BGD、随机SGD、小批量)

​ 文章目录 一、基本概念二、机器学习的三要素1. 模型a. 线性模型b. 非线性模型 2. 学习准则a. 损失函数b. 风险最小化准则 3. 优化机器学习问题转化成为一个最优化问题a. 参数与超参数b. 梯度下降法梯度下降法的迭代公式具体的参数更新公式学习率的选择 c. 随机梯度下降批量…

DevEco Studio 项目鸿蒙(HarmonyOS)资源引用(自定统和系统)

DevEco Studio 项目鸿蒙&#xff08;HarmonyOS&#xff09;资源引用&#xff08;自定统和系统&#xff09; 一、操作环境 操作系统: Windows 10 专业版 IDE:DevEco Studio 3.1 SDK:HarmonyOS 3.1 二、资源访问 HarmonyOS应用资源分为两类&#xff0c;一类是应用资源&…

调用第三方http接口 hutool工具类

1、引入依赖 <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.0.M2</version> </dependency>2、请求组装 String params"<BSXml>" " <MsgHeader>&…