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

news2024/11/16 13:49:23

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

游戏拆解

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

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

代码实现

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

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

食物的实现

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

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

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

代码展示

class Drop {
  width: number = 10
  height: number = 10
  x: number
  y: number
  color: string
  constructor(
    x: number = Math.floor(Math.random() * 49) * 10,
    y: number = Math.floor(Math.random() * 49) * 10,
    color: string = 'black'
  ) {
    this.color = color
    this.x = x
    this.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: string
      constructor(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 } = newHeader
        const { x: foodX, y: foodY } = food
        let isEatFood: boolean = false
        if (newHeaderX === foodX && foodY === newHeaderY) {
          isEatFood = true
        }
        switch (this.direction) {
          case 'ArrowUp':
            newHeader.y -= 10
            break
          case 'ArrowDown':
            newHeader.y += 10
            break
          case 'ArrowLeft':
            newHeader.x -= 10
            break
          case 'ArrowRight':
            newHeader.x += 10
            break
        }
        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 } = endDrop
        const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
        ctx.clearRect(x, y, width, height)
      }
    }
    
  • 我还还应该考虑一些特殊情况,例如移动到屏幕边缘,会不会吃到自己得身体,我们新增一个状态属性来判断他是否出局,所以我们继续填充这个方法
    class Snake {
      list: Array<Drop>
      direction: string
      isOut: boolean
      constructor(direction: string = 'ArrowUp', speed: number = 100) {
        this.list = [new Drop(250, 250, 'red')]
        this.direction = direction
        this.boolean = false
      }
      move() {
        let newHeader = JSON.parse(JSON.stringify(this.list[0]))
        const { x: newHeaderX, y: newHeaderY } = newHeader
        const { x: foodX, y: foodY } = food
        let isEatFood: boolean = false
        if (newHeaderX === foodX && foodY === newHeaderY) {
          isEatFood = true
        }
        if (this.direction) {
        }
        switch (this.direction) {
          case 'ArrowUp':
            newHeader.y -= 10
            break
          case 'ArrowDown':
            newHeader.y += 10
            break
          case 'ArrowLeft':
            newHeader.x -= 10
            break
          case 'ArrowRight':
            newHeader.x += 10
            break
        }
        // 是否吃到自己
        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 } = endDrop
        const 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 } = element
      const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!

      ctx.fillStyle = color
      ctx.fillRect(x, y, width, height)
    })
  } else {
    const { x, y, width, height, color } = dorps
    const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
    ctx.fillStyle = color
    ctx.fillRect(x, y, width, height)
  }
}

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

