该项目利用ts语言中的面向对象的思想完成。
学完ts项目之后,拿此项目练手是一个很不错的选择,这里是一个js->ts的过程 ,js是一门面向过程的语言,而ts是一门面向对象的语言这就是这两门语言的最大区别。
在正式做这个项目之前要先明确贪吃蛇游戏可以分为几个模块,在这里我们可以想象一下,贪吃蛇中可以分为:食物、计分板、蛇这三个对象。那接下来就按照这个顺序一一说明各个对象应该如何完成。
对于一个对象来说它无非就是包含方法和属性两点,分析一下食物它具有的属性应该有:坐标位置(X,Y)。而食物这个对象所要包含的方法,就是改变坐标位置的方法,因为在蛇吃到食物之后 食物要相应的发生变化change()方法。
change():调用随机数生成一个位置。
此类的代码:
class Food{
elements: HT0MLElement;
constructor(){
//直接写可能会报错,因为取不到这个div,
//所以会有报错,但我们肯定能获取到 所以用叹号表明可以获取。
//这就是断言的一种
this.elements=document.getElementById('food')!;
}
//获取食物X轴
get X(){
return this.elements.offsetLeft;
}
get Y(){
return this.elements.offsetTop;
}
change(){
//食物位置最小0 最大290 且因为蛇每次移动10px,所以食物要被10整除
//生成随机位置 round向上取整 floor向下
let top=Math.round(Math.random()*29)*10
let left=Math.round(Math.random()*29)*10
this.elements.style.top=top+'px'
this.elements.style.left=left+'px'
}
}
export default Food;
这里涉及到了几个ts的知识点:
1、类型断言:指的就是当我们的编译器无法判断出某变量是某类型时,但我们自己很肯定就是一个类型就可对此变量进行断言,语法如下:
XX as String
<String>XX
2、创建每个对象的时候,它都会相应的对应一个HTMLElement元素,详见代码第一行,这其实就是说明对象->实例
3、ts中get X(){}
这个X就是一个对象的属性,在这个函数中直接return 你想要表达的属性值就可以设置这个属性,这类似于vue中的一个getter,方便我们在获取属性前对属性进行一些操作。
对于此类来说,它应该含有的属性有分数、等级两个属性。
在这里实现此类的思路是:蛇吃一个食物它的分数score就+1,加到某一个数值其等级就+1,所以此类应该含有两个方法,分别是addScore()和addLevel(),具体的实现代码如下:
addScore():使score属性自增1,然后改变此属性对应的dom显示值后并当该值达到升level时调用addLevel方法。
addLevel():再没达到最高级前,调用该方法则level自增1。
class ScorePannel{
// 记录分数和等级
score=0;
level=1;
//分数 等级所对应的dom元素
scoreEle:HTMLElement;
levelEle:HTMLElement;
// 设置变量限制等级 增加可扩展性,传值为传的值 不传为10
maxLevel:number;
// 设置变量表示多少分升一级
upScore:number;
constructor(maxLevel:number=10,upScore:number=10){
this.scoreEle=document.getElementById('score')!;
this.levelEle=document.getElementById('level')!;
this.maxLevel=maxLevel;
this.upScore=upScore
}
// 加分方法 分数自增
addScore(){
this.score++
this.scoreEle.innerHTML=this.score+'';
// 判断分数 确定是否升级
if(this.score%this.upScore===0){
this.addLevel()
}
}
// 等级增加 自增后赋值 设置一个等级判断
addLevel(){
if(this.level<this.maxLevel)
{
this.levelEle.innerHTML=++this.level+'';
}
}
}
// 测试代码
// const scorePannel=new ScorePannel(100,2)
// for(let i=0;i<200;i++)
// {
// scorePannel.addScore()
// }
export default ScorePannel
这里为了增加升级规则的扩展性,将相关变量定义成构造函数参数的形式,那么在以后开发的时候也可以吸取这一点,为了提高可扩展性,可将想要扩展的变量作为参数。
由于蛇最初的时候只有一个头部 ,所以这里的蛇的html结构如下:
Snake这个类的属性可拆分成:头+身体两部分,也就是说它含有head、body两个属性,但尤其要注意的是head也是属于body的一部分的,而在吃到食物后又需要向其中加入div使蛇变长,所以还需要获取id为snake的这个div作为一个属性以方便后续增加身体;再来分析这个蛇应该具有的方法,首先是控制蛇移动的的方法moveBody是必不可少的,然后由于要增加蛇的长度所以应该存在一个addBody。此外,我们还需要考虑当蛇身体变得很长的时候,自己撞到自己的这种情况,也就需要额外增加一个checkHeadBody的方法。
addBody:比较简单的方法,基于html结构直接向id为snake的div中添加新的dom元素。
moveBody:在set 蛇的Head坐标时调用此方法,在此方法中去遍历除蛇头以外的身体dom节点,使这些节点的位置等于上一个节点的位置,从而实现身体的移动,如:body[3]的坐标在移动的时候就等于body[2]的坐标。
checkHeadBody这个方法就是去遍历除头部以外的身体节点与head的节点是否相等,相等时就是撞到自己了。
setX,setY这两个函数可以理解为设置蛇头下一瞬间的位置,由于蛇某一时刻只会有X一个值或者Y一个值发生变化,所以设置蛇头Head的坐标之前可以判断一下它是否发生变化,没有变化的会就直接return,在判断之后还要检测下当下的值是否超出蛇面板的边界范围了,超出时则说明撞墙了;还有一点就是无论是水平移动还是垂直移动,蛇不可能直接垂直掉头,这里也就要判断一下蛇头下一瞬间所处的坐标位置与与身体第一节坐标(body[1])是否一样判断蛇是否掉头,如果发现是垂直调头了,就进一步判断它是想向那边发出不合理的掉头,并让这个不合理的掉头失效即蛇头位置依旧按时间正确变化。 判断好舌头后继续调用moveBody和checkHeadBody这两个方法了。
具体实现细节如下:
由于
class Snake{
// 蛇头元素 蛇这个div中的第一个子元素
head:HTMLElement;
// 包括蛇头 整个蛇 HTMLCollection会实时更新
bodys:HTMLCollection;
element:HTMLElement;
constructor(){
this.element=document.getElementById('snake')!;
// 断言数据类型 获取snake的第一个div子元素
this.head=document.querySelector('#snake > div') as HTMLElement;
//head属于body的一部分
this.bodys=document.getElementById('snake')!.getElementsByTagName('div');
}
// 获取蛇头坐标:
get X(){
return this.head.offsetLeft;
}
get Y(){
return this.head.offsetTop;
}
// 设置蛇头坐标
set X(value:number){
if(this.X===value)
{
return
}
if(value<0||value>290)
{
throw new Error('蛇撞墙了x')
}
// 该判断句通过判断蛇头的坐标(value) 与身体第一节坐标(body[1])是否一样判断蛇是否掉头
if(this.bodys[1]&&(this.bodys[1] as HTMLElement).offsetLeft===value){
// 此判断句代表下一秒,蛇本来向左(没体现,通过上个判断句判断出来掉头
// 以及下一秒向右走推断出来)
// 判断是向右走
//this.x是上一瞬间的蛇头位置
if(value>this.X){
//下一瞬间蛇头的位置继续向左 所以减10
value=this.X-10
}
}
this.moveBody()
this.head.style.left=value+'px'
this.checkHeadBody()
}
set Y(value:number){
if(this.Y===value)
{
return
}
// 蛇撞墙了就抛出异常
if(value<0||value>290)
{
throw new Error('蛇撞墙了y')
}
// 修改X时是在修改水平坐标 蛇在左右移动 向左移动时不可以直接转向右
if(this.bodys[1]&&(this.bodys[1] as HTMLElement).offsetTop===value){
if(value>this.Y){
value=this.Y-10
}else{
value=this.Y+10
}
}
this.moveBody()
// moveBody在设置head之前调用是因为,head设置的是下一瞬间的移动坐标,所以要先将这一瞬间的
// 身体移动完
this.head.style.top=value+'px'
this.checkHeadBody()
}
// 蛇吃到食物后增加身体长度的方法
addBody(){
// 向element中加div
let newBody=document.createElement('div')
this.element.appendChild(newBody)
}
// 头移动时调用此方法
moveBody(){
// 将第四结等于第三结位置->三等于二->二等于一
// 遍历所有的身体
console.log(this.bodys.length)
for(let i=this.bodys.length-1;i>0;i--)
{
// 获取前边身体位置 类型断言
let X=(this.bodys[i-1] as HTMLElement).offsetLeft;
let Y=(this.bodys[i-1] as HTMLElement).offsetTop;
// 身体改了
(this.bodys[i] as HTMLElement).style.left=X+'PX';
(this.bodys[i] as HTMLElement).style.top=Y+'PX';
}
}
checkHeadBody(){
// 获取所有身体 检查是否和蛇头坐标重叠
for(let i=1;i<this.bodys.length;i++){
let bd=this.bodys[i] as HTMLElement
if(this.X===bd.offsetLeft&&this.Y===bd.offsetTop){
throw new Error('撞到自己了')
}
}
}
}
export default Snake;
PS:这里的报错都是通过抛出异常的形式实现的,因为接下来还要实现一个GameControl类去控制蛇的移动,在这个类里就可以进行异常捕获。
这个类中主要是取控制上述的三个类,从而实现游戏的运行,所以它除了调用以上三个类以外,自身还需要增添的属性应该有direction(判断蛇移动的方向)、isLive(判断蛇是否活着,即游戏是否要终止)。对于方法来说,该类应该实现的方法有init()开始游戏、keydownHandler()按键检查、run()蛇移动以及checkEat()检查吃食物这四个方法。
init(): 监听鼠标按键,并调用run()。
keydownHandler() 监听按下键,并设置属性direction值,方便run方法的实现。
run() 根据按键,确定X,Y的坐标变化,若为上则Y-10,下Y+10,左X-10,右X+10,然后调用checkEat确定是否吃到食物,最后再try修改蛇的X Y坐标,这里用try的原因是为了捕获Snake类中的异常,然后在异常时将isLive调整为false。最后也是最关键的要定时调用run方法,时间越短速度越快,而调用时间则可以根据level去评定,从而实现速度的变化。
checkEat() 这里直接判断蛇头位置与食物位置即可,然后调用Food类的change()以及scorePannel类的addScore(),最后调用snake类的addBody()方法即可。
详细代码如下:
import Snake from "./Snake";
import Food from "./Food";
import ScorePannel from "./ScorePannel";
//控制所有类的统一类
class GameControl {
// 蛇
snake: Snake;
// 食物
food: Food;
//记分牌
scorePannel: ScorePannel;
// 创建一个属性存储蛇的移动方向(按键方向)
direction: string = ''
// 创建属性记录蛇是否活着
isLive=true;
constructor() {
// 声明三个新的类
this.snake = new Snake();
this.food = new Food();
this.scorePannel = new ScorePannel(10,1);
this.init()
}
// 开始游戏
init() {
// 绑定键盘按键按下的事件 注意this指向 需要用bind绑定到gamecontrol这个对象上 否则就是指向document了
document.addEventListener('keydown', this.keydownHandler.bind(this))
// 调用run方法 蛇移动
this.run()
}
// 按下键盘的响应函数 返回字符串
// Chrome: ArrowUp IE: up
// ArrowDown down
// ArrowRight right
// ArrowLeft left
keydownHandler(event: KeyboardEvent) {
console.log(event.key)
// 检查用户是否按了方向键
// 修改direction
this.direction = event.key
}
// 蛇移动
run() {
// 向上 top减少
// 向下 top增加
// 向右 left增加
// 向左 left减少
let X = this.snake.X
let Y = this.snake.Y
// 根据按键方向修改X Y值
switch (this.direction) {
case "ArrowUp":
case "Up":
Y-=10;
break;
case "ArrowDown":
case "Down":
Y+=10;
break;
case "ArrowLeft":
case "Left":
X-=10;
break;
case "ArrowRight":
case "Right":
X+=10;
break;
}
// 检测是否吃到食物
this.checkEat(X,Y)
// 修改蛇的X Y
try{
this.snake.X=X;
this.snake.Y=Y;
}catch(e){
alert((e as Error).message)
this.isLive=false
}
// 开启定时调用run 调用的时间就是速度
this.isLive&&setTimeout(this.run.bind(this),300-(this.scorePannel.level-1)*30)
}
// 定义一个方法 检测蛇是否吃到食物
checkEat(X:number,Y:number){
if (X===this.food.X&&Y===this.food.Y){
this.food.change()
this.scorePannel.addScore()
this.snake.addBody()}
}
}
let test=new Food()
console.log(test.X,test.Y)
export default GameControl
将gameControl类导出到webpack打包的入口文件,并new一个对象即可实现游戏的运行,运行界面如下:
ts与js最大的不同就是ts是一个面向对象开发的过程,而js则是面向过程开发的过程,ts的开发要更抽象一些,但它的逻辑从上述开发可以看出是比较清晰的。