美文网首页
用typescript写个贪吃蛇小游戏

用typescript写个贪吃蛇小游戏

作者: 林世哲 | 来源:发表于2020-03-26 17:10 被阅读0次

写这个练手让我学了一些canvas,rxjs,虽然遇到了点小问题,但是总体收货还是挺大的。


效果图.gif

写之前想好思路非常重要,一开始我的思路就错了,导致写一半发现无法实现还得重想
思路:
要解决的问题:
1.蛇的移动以及转向控制
2.吃食物身体增长
3.食物的随机生成
解决方案:把蛇看成一个一个元素组成的,蛇是一个数组,身体都是数组的元素
1.根据定时器的增长,周期地改变蛇的位置(加或减单位长度);后一个元素复制前一个的位置,就可以正常转向。
2.每次吃食物都新建一个身体元素,放到数组端部
3.随机数生成(x,y)坐标,要避免生成在蛇的身上

想好了开始写,我是基于angular8框架写的,https://github.com/linshizhe123/snake

html:

<div class="body">
  <div class="menu">
    <button (click)="onStart(startOrReset)">{{startOrReset}}</button>
    <button (click)="onSuspend()">暂停</button>
    <span>得分: <a>{{score}}</a></span>
    <span>时间: <a>{{time|snakePipe}}</a></span>
  </div>
  <div [ngStyle]="{opacity:isGameOver?0.6:1}">
    <canvas id="canvas" width="600px" height="600px" #canvas></canvas>
  </div>
</div>

<!--死亡弹窗-->
<div class="over-modal" *ngIf="isGameOver">
  游戏结束,得分为:{{score}}分!
</div>

canvas格子背景:


格子背景.png

css代码:

#canvas {
 margin: 10px 50px;
 background: #58a;
 background-image: linear-gradient(hsla(0, 0%, 100%, .3) 1px, transparent 0),
 linear-gradient(90deg, hsla(0, 0%, 100%, .3) 1px, transparent 0),
 linear-gradient(hsla(0, 0%, 100%, .3) 1px, transparent 0),
 linear-gradient(90deg, hsla(0, 0%, 100%, .3) 1px, transparent 0);
 background-size: 120px 120px, 120px 120px, 24px 24px, 24px 24px;
}

所有css:

.body {
  width: 700px;
  height: 700px;
  margin: 100px auto;
  border: 1px solid #FF0;
}

.menu {
  overflow: hidden;
}

.menu button {
  width: 80px;
  height: 30px;
  margin: 10px 20px;
  float: left;
  outline: none;
  cursor: pointer;
}

.menu span {
  float: right;
  margin-right: 100px;
  line-height: 50px;
}

.menu span a {
  margin-left: 5px;
}

#canvas {
  margin: 10px 50px;
  background: #58a;
  background-image: linear-gradient(hsla(0, 0%, 100%, .3) 1px, transparent 0),
  linear-gradient(90deg, hsla(0, 0%, 100%, .3) 1px, transparent 0),
  linear-gradient(hsla(0, 0%, 100%, .3) 1px, transparent 0),
  linear-gradient(90deg, hsla(0, 0%, 100%, .3) 1px, transparent 0);
  background-size: 120px 120px, 120px 120px, 24px 24px, 24px 24px;
}

.over-modal {
  width: 500px;
  text-align: center;
  font-size: 30px;
  position: absolute;
  z-index: 999;
  top: 50%;
  left: calc(50% - 250px);
}

ts部分的逻辑:
所有变量:

@ViewChild('canvas', {static: true}) private c: ElementRef;

  public snakeList: Array<SnakeItem> = []; //蛇身
  public food: SnakeItem = new SnakeItem(); // 食物

  private timeSubscription: Subscription; //定时器
  private keyboardSubscription: Subscription; // 键盘监听

  public time: number = 0; // 时间

  public suspendTime: number; // 暂停时间

  public score: number = 0; // 得分

  public startOrReset = '开始';

  public isGameOver = false;

  public currentDirectionArr: Array<number> = [0, 24]; // 当前控制方向 :x和y分别加上这个数组的元素

  public canvas: any;

