项目介绍
学习了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;