最近项目上线,近一个星期没更博了,今天来写一个经典的游戏案例——贪吃蛇。在这个简单的案例里可以体会javaScript 面向对象开发相关模式,学习使用面向对象的方式分析问题。
1.功能实现
1.1 搭建页面:放一个容器盛放游戏场景 div#map,设置样式
<div class="map" id="map"></div>
1 <style> 2 #map{ 3 background-color: #000; 4 width: 1500px; 5 height: 700px; 6 position: relative; 7 left: 0; 8 top: 0; 9 } 10 </style>
1.2 分析对象:食物对象、蛇对象、游戏对象
1.3 创建食物对象Food
⑴ 属性:位置(x,y)、大小(width、height)、颜色(color)
1 // 创建Food的构造函数,并设置属性 2 function Food(width,height,bgColor) { 3 // 食物的宽度和高度(像素) 4 this.width=width||10; 5 this.height=height||10; 6 // 食物的颜色 7 this.bgColor=bgColor||"white"; 8 }
⑵ 方法:render() 随机创建一个食物对象,并输出到map上
1 // 通过原型设置render方法,实现随机产生食物对象,并渲染到map上 2 Food.prototype.render=function (map) { 3 remove(map); 4 // 随机食物的位置,map.宽度/food.宽度,总共有多少分food的宽度,随机一下。然后再乘以food的宽度 5 this.x=Math.floor(Math.random()*(map.offsetWidth/this.width))*this.width; 6 this.y=Math.floor(Math.random()*(map.offsetHeight/this.height))*this.height; 7 // 动态创建食物对应的div 8 var newDiv=document.createElement("div"); 9 newDiv.style.position="absolute"; 10 newDiv.style.left=this.x+"px"; 11 newDiv.style.top=this.y+"px"; 12 newDiv.style.backgroundColor=this.bgColor; 13 newDiv.style.width=this.width+"px"; 14 newDiv.style.height=this.height+"px"; 15 map.appendChild(newDiv); 16 li.push(newDiv); 17 }
1.4 创建蛇对象Snake
⑴ 属性:大小(width、height)、颜色(color)、方向(direction)、身体数组对象(body)
1 // Snake构造函数 2 function Snake(width,height,bgColor,direction) { 3 // 设置每一个蛇节的宽度 4 this.width=width||10; 5 this.height=height||10; 6 this.bgColor=bgColor||"white"; 7 // 蛇的运动方向 8 this.direction=direction||"right"; 9 // 蛇的每一部分, 第一部分是蛇头 10 this.body=[ 11 {x:3,y:1}, 12 {x:2,y:1}, 13 {x:1,y:1} 14 ]; 15 }
⑵ 方法:render() 把蛇渲染到map上
1 // render方法,原理与渲染食物相同 2 Snake.prototype.render=function (map) { 3 remove(map); 4 for (var i = 0; i < this.body.length; i++) { 5 var newDiv=document.createElement("div"); 6 newDiv.style.position="absolute"; 7 newDiv.style.left=this.body[i].x*this.width+"px"; 8 newDiv.style.top=this.body[i].y*this.height+"px"; 9 newDiv.style.width=this.width+"px"; 10 newDiv.style.height=this.height+"px"; 11 newDiv.style.backgroundColor=this.bgColor; 12 map.appendChild(newDiv); 13 list.push(newDiv); 14 } 15 }
1.5 创建游戏对象Game(用来管理游戏中的所有对象和开始游戏)
⑴ 属性:food、snake、map
1 // Game构造函数 2 function Game(map) { 3 this.map=map; 4 this.snake=new Snake(); 5 this.food=new Food(); 6 that=this; 7 }
⑵ 方法:start() 开始游戏(绘制所有游戏对象)
1 // 开始游戏,渲染食物对象和蛇对象 2 Game.prototype.startGame=function () { 3 this.food.render(this.map); 4 this.snake.render(this.map); 5 autoMove(); 6 keyBind(); 7 }
// 在自调用函数中暴露Game对象 window.Game=Game;
2.游戏逻辑
2.1 蛇的move方法
⑴ 在蛇对象(snake.js)中,在Snake的原型上新增move方法
⑵ 让蛇移动起来,把蛇身体的每一部分往前移动一下
⑶ 蛇头部分根据不同的方向决定 往哪里移动
1 Snake.prototype.move=function (food,map) { 2 // 让蛇身体的每一部分往前移动一下 3 for (var i = this.body.length-1; i >0; i-- ){ 4 this.body[i].x=this.body[i-1].x; 5 this.body[i].y=this.body[i-1].y; 6 } 7 // 根据移动的方向,决定蛇头如何处理 8 switch (this.direction) { 9 case "left": 10 this.body[0].x--; 11 break; 12 case "right": 13 this.body[0].x++; 14 break; 15 case "up": 16 this.body[0].y--; 17 break; 18 case "down": 19 this.body[0].y++; 20 break; 21 default: 22 break; 23 } 24 }
//在game中测试 this.snake.move(this.food, this.map); this.snake.render(this.map);
2.2 让蛇自己动起来
⑴ 在game.js中 添加autoMove的私有方法,开启定时器调用蛇的move和render方法,让蛇动起来(私有方法即不能被外部访问的方法,使用自调用函数包裹)
1 function autoMove() { 2 var timeId=setInterval(function () { 3 this.snake.move(this.food,this.map); 4 this.snake.render(this.map); 5 // 判断蛇是否撞墙 6 var snakeHeadX=this.snake.body[0].x*this.snake.width; 7 var snakeHeadY=this.snake.body[0].y*this.snake.width; 8 if(snakeHeadX<0 || snakeHeadY<0 || snakeHeadX>=this.map.offsetWidth || snakeHeadY>=this.map.offsetHeight){ 9 clearInterval(timeId); 10 alert("Game over!"); 11 } 12 }.bind(that),50); 13 }
⑵ 在snake中添加删除蛇的私有方法,在render中调用
1 function remove(map) { 2 for (var i = 0; i < list.length; i++) { 3 map.removeChild(list[i]); 4 } 5 list.length=0; 6 }
⑶ 在game中通过键盘控制蛇的移动方向
1 function keyBind() { 2 window.onkeydown=function (e) { 3 e=e||window.event; 4 e.keyCode= e.keyCode|| e.charCode|| e.which; 5 // console.log(e.keyCode); 6 switch (e.keyCode) { 7 case 37: 8 if(this.snake.direction!="right"){ 9 this.snake.direction="left"; 10 } 11 break; 12 case 38: 13 if (this.snake.direction != "down") { 14 this.snake.direction = "up"; 15 } 16 break; 17 case 39: 18 if (this.snake.direction != "left") { 19 this.snake.direction = "right"; 20 } 21 break; 22 case 40: 23 if (this.snake.direction != "up") { 24 this.snake.direction = "down"; 25 } 26 break; 27 default: 28 break; 29 } 30 }.bind(that); 31 }
⑷ 在start方法中调用keyBind()
2.3 判断蛇是否吃到食物
在Snake的move方法中添加判断
1 // 在移动的过程中判断蛇是否吃到食物 2 var snakeHeadX=this.body[0].x*this.width; 3 var snakeHeadY=this.body[0].y*this.height; 4 var snakeTile=this.body[this.body.length-1]; 5 // 如果蛇头和食物的位置重合代表吃到食物 6 if(snakeHeadX==food.x && snakeHeadY==food.y){ 7 // 吃到食物,往蛇节的最后加一节 8 this.body.push({ 9 // 食物的坐标是像素,蛇的坐标是几个宽度,进行转换 10 x:snakeTile.x, 11 y:snakeTile.y 12 }); 13 // 把现在的食物对象删除,并重新随机渲染一个食物对象 14 food.render(map); 15 }
★ ★ 自调用函数的参数
1 (function (window, undefined) { 2 var document = window.document; 3 }(window, undefined))
⑴ 传入window对象:代码压缩的时候,可以把function (window) 压缩成 function (w)
⑵ 传入undefined:把undefined作为函数的参数(当前案例没有使用) ,防止undefined 被重新赋值,因为在有的老版本的浏览器中 undefined可以被重新赋值
★ ★ 关于自调用函数的问题
⑴ 如果存在多个自调用函数要用分号分割,否则语法错误
1 // 下面代码会报错 2 (function () { 3 }()) 4 5 (function () { 6 }()) 7 // 所以代码规范中会建议在自调用函数之前加上分号 8 // 下面代码没有问题 9 ;(function () { 10 }()) 11 12 ;(function () { 13 }())
⑵ 当自调用函数前面有函数声明时,会把自调用函数作为参数
1 // 所以建议自调用函数前,加上; 2 var a = function () { 3 alert('11'); 4 } 5 6 (function () { 7 alert('22'); 8 }())