关于控制蛇的方向,我想的是通过一组数组正负来判断,数组[x,y],经过计算,每次移动都是24px,我给的默认是向下移动,所以控制方向的数组默认是[0,24]。
[0,24]就是向下,
[0,-24]就是向上,以此类推。

ngOnInit() {
    this.listenKeyboard(); // 注册键盘监听

    // 重置所有
    this.snakeList = [];
    this.snakeList.push({x: 4, y: 4, w: 16, h: 16, directionArr: [0, 24]});
    this.snakeList.push({x: 4, y: 28, w: 16, h: 16, directionArr: [0, 24]});
    this.currentDirectionArr = [0, 24];
    this.score = 0;

    this.canvas = this.c.nativeElement.getContext('2d');
    // 生成初始小蛇 规律是+24
    this.canvas.fillStyle = 'orange';
    this.canvas.fillRect(4, 4, 16, 16);
    this.canvas.fillStyle = 'black';
    this.canvas.fillRect(4, 28, 16, 16);
  }
初始小蛇.png

解除所有订阅:

ngOnDestroy(): void {
    this.keyboardSubscription.unsubscribe();
    this.timeSubscription.unsubscribe();
  }

开始按钮点击,对应的函数:

public onStart(startOrReset: string) {
    this.isGameOver = false;
    this.timeSubscription && this.timeSubscription.unsubscribe();
    this.startOrReset = '重来';
    if (startOrReset !== '继续') {
      this.time = 0;
      this.suspendTime = 0;
      this.clearCanvas();
      this.ngOnInit();
      //生成初始食物
      this.generateFood();
    } else {
      this.time = this.suspendTime;
    }
    // 定时器启动
    this.timeSubscription = timer(1000, 500).subscribe(sec => {
      this.refreshCanvas();
      this.time = startOrReset !== '继续' ? sec + 1 : sec + this.suspendTime + 1;
    });
  }

我加了暂停,点开始,开始变成重来;点暂停,重来变成开始。

暂停函数:

public onSuspend() {
    this.suspendTime = this.time;
    this.timeSubscription.unsubscribe();
    this.startOrReset = '继续';
  }

画矩形函数:

public drawNewRect(x: number, y: number, isHead = false) {
    if (isHead) {
      this.canvas.fillStyle = 'black';
      this.canvas.fillRect(x, y, 16, 16);
    } else {
      this.canvas.fillStyle = 'orange';
      this.canvas.fillRect(x, y, 16, 16);
    }
  }

isHead来区分是画的蛇头还是蛇身

清除画布:

public clearCanvas() {
    this.canvas.clearRect(0, 0, 600, 600);
  }

更新蛇的位置,由定时器触发,周期跟新画布:

public refreshCanvas() {
    // 清除画布
    this.clearCanvas();

    // 判断是否吃到食物
    if (this.onEatFood()) {
      // 重新随机生成食物
      this.generateFood();
      //加分
      this.score += 10;

      //生成新身体,并放到数组头部
      const addSnakeBody = new SnakeItem();
      //新元素比尾部少一个单位 故减去directionArr
      addSnakeBody.x = this.snakeList[0].x - this.snakeList[0].directionArr[0];
      addSnakeBody.y = this.snakeList[0].y - this.snakeList[0].directionArr[1];
      addSnakeBody.directionArr = this.snakeList[0].directionArr.concat();
      this.snakeList.unshift(addSnakeBody);
    } else {
      // 重画食物,保持食物位置不变
      this.drawNewRect(this.food.x, this.food.y);
    }

    // 蛇身体逻辑,后一位复制前一位属性
    for (let i = 0; i < this.snakeList.length; i++) {
      if (i === this.snakeList.length - 1) { // 判断是否是头部
        this.drawNewRect(this.snakeList[i].x + this.currentDirectionArr[0],
          this.snakeList[i].y + this.currentDirectionArr[1], true);
        this.snakeList[i].directionArr = this.currentDirectionArr.concat();
        this.snakeList[i].x = this.snakeList[i].x + this.currentDirectionArr[0];
        this.snakeList[i].y = this.snakeList[i].y + this.currentDirectionArr[1];
      } else {
        this.drawNewRect(this.snakeList[i].x + this.snakeList[i + 1].directionArr[0],
          this.snakeList[i].y + this.snakeList[i + 1].directionArr[1], false);
        this.snakeList[i].directionArr = this.snakeList[i + 1].directionArr.concat();
        this.snakeList[i].x = this.snakeList[i].x + this.snakeList[i + 1].directionArr[0];
        this.snakeList[i].y = this.snakeList[i].y + this.snakeList[i + 1].directionArr[1];
      }
    }

    //死亡检测
    if (this.checkSnakeDeath()) {
      this.keyboardSubscription.unsubscribe();
      this.timeSubscription.unsubscribe();
      this.isGameOver = true;
    }
  }

