初识egret,十分简约的一款H5游戏引擎,话不多说,开始制作我们的简易版彩虹贪吃蛇。
大体上是一个忽略的自生碰撞自身可以以一种离奇方式扭曲的彩虹蛇,我们姑且把他认为是五维生物。
首先在这条贪吃的彩虹蛇还未出现之前,神奇的宇宙便突然出现了美味的彩虹糖,它的颜色都是随机生成生成的,当然或许是上帝偏爱七色彩虹,所以你也可以指定这些颜色,只需把下面代码中注释的地方去掉,并且注释下一行代码即可,例如:
/**
* 食物颜色
*/
private static colorList: number[] =
[0x70f3ff, 0xff461f, 0x00bc12, 0x21a675, 0x4c221b, 0xbf242a, 0x161823, 0xffa400,];
/**
* 获取随机的颜色
*/
private randomColor(): number {
// return Food.colorList[Math.round(Math.random() * Food.colorList.length)];
return parseInt("0x" + ("000000" + ((Math.random() * 16777215 + 0.5) >> 0).toString(16)).slice(-6));
}
随机颜色的获取其实就是获取六位随机十六进制数字然后在前加上“0x”标志。
为了确定食物所在的具体位置和大小,我们还需要传入二维坐标和食物的半径
private food: egret.Shape;
public color: number;
/**
* 初始化
*
* 1.绘制果实
*/
private init(x: number, y: number, r: number): void {
//获取随机颜色
this.color = this.randomColor();
this.food = new egret.Shape();
this.food.graphics.beginFill(this.color);
this.food.graphics.drawCircle(0, 0, r);
this.food.graphics.endFill();
this.food.x = r;
this.food.y = r;
//位置
this.x = x;
this.y = y;
this.addChild(this.food);
}
我们可以看到,食物的形状是作为一个Shape存在的,这是egret内置的一个对象类,可以使用画笔进行绘图,先获取事物对象的graphics画笔,由于粒子间的相互作用,食物被力制约成圆形,所以我们使用drawCircle( )函数进行绘制,绘制完成后设置食物的位置
我们可以看到上块代码中有
this.food.x = r;
this.food.y = r;
//位置
this.x = x;
this.y = y;
以上两种位置设置,首先需要说明的是typescript就像所有面向对象语言一样,在在类中this就代表了当前实例对象。那么第一个是设置该图像(即圆形)出现在食物中的位置,我们设定为处于中间位置即可,第二个是设置食物在空间中的位置,我们将传进来的位置写入即可。
最后作为食物就要有被吃掉的觉悟,我们将食物这个物体在父节点——也就是舞台中去除就好了。
/**
* 被吃的
*/
public onEat() {
this.parent.removeChild(this);
}
当然我们仍然不能忘记食物作为一个自定义类就必须拥有我们的构造函数。它需要传入的横纵坐标来确定位置,还需要半径来确定大小,之后我们调用前面的init方法初始化食物的样式和位置即可。
/**
* @param x 横坐标
* @param y 纵坐标
* @param r 半径
*/
public constructor(x: number, y: number, r: number) {
super();
this.init(x, y, r);
}
食物作为一个会在屏幕上反复出现的元素,我们将其称为精灵
在egret中使用Sprite来定义,所以我们的食物类便需要继承自它当然,我们完全可以将食物看做在游戏中一次只会出现一个的“固定”的对象,每次更改食物都是单纯的更改它的属性;但为了日后可以制作多人网络对战的版本,我们选用目前的精灵版。
最后我们彩虹糖食物的代码看起来就像这样
class Food extends egret.Sprite {
/**
* 食物颜色
*/
private static colorList: number[] =
[0x70f3ff, 0xff461f, 0x00bc12, 0x21a675, 0x4c221b, 0xbf242a, 0x161823, 0xffa400,];
/**
* @param x 横坐标
* @param y 纵坐标
* @param r 半径
*/
public constructor(x: number, y: number, r: number) {
super();
this.init(x, y, r);
}
private food: egret.Shape;
public color: number;
/**
* 初始化
*
* 1.绘制果实
*/
private init(x: number, y: number, r: number): void {
//获取随机颜色
this.color = this.randomColor();
this.food = new egret.Shape();
this.food.graphics.beginFill(this.color);
this.food.graphics.drawCircle(0, 0, r);
this.food.graphics.endFill();
this.food.x = r;
this.food.y = r;
//位置
this.x = x;
this.y = y;
this.addChild(this.food);
}
/**
* 获取随机的颜色
*/
private randomColor(): number {
// return Food.colorList[Math.round(Math.random() * Food.colorList.length)];
return parseInt("0x" + ("000000" + ((Math.random() * 16777215 + 0.5) >> 0).toString(16)).slice(-6));
}
/**
* 被吃
*/
public onEat() {
this.parent.removeChild(this);
}
}
即使作为一只四维生物,彩虹蛇仍然改变不了贪吃的本质,所以我们要先给它一颗张着嘴巴的脑袋:
//蛇头
private head: egret.Shape;
//蛇身的半径
private radius;
//蛇身的全部节点list(保存蛇每个节点的信息和蛇本身做区别)
private bodyList: egret.Shape[] = [];
/**
* 根据横纵坐标,半径和颜色来初始化蛇头
* @param x 横坐标
* @param y 纵坐标
* @param r 半径
* @param color 颜色
*/
private init(x: number, y: number, r: number, color: number) {
//绘制蛇头,同样是一个实心圆
this.head = new egret.Shape();
this.head.graphics.lineStyle(10, 0xff4777);
this.head.graphics.beginFill(color);
this.head.graphics.drawCircle(r, r, r);
this.head.graphics.endFill();
//设置蛇头在蛇身的位置
this.head.x = 0;
this.head.y = 0;
this.radius = r;
//设置蛇身的位置
this.x = x;
this.y = y;
//将蛇头添加入蛇身的list
this.bodyList.push(this.head);
//将蛇头加入到蛇身中并指定显示索引为最大,以保证蛇头永远在蛇身其他节点的上方。
this.addChild(this.bodyList[this.bodyList.length - 1]);
this.setChildIndex(this.bodyList[this.bodyList.length - 1], -999);
}
同样地添加蛇的构造函数,此时蛇的颜色不是随机的,由我们在以后来指定。
public constructor(x: number, y: number, r: number, color: number) {
super();
this.init(x, y, r, color);
}
当我们的彩虹蛇吃到食物时,就会引发身体变长的事件,也就是蛇身新增一个节点,我们将食物自身的颜色作为新增节点的颜色,同时绘制一个实心圆添加到蛇身上即可。
/***
* 吃食物后增加节点
* @param color 食物的颜色
*/
public afterEat(color: number) {
//新增的节点(蛇身)
var node: egret.Shape = new egret.Shape();
node.graphics.beginFill(color);
node.graphics.drawCircle(this.radius, this.radius, this.radius);
node.graphics.endFill();
//指定新增节点的位置在蛇身节点list的最后一个节点,也就是蛇尾的一个坐标偏移(这里可以随便指定合理的位置即可)
node.x = this.bodyList[this.bodyList.length - 1].x + this.radius * 0.6;
node.y = this.bodyList[this.bodyList.length - 1].y + this.radius * 0.6;
//将新增节点添加入蛇身和蛇身节点list
this.bodyList.push(node);
this.addChild(this.bodyList[this.bodyList.length - 1]);
//不要忘了指定新增节点的显示索引,我们将它放在所有节点的最下面。
this.setChildIndex(this.bodyList[this.bodyList.length - 1], 0);
}
我们的彩虹蛇现在万事俱备,只需要扭动着它肥胖的身体,受着贪婪欲望的指使便可以来一场饕鬄盛宴了,我们为它加上移动的逻辑:
首先考虑到游戏玩法是用鼠标或者手指在屏幕上点击,以获得一个位置坐标,然后我们将这个坐标视为彩虹蛇移动的目的地,将它移动过去即可;
为了获取方向,我们必须先得到目标点坐标和蛇头坐标:
//我们可以利用用户的点击事件 e:egret.TouchEvent来方便的获取到点击坐标
var mx = e.stageX;
var my = e.stageY;
为了获取蛇头的坐标,我们必须先理解一个概念:用户点击的点存在于Stage舞台上;而彩虹蛇本身作为一个Spite出现在Stage上,蛇头和蛇身都是彩虹蛇内部的一个Shape,为了使蛇移动,那么我们需要获取到蛇头在Stage上的位置,使用它和目标点进行运算才可以确定蛇移动下一步的位置。
//我们前面知道蛇身节点list的首个节点其实就是蛇头
//让它在蛇中的偏移坐标加上蛇在舞台中的坐标,便可以得到全局坐标
var hx = this.x + this.bodyList[0].x;
var hy = this.y + this.bodyList[0].y;
好了,反过头来看蛇身节点,我们知道彩虹蛇总是位于后边的节点会走到它前边的节点的位置上,所以我们遍历所有节点,让它新的位置改为它的上一个节点的位置即可:
var tween: egret.Tween;
for (var i = this.bodyList.length - 1; i >= 1; i--) {
tween = egret.Tween.get(this.bodyList[i]);
tween.to({ x: this.bodyList[i - 1].x, y: this.bodyList[i - 1].y }, during);
}
egret的缓动动画由Tween定义,使用tween.to()函数进行对某些数值的变化,在上面代码中,我们将该节点当前位置的坐标改为上一个节点的坐标,持续时间是during所定义好的数值,会有一个平滑的数值改变,增强用户体验。
需要看到的是,我们使用了反向遍历节点集合,这样做的目的很好理解,因为我们需要位于索引前的节点的坐标值。
最后我们的首节点,也就是蛇头需要一个明确的位置去移动。
所以根据所学的平面几何可以很方便的计算两点的连线:
//斜率
var mk = (my - hy) / (mx - hx);
//角度
var mangle = Math.atan(mk);
之后便可以按照预设好的移动距离,将蛇头的坐标更改上去即可
//
//修改横纵坐标
tmpx = this.bodyList[0].x - this.speed * Math.cos(mangle);
tmpy = this.bodyList[0].y - this.speed * Math.sin(mangle);
//别忘了缓动动画
tween.to({ x: tmpx, y: tmpy }, during);
当然我们还应该考虑多种情况:
所以最后看起了像这样
tween = egret.Tween.get(this.bodyList[0]);
var tmpx, tmpy;
if (hx == mx && hy == my) {
//位置相同
return;
}
if (hx != mx) {
//非垂直
//斜率
var mk = (my - hy) / (mx - hx);
//角度
var mangle = Math.atan(mk);
if (mx //左边
tmpx = this.bodyList[0].x - this.speed * Math.cos(mangle);
tmpy = this.bodyList[0].y - this.speed * Math.sin(mangle);
tween.to({ x: tmpx, y: tmpy }, during);
} else {
//右边
tmpx = this.bodyList[0].x + this.speed * Math.cos(mangle);
tmpy = this.bodyList[0].y + this.speed * Math.sin(mangle);
tween.to({ x: tmpx, y: tmpy }, during);
}
} else {
//垂直
if (mx //水平向左
tmpx = this.bodyList[0].x - this.speed;
tween.to({ x: tmpx, y: tmpy }, during);
} else {
//水平向右
tmpx = this.bodyList[0].x + this.speed;
tween.to({ x: tmpx, y: tmpy }, during);
}
}
好了,最后我们的彩虹蛇也制作完成了,Snake.ts的完整代码如下:
class Snake extends egret.Sprite {
public constructor(x: number, y: number, r: number, color: number) {
super();
this.init(x, y, r, color);
}
//蛇头
private head: egret.Shape;
//蛇身的半径
private radius;
//蛇身的全部节点list(保存蛇每个节点的信息和蛇本身做区别)
private bodyList: egret.Shape[] = [];
/**
* 根据横纵坐标,半径和颜色来初始化蛇头
* @param x 横坐标
* @param y 纵坐标
* @param r 半径
* @param color 颜色
*/
private init(x: number, y: number, r: number, color: number) {
//绘制蛇头,同样是一个实心圆
this.head = new egret.Shape();
this.head.graphics.lineStyle(10, 0xff4777);
this.head.graphics.beginFill(color);
this.head.graphics.drawCircle(r, r, r);
this.head.graphics.endFill();
//设置蛇头在蛇身的位置
this.head.x = 0;
this.head.y = 0;
this.radius = r;
//设置蛇身的位置
this.x = x;
this.y = y;
//将蛇头添加入蛇身的list
this.bodyList.push(this.head);
//将蛇头加入到蛇身中并指定显示索引为最大,以保证蛇头永远在蛇身其他节点的上方。
this.addChild(this.bodyList[this.bodyList.length - 1]);
this.setChildIndex(this.bodyList[this.bodyList.length - 1], -999);
}
/***
* 吃食物后增加节点
* @param color 食物的颜色
*/
public afterEat(color: number) {
//新增的节点(蛇身)
var node: egret.Shape = new egret.Shape();
node.graphics.beginFill(color);
node.graphics.drawCircle(this.radius, this.radius, this.radius);
node.graphics.endFill();
//指定新增节点的位置在蛇身节点list的最后一个节点,也就是蛇尾的一个坐标偏移(这里可以随便指定合理的位置即可)
node.x = this.bodyList[this.bodyList.length - 1].x + this.radius * 0.6;
node.y = this.bodyList[this.bodyList.length - 1].y + this.radius * 0.6;
//将新增节点添加入蛇身和蛇身节点list
this.bodyList.push(node);
this.addChild(this.bodyList[this.bodyList.length - 1]);
//不要忘了指定新增节点的显示索引,我们将它放在所有节点的最下面。
this.setChildIndex(this.bodyList[this.bodyList.length - 1], 0);
}
public speed = 20;
public move(e: egret.TouchEvent, during: number) {
//我们可以利用用户的点击事件 e:egret.TouchEvent来方便的获取到点击坐标
var mx = e.stageX;
var my = e.stageY;
var tween: egret.Tween;
for (var i = this.bodyList.length - 1; i >= 1; i--) {
tween = egret.Tween.get(this.bodyList[i]);
tween.to({ x: this.bodyList[i - 1].x, y: this.bodyList[i - 1].y }, during);
}
//我们前面知道蛇身节点list的首个节点其实就是蛇头
//让它在蛇中的偏移坐标加上蛇在舞台中的坐标,便可以得到全局坐标
var hx = this.x + this.bodyList[0].x;
var hy = this.y + this.bodyList[0].y;
//设置当前缓动对象为蛇头
tween = egret.Tween.get(this.bodyList[0]);
var tmpx, tmpy;
if (hx == mx && hy == my) {
//位置相同
return;
}
if (hx != mx) {
//非垂直
//斜率
var mk = (my - hy) / (mx - hx);
//角度
var mangle = Math.atan(mk);
if (mx //左边
tmpx = this.bodyList[0].x - this.speed * Math.cos(mangle);
tmpy = this.bodyList[0].y - this.speed * Math.sin(mangle);
tween.to({ x: tmpx, y: tmpy }, during);
} else {
//右边
tmpx = this.bodyList[0].x + this.speed * Math.cos(mangle);
tmpy = this.bodyList[0].y + this.speed * Math.sin(mangle);
tween.to({ x: tmpx, y: tmpy }, during);
}
} else {
//垂直
if (mx //水平向左
tmpx = this.bodyList[0].x - this.speed;
tween.to({ x: tmpx, y: tmpy }, during);
} else {
//水平向右
tmpx = this.bodyList[0].x + this.speed;
tween.to({ x: tmpx, y: tmpy }, during);
}
}
}
public getHead() {
return this.bodyList[0];
}
}
作为游戏的控制类,egret以它作为游戏创建的响应事件:
this.addEventListener(egret.Event.ADDED_TO_STAGE, this.createGameScene, this);
新建一个项目的时候egret在Main.ts中自带了一大串加载图片资源的代码,我们可以把它删除,或者完全不用理会,直接考虑游戏的主题逻辑:
private food: Food;
private snake: Snake;
private stageW: number;
private stageH: number;
private radius = 30;
/**
* 创建游戏场景
* Create a game scene
*/
private createGameScene(): void {
//获取舞台宽和高,在Main.ts中,this代表了整个游戏
this.stageW = this.stage.stageWidth;
this.stageH = this.stage.stageHeight;
//白色背景填满整个屏幕
var bg = new egret.Shape();
bg.graphics.beginFill(0xffffff);
bg.graphics.drawRect(0, 0, this.stageW, this.stageH);
bg.graphics.endFill();
this.addChild(bg);
//调用方法生产随机食物
this.randomFood();
//生成彩虹蛇
this.snake = new Snake(this.stageW * 0.5, this.stageH * 0.5, this.radius, 0x000000);
this.addChild(this.snake);
//开启舞台的点击,并注册指定的点击(触摸)事件
this.touchEnabled = true;
this.addEventListener(egret.TouchEvent.TOUCH_TAP, this.move, this);
this.addEventListener(egret.TouchEvent.TOUCH_MOVE, this.onMove, this);
this.addEventListener(egret.TouchEvent.TOUCH_END, this.moveEnd, this);
}
食物需要随机生成,并显示在舞台上:
private randomFood() {
//随机坐标
var tmpx = Math.random() * (this.stageW - this.radius * 2);
var tmpy = Math.random() * (this.stageH - this.radius * 2);
//新建食物对象
this.food = new Food(tmpx, tmpy, this.radius);
//显示
this.addChild(this.food);
}
当食物被吃时的事件:
private onEat() {
//在舞台上移除食物
this.removeChild(this.food);
//调用彩虹蛇吃食物的事件
this.snake.afterEat(this.food.color);
//随机产生食物
this.randomFood();
}
最后就是用户的点击事件了:我们需要理解这个过程:
这三个事件是按照顺序一步步进行的
//定时器
private timer: egret.Timer;
private during: number = 40;
private moveEvent: egret.TouchEvent;
private head: egret.Shape;
//根据点击事件调用彩虹蛇的移动方法
private move(e: egret.TouchEvent) {
this.snake.move(e, this.during);
}
//当点击拖动时
private onMove(e: egret.TouchEvent) {
//保存event
this.moveEvent = e;
//开启一个计时器
if (this.timer == null) {
this.timer = new egret.Timer(this.during);
this.timer.addEventListener(egret.TimerEvent.TIMER, this.onTimer, this);
this.timer.start();
}
}
//点击结束
private moveEnd(e: egret.TouchEvent) {
//关闭计时器
if (this.timer != null) {
this.timer.stop();
this.timer = null;
}
}
//计时器逻辑
private onTimer(e: egret.TimerEvent) {
//获取蛇头
this.head = this.snake.getHead();
//调用方法,检测蛇头和事物是否发生碰撞
if (this.hit(this.head, this.food))
//发生碰撞,则调用食物被吃事件
this.onEat();
//彩虹蛇继续移动
this.snake.move(this.moveEvent, this.during);
}
//使用包装盒检测碰撞
private hit(a, b) {
return (new egret.Rectangle(a.x + this.snake.x, a.y + this.snake.y, a.width, a.height))
.intersects(new egret.Rectangle(b.x, b.y, b.width, b.height));
}
好了,大功告成,Main.ts的完整代码如下:
class Main extends egret.DisplayObjectContainer {
/**
* 加载进度界面
* Process interface loading
*/
private loadingView: LoadingUI;
public constructor() {
super();
this.addEventListener(egret.Event.ADDED_TO_STAGE, this.createGameScene, this);
}
private food: Food;
private snake: Snake;
private stageW: number;
private stageH: number;
private radius = 30;
/**
* 创建游戏场景
* Create a game scene
*/
private createGameScene(): void {
this.stageW = this.stage.stageWidth;
this.stageH = this.stage.stageHeight;
//白色背景
var bg = new egret.Shape();
bg.graphics.beginFill(0xffffff);
bg.graphics.drawRect(0, 0, this.stageW, this.stageH);
bg.graphics.endFill();
this.addChild(bg);
this.randomFood();
this.snake = new Snake(this.stageW * 0.5, this.stageH * 0.5, this.radius, 0x000000);
this.addChild(this.snake);
this.touchEnabled = true;
this.addEventListener(egret.TouchEvent.TOUCH_TAP, this.move, this);
this.addEventListener(egret.TouchEvent.TOUCH_MOVE, this.onMove, this);
this.addEventListener(egret.TouchEvent.TOUCH_END, this.moveEnd, this);
}
private color = 0x4c8dae;
private onEat() {
this.removeChild(this.food);
this.snake.afterEat(this.food.color);
this.randomFood();
}
private randomFood() {
//显示果实
var tmpx = Math.random() * (this.stageW - this.radius * 2);
var tmpy = Math.random() * (this.stageH - this.radius * 2);
this.food = new Food(tmpx, tmpy, this.radius);
this.addChild(this.food);
//模拟被吃
// this.food.touchEnabled = true;
// this.food.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onEat, this);
}
private timer: egret.Timer;
private during: number = 40;
private moveEvent: egret.TouchEvent;
private head: egret.Shape;
private move(e: egret.TouchEvent) {
this.snake.move(e, this.during);
}
private onMove(e: egret.TouchEvent) {
this.moveEvent = e;
if (this.timer == null) {
this.timer = new egret.Timer(this.during);
this.timer.addEventListener(egret.TimerEvent.TIMER, this.onTimer, this);
this.timer.start();
}
}
private moveEnd(e: egret.TouchEvent) {
if (this.timer != null) {
this.timer.stop();
this.timer = null;
}
}
private onTimer(e: egret.TimerEvent) {
this.head = this.snake.getHead();
if (this.hit(this.head, this.food))
this.onEat();
this.snake.move(this.moveEvent, this.during);
}
private hit(a, b) {
return (new egret.Rectangle(a.x + this.snake.x, a.y + this.snake.y, a.width, a.height))
.intersects(new egret.Rectangle(b.x, b.y, b.width, b.height));
}
}
目录结构如下:
OK!大功告成,你现在可以指引着彩虹蛇在宇宙遨游啦,试着给它添加计分器吧!