ts练习项目-贪吃蛇(游戏类)

118 阅读5分钟

项目介绍

学习了typescript语法。教程中的练习项目是贪吃蛇。项目中是利用元素的绝对定位来实现蛇与食物的动作。个人觉得不是很好,于是自己设计了一套实现方法。

项目代码已全部完成,并放到了github

项目内容较多,准备以专栏的形式记录到《ts练习项目-贪吃蛇(greedy-snake)》

效果演示

项目演示

一、设计构思


整个项目需要对外暴露一个游戏Game )类,使用者通过简单配置就可以用它来实例化游戏对象。

二、基本功能


按照总体构思,游戏Game )中需要有Snake )、食物( Food )、时间Time )、控制器Control )、渲染器Renderer )和一个计分板ScorePanel )。还要记录下配置的列数( x )、行数( y )和页面挂载的节点( container )。

另外游戏Game )的方法需要有准备prepare )、开始start )、重新开始reStart )、暂停pause )、继续play )和一个 requestAnimationFrame 驱动的运行run )方法。

最后游戏Game )与外界的交互,预留了游戏开始onGameStart )、游戏结束onGameOver )、游戏暂停onGamePause )、游戏继续onGamePlay )、得分onAddScore )和升级onLevelUp )六个事件钩子。其中得分onAddScore )和升级onLevelUp )是留给计分板ScorePanel )使用的。

游戏Game )有很多配置项,先来一个接口限制一下。

export interface GameOptions {
    readonly container: HTMLElement,//页面挂载的节点
    readonly x?: number,//横轴长度(列数)
    readonly y?: number,//纵轴长度(行数)
    maxLevel?: number,//最高等级
    upScore?: number,//升级的分数
    onGameStart?: () => void,//开始的回调函数
    onGameOver?: (msg: string) => void,//结束的回调函数
    onGamePause?: () => void,//暂停的回调函数
    onGamePlay?: () => void,//继续的回调函数
    onAddScore?: (score: number) => void,//得分的回调函数
    onLevelUp?: (level: number) => void//升级的回调函数
}

游戏Game )类先写一个架子。内容之后补充

class Game {
    private _x: number;//横轴长度
    private _y: number;//纵轴长度
    private _container: HTMLElement;//页面挂载节点
    private _time: Time;//时间
    private _food: Food;//食物
    private _snake: Snake;//蛇
    private _control: Control;//控制器
    private _renderer: Renderer;//渲染器
    private _scorePanel: ScorePanel;//计分板
    private _isStart: boolean = false;//是否开始标志位
    private _isPause: boolean = false;//是否暂停标志位
    
    private _onGameStart?: () => void;//开始回调函数
    private _onGameOver?: (msg: string) => void;//结束回调函数
    private _onGamePause?: () => void;//暂停回调函数
    private _onGamePlay?: () => void;//继续回调函数

    //构造方法
    constructor(options: GameOptions) {
        
    }

    //准备
    private _prepare() {
        
    }

    //开始
    start() {
    }

    //重新开始
    reStart() {
    }

    //暂停
    pause() {
      
    }
    
    //继续
    play() {
        
    }

    //运行方法
    private _run() {
        
    }
}
  • 1、构造函数
constructor(options: GameOptions) {
    this._container = options.container;//页面挂载的节点
    this._x = options.x && options.x > 5? Math.floor(options.x): 5;//横轴长度,限制最小是5列
    this._y = options.y && options.y > 5? Math.floor(options.y): 5;//纵轴长度,限制最小是5行
    this._scorePanel = new ScorePanel(options);//实例化计分板(详见本专栏的计分板类)
    this._renderer = new Renderer(this);//实例化渲染器(详见本专栏的渲染器类)
    this._time = new Time();//实例化时间(详见本专栏的时间类)
    this._snake = new Snake(this);//实例化蛇(详见本专栏蛇类)
    this._food = new Food(this);//实例化食物(详见本专栏的食物类)
    this._control = new Control(this);//实例化控制器(详见本专栏的控制器类)

    this._onGameStart = options.onGameStart;
    this._onGameOver = options.onGameOver;
    this._onGamePause = options.onGamePause;
    this._onGamePlay = options.onGamePlay;

    this._run = this._run.bind(this);//绑定run的this
    this._prepare();//准备
}
  • 2、准备方法
private _prepare() {
    this._isStart = false;//是否开始设置为false
    this._isPause = false;//是否暂停设置为false
    this._renderer.prepareRenderer();//渲染器准备(详见“渲染器类”)
    this._scorePanel.prepareScorePanel();//计分板准备(详见“计分板类”)
    this._snake.prepareSnake();//蛇准备(详见“蛇类”)
    this._food.prepareFood();//食物准备(详见“食物类”)

    this._renderer.renderer();//渲染开始界面(详见“渲染器类”)
}
  • 3、开始方法
start() {
    this._isStart = true;//是否开始设置为true
    this._time.start();//开始计时(详见“时间类”)
    this._run();//运行
    this._onGameStart && this._onGameStart();//运行开始的回调
}
  • 4、重新开始方法
reStart() {
    this._prepare();//直接重新准备就可以了
}
  • 5、暂停方法
pause() {
    this._isPause = true;//是否暂停设置为true
    this._time.pause();//时间暂停运行(详见“时间类”)
    this._onGamePause && this._onGamePause();//运行暂停的回调
}
  • 6、继续方法