原理:遍历元素,最后一个元素(头部)方向由this.currentDirectionArr控制, 其他的元素(身体)由前一个元素的directionArr控制

死亡检查:

public checkSnakeDeath() {
    const snakeArr = this.snakeList;
    for (let i = 0; i < snakeArr.length - 1; i++) {
      if (snakeArr[snakeArr.length - 1].x === snakeArr[i].x && snakeArr[snakeArr.length - 1].y === snakeArr[i].y) {
        return true;
      }
    }
    if (snakeArr[snakeArr.length - 1].x > 600 || snakeArr[snakeArr.length - 1].x < 0 ||
      snakeArr[snakeArr.length - 1].y > 600 || snakeArr[snakeArr.length - 1].y < 0) {
      return true;
    }
    return false;
  }

蛇头不能碰壁,不能碰身体

食物生成:

public generateFood() {
    // 生成0~24随机整数 确定食物位置
    let randomX;
    let randomY;
    do {
      randomX = Math.floor(Math.random() * 25);
      randomY = Math.floor(Math.random() * 25);
    } while (this.checkFoodPosition(randomX, randomY));
    this.food.x = 4 + randomX * 24;
    this.food.y = 4 + randomY * 24;
    this.drawNewRect(4 + randomX * 24, 4 + randomY * 24);
  }

//检查食物是否出现在蛇的身体上
  public checkFoodPosition(x, y): boolean {
    for (let i = 0; i < this.snakeList.length; i++) {
      if (this.snakeList[i].x === x && this.snakeList[i].y === y) {
        return true;
      }
    }
    return false;
  }

/**
   * 检测是否吃到食物
   */
  public onEatFood(): boolean {
    // 蛇头和食物重叠
    if (this.snakeList[this.snakeList.length - 1].x === this.food.x &&
      this.snakeList[this.snakeList.length - 1].y === this.food.y) {
      return true;
    } else {
      return false;
    }
  }

键盘监听和转向逻辑:

 /**
   * 键盘事件
   */
  public onKeyDown(key: string) {
    // 转向逻辑
    if (this.currentDirectionArr.toString() === [-24, 0].toString() ||
      this.currentDirectionArr.toString() === [24, 0].toString()) {
      if (key === 'ArrowUp') {
        this.currentDirectionArr = [0, -24];
      } else if (key === 'ArrowDown') {
        this.currentDirectionArr = [0, 24];
      } else {
        return;
      }
    } else {
      if (key === 'ArrowRight') {
        this.currentDirectionArr = [24, 0];
      } else if (key === 'ArrowLeft') {
        this.currentDirectionArr = [-24, 0];
      } else {
        return;
      }
    }
  }

  /**
   * 键盘监听
   */
  private listenKeyboard() {
    this.keyboardSubscription && this.keyboardSubscription.unsubscribe();
    this.keyboardSubscription = fromEvent(window, 'keydown').subscribe((event: any) => {
      this.onKeyDown(event.key);
    });
  }

蛇的类:

class SnakeItem {
  x: number;
  y: number;
  w = 16;
  h = 16;
  directionArr: Array<number> = [0, 24];
}

所有代码:

import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { fromEvent, Subscription, timer } from 'rxjs';

@Component({
  selector: 'app-snake',
  templateUrl: './snake.component.html',
  styleUrls: ['./snake.component.css']
})
export class SnakeComponent implements OnInit, OnDestroy {

  @ViewChild('canvas', {static: true}) private c: ElementRef;

  public snakeList: Array<SnakeItem> = []; //蛇身
  public food: SnakeItem = new SnakeItem(); // 食物

