7. Use ts to write a small snake game

I have learned the basics of ts in several articles before. Today we will use ts to complete a small snake game.

Game teardown

We will briefly dismantle our task and analyze it.

  1. First we should have a window, which we call screen. Let the snake move inside, so we should think of designing a big box as a map. Taking into account food and snake drawing we can use canvasto achieve.
  2. Secondly, we will also randomly place food on the map (you can also consider whether food should appear on the snake's body nodes, which will not be considered in this article), so we will most likely create a class, which is used to create a random block. That is food.
  3. Then we consider the snake. The snake should also be a random block at the beginning, and then eat food and grow by moving.

Code

Next, we will do detailed requirements sorting and code implementation based on the above disassembly.
Screen implementation
The screen implementation is the simplest. We decided to use canvas to draw food and snakes, so we can directly create a canvas tag as the screen.

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

food realization

  • Next we think about how food should be implemented. Since you have decided to draw food on canvas, the easiest way is to draw the food into a rectangle. The drawing of a rectangle requires four parameters, namely the coordinates of the starting point and the width and height. We set the width and height of the food to 10, so the only uncertainty is the coordinates of the starting point. This coordinate determines where on the screen he will appear.

  • It should also be noted that his starting position must be on the snake's movement path. For example, the width of our snake is 10. If the starting point of your food is at the coordinates (11, 11), then he cannot eat it at once. This food.
    Insert image description here
    So the coordinates of the food should be multiples of 10 and cannot exceed the boundaries of the screen.

  • We also need to consider that food should disappear automatically after being eaten, so the snake class should also have a clearing method to clear itself.

Code display

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)
  }
}

The implementation of snake
The implementation of snake is relatively more complicated.

  • First, let’s think about what the snake’s body should look like. In order for it to turn flexibly, the simplest way is that its body should be made of rectangles spliced ​​together one by one. In this case, we can directly use the food class above, which is why I named the above class Dropinstead Food, and I added colors to the class to distinguish snakes from food.
  • Next, we thought that since the snake is made up of multiple rectangles, there should be a container to store these rectangles in order, so we defined an array listto store the body data.
    class Snake {
          
          
    	list: Array<Drop>
    	constructor() {
          
          
    		this.list = [new Drop(250, 250, 'red')]
    	}
    	
    }
    
    We let him spawn at the center point of the map and use red to distinguish it from food.
  • Next we think about mobility methods. When the snake moves, it first needs to confirm the method. We can set a direction attribute and default a direction value during initialization. The next step is to move in the direction. How to move? If you simply use translation, you will find that the snake does not seem to be able to turn flexibly, and the snake's body does not bend. At this time we need to change our thinking. Since the snake is made up of rectangles, we only need to control the rectangles inside. Of course, it is not to control the translation of the rectangle inside, but to add and delete the rectangle. Imagine that when the snake moves up one grid (here we set the basic grid to be 10 x 10 units), does it mean that we subtract 10 from the y value of the starting point coordinate of this rectangle, so we directly create a snake head box? Subtract 10 from the starting point coordinate y value of the box, and then directly delete the last box of the snake. Can it be regarded as moving one frame?
    Insert image description hereOf course, we must also consider the situation of eating food. In this case, we do not need to delete the tail rectangle. Then our class will be supplemented like this
    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)
      }
    }
    
  • I should also consider some special situations, such as whether moving to the edge of the screen will eat my own body. We add a new status attribute to determine whether he is out, so we continue to fill in this method.
    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)
      }
    }
    

Rendering snakes and food
We wrote the classes for food and snakes, but we haven't actually drawn them on canvas yet. Next, we use the overload of ts to draw the rendering class.

// 创建渲染函数
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)
  }
}

Keyboard monitoring
If we use the arrow keys to control the movement of the snake, we need to monitor keyboard events. It should be noted that when the body length is 1, we can usually move at will, such as directly from right to left or from top to bottom, but when the body length is not 1, we have the definition of head and tail. , it should not move up and down or left and right at will. After all, it doesn't have a front and rear locomotive like a train.

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
  }
})

Finally, add the complete implementation 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
    }
  })
})()

This is just a simple version of the snake effect. It has not been rigorously tested. There will definitely be bugs. I hope you can leave a message to communicate!
I will launch another extended component library of my own plug-in element-ui, which is still being improved. I hope you will support it.

おすすめ

転載: blog.csdn.net/qq_44473483/article/details/135025216