play() {
    this._isPause = false;//是否暂停设置为false
    this._time.play();//时间继续运行(详见“时间类”)
    this._run();//继续运行
    this._onGamePlay && this._onGamePlay();//运行继续的回调
}
  • 7、运行方法
private _run() {
    /**
     * TICK_DURATION是定义好的常量,表示默认界面更新时长。默认是500毫秒。
     * this.scorePanel.speedRatio是根据当前等级线性计算得到的速率比例。用来控制每次界面更新的时长。
     * 界面更新时长越短,蛇跑的越快。
     */
    const tickTime = TICK_DURATION * this.scorePanel.speedRatio;
    //判断界面是否该更新了(即tick了,详见“时间类”)。该更新了就运行界面的更新方法this._update()。
    this._time.isTick(tickTime) && this._update();
    //利用requestAnimationFrame方法保持一致“运行”。
    this._isStart && !this._isPause && requestAnimationFrame(this._run);
}

//界面的更新方法
private _update() {
    //蛇前进一格(吃到食物的逻辑也在这里面)(详见“蛇类”)
    this._snake.move();
    
    //判断是否撞到墙(详见“蛇类”)
    if (this._snake.touchWall()) {
        this.gameOver("撞到墙,游戏结束!");
        return;
    }
    
    //判断是否撞到自己(详见“蛇类”)
    if (this._snake.touchSelf()) {
        this.gameOver("撞到自己,游戏结束!");
        return;
    }
    
    //没撞到墙或自己,游戏没有结束,渲染界面。(详见“渲染类”)
    this._renderer.renderer();
}

//游戏结束
gameOver(msg: string) {
    this._isStart = false;//是否开始设置为false
    this._onGameOver && this._onGameOver(msg);//运行结束的回调
}
  • 8、是否胜利方法
//验证是否胜利(随机生成食物前需要先验证游戏是否胜利)
isGameWin() {
    //当蛇占满整个界面时,代表游戏胜利。
    return this._x * this._y === this._snake.positions.length;
}

完整代码

import Food from "./Food";
import Renderer from "./Renderer";
import Time from "./Time";
import {TICK_DURATION} from "./Constant";
import Snake from "./Snake";
import Control from "./Control";
import ScorePanel from "./ScorePanel";
import {GameOptions} from "./GameOptions";

class Game {
    private _x: number;//横轴
    private _y: number;//纵轴
    private _container: HTMLElement;
    private _food: Food;
    private _time: Time;
    private _snake: Snake;
    private _control: Control;
    private _renderer: Renderer;
    private _scorePanel: ScorePanel;
    private _isStart: boolean = false;
    private _isPause: boolean = false;

    private _onGameStart?: () => void;
    private _onGameOver?: (msg: string) => void;
    private _onGamePause?: () => void;
    private _onGamePlay?: () => void;

    constructor(options: GameOptions) {
        this._container = options.container;
        this._x = options.x && options.x > 5? Math.floor(options.x): 5;
        this._y = options.y && options.y > 5? Math.floor(options.y): 5;
        this._scorePanel = new ScorePanel(options);
        this._renderer = new Renderer(this);
        this._time = new Time();
        this._snake = new Snake(this);
        this._food = new Food(this);
        this._control = new Control(this);

        this._onGameStart = options.onGameStart;
        this._onGameOver = options.onGameOver;
        this._onGamePause = options.onGamePause;
        this._onGamePlay = options.onGamePlay;

        this._run = this._run.bind(this);
        this._prepare();
    }

    get x() {
        return this._x;
    }

    get y() {
        return this._y;
    }

    get isPause() {
        return this._isPause;
    }

    get container() {
        return this._container;
    }

    get snake() {
        return this._snake;
    }

    get food() {
        return this._food;
    }

    get isStart() {
        return this._isStart;
    }

    get scorePanel() {
        return this._scorePanel;
    }
    
    //准备
    private _prepare() {
        this._isStart = false;
        this._isPause = false;
        this._renderer.prepareRenderer();
        this._scorePanel.prepareScorePanel();
        this._snake.prepareSnake();
        this._food.prepareFood();

        this._renderer.renderer();//渲染开始界面
    }

    //开始
    start() {
        this._isStart = true;
        this._time.start();
        this._run();
        this._onGameStart && this._onGameStart();
    }

    //重新开始
    reStart() {
        this._prepare();
    }
    
    //暂停
    pause() {
        this._isPause = true;
        this._time.pause();
        this._onGamePause && this._onGamePause();
    }

    //继续
    play() {
        this._isPause = false;
        this._time.play();
        this._run();
        this._onGamePlay && this._onGamePlay();
    }

    //运行
    private _run() {
        const tickTime = TICK_DURATION * this.scorePanel.speedRatio;
        this._time.isTick(tickTime) && this._update();

        this._isStart && !this._isPause && requestAnimationFrame(this._run);
    }
    
    //更新
    private _update() {
        this._snake.move();

        if (this._snake.touchWall()) {
            this.gameOver("撞到墙,游戏结束!");
            return;
        }

        if (this._snake.touchSelf()) {
            this.gameOver("撞到自己,游戏结束!");
            return;
        }

        this._renderer.renderer();
    }

    //游戏结束
    gameOver(msg: string) {
        this._isStart = false;
        this._onGameOver && this._onGameOver(msg);
    }

    //验证是否胜利
    isGameWin() {
        return this._x * this._y === this._snake.positions.length;
    }

}

export default Game;