  private timeSubscription: Subscription; //定时器
  private keyboardSubscription: Subscription; // 键盘监听

  public time: number = 0; // 时间

  public suspendTime: number; // 暂停时间

  public score: number = 0; // 得分

  public startOrReset = '开始';

  public isGameOver = false;

  public currentDirectionArr: Array<number> = [0, 24]; // 当前控制方向 :x和y分别加上这个数组的元素

  public canvas: any;

  constructor() {
  }

  ngOnInit() {
    this.listenKeyboard(); // 注册键盘监听

    // 重置所有
    this.snakeList = [];
    this.snakeList.push({x: 4, y: 4, w: 16, h: 16, directionArr: [0, 24]});
    this.snakeList.push({x: 4, y: 28, w: 16, h: 16, directionArr: [0, 24]});
    this.currentDirectionArr = [0, 24];
    this.score = 0;

    this.canvas = this.c.nativeElement.getContext('2d');
    // 生成初始小蛇 规律是+24
    this.canvas.fillStyle = 'orange';
    this.canvas.fillRect(4, 4, 16, 16);
    this.canvas.fillStyle = 'black';
    this.canvas.fillRect(4, 28, 16, 16);
  }

  ngOnDestroy(): void {
    this.keyboardSubscription.unsubscribe();
    this.timeSubscription.unsubscribe();
  }

  /**
   * 开始
   */
  public onStart(startOrReset: string) {
    this.isGameOver = false;
    this.timeSubscription && this.timeSubscription.unsubscribe();
    this.startOrReset = '重来';
    if (startOrReset !== '继续') {
      this.time = 0;
      this.suspendTime = 0;
      this.clearCanvas();
      this.ngOnInit();
      //生成初始食物
      this.generateFood();
    } else {
      this.time = this.suspendTime;
    }
    // 定时器启动
    this.timeSubscription = timer(1000, 500).subscribe(sec => {
      this.refreshCanvas();
      this.time = startOrReset !== '继续' ? sec + 1 : sec + this.suspendTime + 1;
    });
  }

  /**
   * 暂停
   */
  public onSuspend() {
    this.suspendTime = this.time;
    this.timeSubscription.unsubscribe();
    this.startOrReset = '继续';
  }

  /** 画新矩形
   * @param x,y
   * */
  public drawNewRect(x: number, y: number, isHead = false) {
    if (isHead) {
      this.canvas.fillStyle = 'black';
      this.canvas.fillRect(x, y, 16, 16);
    } else {
      this.canvas.fillStyle = 'orange';
      this.canvas.fillRect(x, y, 16, 16);
    }
  }

  /**
   * 清楚画布
   */
  public clearCanvas() {
    this.canvas.clearRect(0, 0, 600, 600);
  }

  /**
   * 刷新画布,每秒一次,更新位置
   * 原理:遍历元素,最后一个元素(头部)方向由this.currentDirectionArr控制,
   * 其他的元素(身体)由前一个元素的directionArr控制
   */
  public refreshCanvas() {
    // 清除画布
    this.clearCanvas();

    // 判断是否吃到食物
    if (this.onEatFood()) {
      // 重新随机生成食物
      this.generateFood();
      //加分
      this.score += 10;

      //生成新身体,并放到数组头部
      const addSnakeBody = new SnakeItem();
      //新元素比尾部少一个单位 故减去directionArr
      addSnakeBody.x = this.snakeList[0].x - this.snakeList[0].directionArr[0];
      addSnakeBody.y = this.snakeList[0].y - this.snakeList[0].directionArr[1];
      addSnakeBody.directionArr = this.snakeList[0].directionArr.concat();
      this.snakeList.unshift(addSnakeBody);
    } else {
      // 重画食物,保持食物位置不变
      this.drawNewRect(this.food.x, this.food.y);
    }

    // 蛇身体逻辑,后一位复制前一位属性
    for (let i = 0; i < this.snakeList.length; i++) {
      if (i === this.snakeList.length - 1) { // 判断是否是头部
        this.drawNewRect(this.snakeList[i].x + this.currentDirectionArr[0],
          this.snakeList[i].y + this.currentDirectionArr[1], true);
        this.snakeList[i].directionArr = this.currentDirectionArr.concat();
        this.snakeList[i].x = this.snakeList[i].x + this.currentDirectionArr[0];
        this.snakeList[i].y = this.snakeList[i].y + this.currentDirectionArr[1];
      } else {
        this.drawNewRect(this.snakeList[i].x + this.snakeList[i + 1].directionArr[0],
          this.snakeList[i].y + this.snakeList[i + 1].directionArr[1], false);
        this.snakeList[i].directionArr = this.snakeList[i + 1].directionArr.concat();
        this.snakeList[i].x = this.snakeList[i].x + this.snakeList[i + 1].directionArr[0];
        this.snakeList[i].y = this.snakeList[i].y + this.snakeList[i + 1].directionArr[1];
      }
    }

    //死亡检测
    if (this.checkSnakeDeath()) {
      this.keyboardSubscription.unsubscribe();
      this.timeSubscription.unsubscribe();
      this.isGameOver = true;
    }
  }