window.addEventListener('keydown', function (e) {
 const { code } = e
  const keys: string[] = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
  if (keys.includes(code)) {
    if (snake.list.length === 1) {
      snake.direction = code
      return
    }
    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 = 10
  height: number = 10
  x: number
  y: number
  color: string
  constructor(
    x: number = Math.floor(Math.random() * 49) * 10,
    y: number = Math.floor(Math.random() * 49) * 10,
    color: string = 'black'
  ) {
    this.color = color
    this.x = x
    this.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: string
  isOut: boolean
  constructor(direction: string = 'ArrowUp', speed: number = 100) {
    this.list = [new Drop(250, 250, 'red')]
    this.direction = direction
    this.isOut = false
  }
  move() {
    let newHeader = JSON.parse(JSON.stringify(this.list[0]))
    const { x: newHeaderX, y: newHeaderY } = newHeader
    const { x: foodX, y: foodY } = food
    let isEatFood: boolean = false
    if (newHeaderX === foodX && foodY === newHeaderY) {
      isEatFood = true
    }
    if (this.direction) {
    }
    switch (this.direction) {
      case 'ArrowUp':
        newHeader.y -= 10
        break
      case 'ArrowDown':
        newHeader.y += 10
        break
      case 'ArrowLeft':
        newHeader.x -= 10
        break
      case 'ArrowRight':
        newHeader.x += 10
        break
    }
    // 是否吃到自己
    const isEatSelf = this.list.some(({ x, y }) => {
      if (x === newHeader.x && y === newHeader.y) {
        return true
      }
    })
    if (isEatSelf) {
      this.isOut = true
      return 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 = true
      return alert('撞墙了!')
    }
    renderDorp(this.list)
  }
  addHead(dorp: Drop) {
    this.list.unshift(dorp)
  }
  delFooter() {
    const endDrop: Drop = this.list.pop()!
    const { x, y, width, height } = endDrop
    const 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 } = element
      const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!

      ctx.fillStyle = color
      ctx.fillRect(x, y, width, height)
    })
  } else {
    const { x, y, width, height, color } = dorps
    const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
    ctx.fillStyle = color
    ctx.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 } = e
    const 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.coloradmin.cn/o/1314573.html

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

相关文章

【Java代码审计】文件上传篇

【Java代码审计】文件上传篇 1.Java常见文件上传方式2.文件上传漏洞修复 1.Java常见文件上传方式 1、通过文件流的方式上传 public static void uploadFile(String targetURL, String filePath) throws IOException {File file new File(filePath);FileInputStream fileInpu…

【单调栈】【区间合并】LeetCode85:最大矩形

作者推荐 【动态规划】【广度优先搜索】LeetCode:2617 网格图中最少访问的格子数 本文涉及的知识点 单调栈 区间合并 题目 给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵&#xff0c;找出只包含 1 的最大矩形&#xff0c;并返回其面积。 示例 1&#xff1…

遥感图像分割系统:融合空间金字塔池化(FocalModulation)改进YOLOv8

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义 遥感图像分割是遥感技术领域中的一个重要研究方向&#xff0c;它的目标是将遥感图像中的不同地物或地物类别进行有效的分割和识别。随着遥感技术的不断发展和遥感…

iOS_给View的部分区域截图 snapshot for view

文章目录 1.将整个view截图返回image&#xff1a;2.截取view的部分区域&#xff0c;返回image&#xff1a;3.旧方法&#xff1a;4.Tips参考&#xff1a; 1.将整个view截图返回image&#xff1a; 这些 api 已被废弃&#xff0c;所以需要判断 iOS 版本 写两套代码&#xff1a; R…

【Java】5分钟读懂Java虚拟机架构

5分钟读懂Java虚拟机架构 Java虚拟机&#xff08;JVM&#xff09;架构JVM是如何工作的&#xff1f;1. 类加载器子系统2. 运行时数据区3. 执行引擎 相关资料 本文阐述了JVM的构成和组件。每个Java开发人员都知道字节码经由JRE&#xff08;Java运行时环境&#xff09;执行。但他们…

php入门、安装wampserver教程

php声称是全世界最好的语言&#xff0c;今天这篇文章就带大家入门学习php&#xff0c;php和python、javasript一样&#xff0c;是一种弱类型的脚本语言。 一、php开发环境搭建 作为初学者&#xff0c;学习php建议安装wampserver&#xff0c;wampserver是包含了apache、php和mys…

oracle 锁表解决办法

相关表介绍 V$LOCKED_OBJECT&#xff08;记录锁信息的表&#xff09;v$session&#xff08;记录会话信息的表&#xff09;v$sql&#xff08;记录 sql 执行的表&#xff09;dba_objects&#xff08;用来管理对象&#xff0c;表、库等等&#xff09; 查询锁表的 SID select b.…

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

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

Android多国语言翻译 国际化

语言目录详细对应关系 Arabic, Egypt (ar-rEG) —————————–阿拉伯语&#xff0c;埃及 Arabic, Israel (ar-rIL) ——————————-阿拉伯语&#xff0c;以色列 Bulgarian, Bulgaria (bg-rBG) ———————保加利亚语&#xff0c;保加利亚 Catalan, Spain (ca-r…

函数栈帧的创建和销毁(编程底层原理)

本篇的内容格外的难写&#xff0c;里面包含了许多的专业术语名和汇编指令等晦涩难懂的东西&#xff0c;既不利于讲解&#xff0c;也不利于读者的理解。但我会尽力去讲述出里面的底层逻辑&#xff0c;帮助大家去理解里面的过程&#xff0c;理解编程的底层原理可以为我们后续更为…

YOLOv8 | 代码逐行解析(一) | 项目目录构造分析

一、本文介绍 Hello&#xff0c;大家好这次给大家带来的不是改进&#xff0c;是整个YOLOv8项目的分析&#xff0c;整个系列大概会更新7-10篇左右的文章&#xff0c;从项目的目录到每一个功能代码的都会进行详细的讲解&#xff0c;同时YOLOv8改进系列也突破了三十篇文章&#x…

助力工业产品质检,基于yolov5l集成CBAM注意力机制开发构建智能PCB电路板质检分析系统

AI助力工业质检智能生产制造已经有很多成功的实践应用了&#xff0c;在我们前面的系列博文中也有很多对应的实践&#xff0c;感兴趣的话可以自行移步阅读前面的博文即可&#xff0c;这里本文的核心目的就是想要基于改进的yolov5l来开发构建用于PCB电路板智能检测分析的模型&…

GZ015 机器人系统集成应用技术样题1-学生赛

2023年全国职业院校技能大赛 高职组“机器人系统集成应用技术”赛项 竞赛任务书&#xff08;学生赛&#xff09; 样题1 选手须知&#xff1a; 本任务书共 25页&#xff0c;如出现任务书缺页、字迹不清等问题&#xff0c;请及时向裁判示意&#xff0c;并进行任务书的更换。参赛队…

【Trino权威指南(第二版)】Trino的架构、trino架构组件、 trino连接器架构的细节、trino的查询执行模型

文章目录 一. Trino架构1. 架构概览2. 协调器3. 发现服务4. 工作节点 二. 基于连接器的架构三. 查询执行模型1. 解析—>查询计划2. 查询计划 —> 分布式查询计划3. 运行阶段3.1. 基础概念切片&#xff1a;并行单元page 与 exchange算子pipeline切片的driverOperator 3.2.…

C#上位机与欧姆龙PLC的通信01----项目背景

最近&#xff0c;【西门庆】作为项目经理负责一个70万的北京项目&#xff0c;需要在工控系统集成软件开发中和欧 姆龙PLC对接&#xff0c;考虑项目现场情况优先想到了采用FinsTCP通讯协议&#xff0c;接下来就是记录如何一步步实现这些通讯过程的&#xff0c;希望给电气工程师&…

Netty常见的设计模式

简介 设计模式在软件开发中起着至关重要的作用&#xff0c;它们是解决常见问题的经过验证的解决方案。而Netty作为一个优秀的网络应用程序框架&#xff0c;同样也采用了许多设计模式来提供高性能和可扩展性。在本文中&#xff0c;我们将探讨Netty中使用的一些关键设计模式&…

探索Linux服务器配置信息的命令

目录 前言1 uname2 lscpu3 free4 df5 lspci6 lsusb7 lshw结语 前言 Linux系统提供了许多命令&#xff0c;用于获取和查看服务器的软硬件配置信息。这些命令可以帮助管理员和用户了解系统的状态、资源使用情况以及硬件设备的相关信息。以下是一些常用的命令以及它们的作用、使用…

【单调栈]LeetCode84: 柱状图中最大的矩形

作者推荐 【动态规划】【广度优先搜索】LeetCode:2617 网格图中最少访问的格子数 本文涉及的知识点 单调栈 题目 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形…

解决kernel32.dll丢失的修复方式,kernel32.dll预防错误的方法

kernel32.dll文件是电脑中的一个重要文件&#xff0c;如果电脑出现kernel32.dll丢失的错误提示&#xff0c;那么电脑中的一些程序将不能正常使用&#xff0c;那么出现这样的问题有什么解决办法呢&#xff1f;那么今天就和大家说说解决kernel32.dll丢失的修复方式。 一.kernel32…

elasticsearch|大数据|kibana的安装(https+密码)

前言&#xff1a; kibana是比较好安装的&#xff0c;但https密码就比较麻烦一些了&#xff0c;下面将就如何安装一个可在生产使用的kibana做一个简单的讲述 一&#xff0c; kibana版本和下载地址 这里我想还是强调一下&#xff0c;kibana的版本需要和elasticsearch的版本一…