  /**
   * 键盘事件
   */
  public onKeyDown(key: string) {
    // 转向逻辑
    if (this.currentDirectionArr.toString() === [-24, 0].toString() ||
      this.currentDirectionArr.toString() === [24, 0].toString()) {
      if (key === 'ArrowUp') {
        this.currentDirectionArr = [0, -24];
      } else if (key === 'ArrowDown') {
        this.currentDirectionArr = [0, 24];
      } else {
        return;
      }
    } else {
      if (key === 'ArrowRight') {
        this.currentDirectionArr = [24, 0];
      } else if (key === 'ArrowLeft') {
        this.currentDirectionArr = [-24, 0];
      } else {
        return;
      }
    }
  }

  /**
   * 键盘监听
   */
  private listenKeyboard() {
    this.keyboardSubscription && this.keyboardSubscription.unsubscribe();
    this.keyboardSubscription = fromEvent(window, 'keydown').subscribe((event: any) => {
      this.onKeyDown(event.key);
    });
  }

  /**
   * 食物生成
   */
  public generateFood() {
    // 生成0~24随机整数 确定食物位置
    let randomX;
    let randomY;
    do {
      randomX = Math.floor(Math.random() * 25);
      randomY = Math.floor(Math.random() * 25);
    } while (this.checkFoodPosition(randomX, randomY));
    this.food.x = 4 + randomX * 24;
    this.food.y = 4 + randomY * 24;
    this.drawNewRect(4 + randomX * 24, 4 + randomY * 24);
  }

  //检查食物是否出现在蛇的身体上
  public checkFoodPosition(x, y): boolean {
    for (let i = 0; i < this.snakeList.length; i++) {
      if (this.snakeList[i].x === x && this.snakeList[i].y === y) {
        return true;
      }
    }
    return false;
  }

  /**
   * 检测是否吃到食物
   */
  public onEatFood(): boolean {
    // 蛇头和食物重叠
    if (this.snakeList[this.snakeList.length - 1].x === this.food.x &&
      this.snakeList[this.snakeList.length - 1].y === this.food.y) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * 检测死亡 蛇头不能碰壁,不能碰身体
   */
  public checkSnakeDeath() {
    const snakeArr = this.snakeList;
    for (let i = 0; i < snakeArr.length - 1; i++) {
      if (snakeArr[snakeArr.length - 1].x === snakeArr[i].x && snakeArr[snakeArr.length - 1].y === snakeArr[i].y) {
        return true;
      }
    }
    if (snakeArr[snakeArr.length - 1].x > 600 || snakeArr[snakeArr.length - 1].x < 0 ||
      snakeArr[snakeArr.length - 1].y > 600 || snakeArr[snakeArr.length - 1].y < 0) {
      return true;
    }
    return false;
  }
}

class SnakeItem {
  x: number;
  y: number;
  w = 16;
  h = 16;
  directionArr: Array<number> = [0, 24];
}

第一次写,写的不好,思路也比较笨拙,肯定还有许多优秀简洁的实现,就当是锻炼自己是思维能力,希望也能给大家带来启发!

相关文章

网友评论

      本文标题:用typescript写个贪吃蛇小游戏

      本文链接:https://www.haomeiwen.com/subject/dddcuhtx.html