最近前端以及JavaScript很火,再加上上课web的需要,感觉是时候系统的学学HTML5了,之前和同学合作用html5的canvas开发了一个贪吃蛇改进版的小游戏,很有趣,当时的核心代码是同学敲得,而我也在忙别的事情一直没有回过头来研究一下。好在快一年的时间了,自己也学了很多种UI的皮毛,见得多了,再看canvas感觉就是只要有文档就能直接用了。
这里分享一下开发的过程吧。
效果图。
还有穿墙功能。
前置技能
使用canvas。
所需工具以及前置技能:支持HTML5的浏览器,html+css, javascript,Jquery
canva是HTML5中新特性的一种,直接可以进行绘图,和其他UI差别不大。
布局
首先,我们写个index.html,并进行简单的布局及样式。
设置整个画布为720 x 480 的大小。每个蛇身、食物的半径为12,所以一行能放720 / 24 = 30个,每列能放 480 / 24 = 20个元素。
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>Snake</title>
<style type="text/css"> //简单的定义了一下样式
body{
background: #dddddd;
}
#canvas1{
border:#8b4513 solid 5px;
border-radius:5px;
margin: :10px;
background: #ffffff;
display:block;
}
</style>
</head>
<body>
<div class="container" id=container> //简单的布局
<center>
<button id=startGame>Start Game</button>
<canvas id="canvas1" width="720" height="480">
canvas not supported
</canvas>
<span >Score</span> //得分
<input type="text" id=score name=score disabled="disabled">
<span >Level</span> //关卡
<input type="text" id=level name=level disabled="disabled">
</center>
</div>
</body>
<script src="../bootstrap/js/jquery.min.js"></script> //引入jquery
</html>
主逻辑
然后我们编写主逻辑。
首先获取Canvas对象,然后设置网页的键盘监听器(接受上下左右的按键),接着通过Jquery获取StartGame这个按钮的事件。
在引入Jquery的代码下面插入一段JS脚本。
<script type="text/javascript">
var canvas = document.getElementById('canvas1'); //获取canvas画布
var ctxt = canvas.getContext('2d'); //以后就用ctxt来在canvas画布上绘画
document.addEventListener('keydown',keyDownCheck,false); //设置键盘按下的监听事件,通过keyDownCheck这个函数处理,
$("#startGame").click(function(){
beginGame(1); // 通过beginGame(1)这个函数来开始游戏,1代表了开始的关卡
});
</script>
好,这样我们游戏的外部框架出来了,接着我们往里深入,来实现贪吃蛇的主体逻辑。
Snake类
首先,我们先创建一个Snake的类,JavaScript的类用function就可以实现。
贪吃蛇需要有身子,身子由什么组成呢? 因为是2D游戏,所以由一个个相连的二位坐标点组成。
到这里我们想到,蛇身由点组成,那么食物也可由点组成,于是这里我们把点写成一个类,num属性代表食物的得分。
function Pos(x,y,num = 2){
this.x = x;
this.y = y;
this.num = num;
}
this.body = [new Pos(20,15), new Pos(21,15),new Pos(22,15),
new Pos(23,15), new Pos(24,15),new Pos(25,15)]; //定义蛇身,从20 ~ 25相邻的一串点
当然食物(障碍物)obstacle,我们也用一样的方法定义了。
function Obstacle(){
this.body = [new Pos(0,0),new Pos(0,1),new Pos(1,0)
,new Pos(1,1),new Pos(29,19),new Pos(10,10),new Pos(11,11)];
}
有了蛇身,还要定义蛇当前行进的方向。
这里我们规定 1 ,2 ,3 ,4 分别为上下左右
this.dir = 3; // 初始向左
有了身子方向,接着我们要想怎么去移动move。
这里提供一个思路。
首先,我们通过蛇头,和方向判断出蛇下一步要走的那个点的坐标。我们记做head,记录下这一坐标。然后从蛇尾遍历到蛇头,每一个的坐标赋值为其前一个位置的坐标,而蛇头就用head进行赋值,这样就能实现移动了。
当然,有了head我们还能做更多的事,判断head所在位置是不是墙,是不是蛇身,是不是食物,我们就能实现判断是否游戏结束,以及得分情况了。
this.move = function(level){
var isEat = false;
var head;
if(this.dir == 1)
head = new Pos(this.body[0].x,this.body[0].y-1);
else if(this.dir == 2)
head = new Pos(this.body[0].x,this.body[0].y+1);
else if(this.dir == 3)
head = new Pos(this.body[0].x-1,this.body[0].y);
else if(this.dir == 4)
head = new Pos(this.body[0].x+1,this.body[0].y);
//判定是否死亡
if(level == 1){ //可以穿墙 ,这里设置第一关可以穿越围墙到另一面
if(head.x < 0) head.x = 29;
if(head.x > 29) head.x = 0;
if(head.y < 0) head.y = 19;
if(head.y > 19) head.y = 0;
}
else if (head.x <0 || head.x > 29 || head.y < 0 || head.y > 20) //碰到边界
this.dead = true;
if(!this.dead){ //吃到自身
for(i=0;i<snake.body.length;i++){
if(snake.body[i].x == head.x && snake.body[i].y == head.y ){
this.dead = true ;
break;
}
}
}
if(this.dead){
GameOver(); return; //如果死亡,则执行GameOver函数。
}
//判定是否吃到食物
for(i = 0;i < obstacle.body.length;i++){
if(head.x == obstacle.body[i].x && head.y == obstacle.body[i].y){
this.score += obstacle.body[i].num; //增加得分
this.body.splice(0,0,obstacle.body[i]); //在头部增加元素。
obstacle.body.splice(i,1); // 删除i位置的一个元素
isEat = true;
}
}
if(!isEat){ //没有吃到就每个蛇身往前移动一步,直接用前一个蛇身赋值即可。
for(i = this.body.length-1;i>=0;i--){
if(i == 0)
this.body[i].x = head.x , this.body[i].y = head.y;
else
this.body[i].x = this.body[i-1].x,this.body[i].y = this.body[i-1].y;
}
/* 另一种移动方法 this.body.splice(0,0,head); // 吃掉头
this.body.pop(); // 删除尾部 ,这样就移动了!*/
}
生成食物
吃到食物之后,为了让游戏进行下去,我们还要不断地生成食物。
直接用Math.Random()函数生成待生成食物的x,y坐标,然后判断是否当前坐标已经被占用,如果占用就再生成一次。
if(isEat){ // 添加食物
var isOcupy = false;
var tmp;
do{
isOcupy = false;
tmp = createObstacle();
for(i=0;i<snake.body.length;i++){
if(snake.body[i].x == tmp.x && snake.body[i].y == tmp.y){
isOcupy = true;break;
}
}
if(!isOcupy){
for(i=0;i<obstacle.body.length;i++){
if(obstacle.body[i].x == tmp.x && obstacle.body[i].y == tmp.y){
isOcupy = true;break;
}
}
}
}while(isOcupy);
obstacle.body.push(tmp);
}
}
function createObstacle(){
var tmp = new Pos(Math.floor(Math.random()*30), Math.floor(Math.random()*20));
tmp.num = Math.random() < 0.1 ? 4: 2; // 出现4和2的比例 1 : 9
return tmp;
}
这样,整体的游戏逻辑算是基本实现了。
接下来完成剩余的工作。
接受键盘数据
主要是上下左右,除了方向键,还增添了wasd,同时保证了按相反的方向无效,因为贪吃蛇不会直接回头走。
function keyDownCheck(event){
var e=event||window.event||arguments.callee.caller.arguments[0]; //各种兼容下获取event
// alert(e.keyCode);
if(e.keyCode == 87 || e.keyCode == 38){ // up
if(snake.dir == 2)
return;
snake.dir = 1;
}
else if(e.keyCode == 83 || e.keyCode == 40){ // down
if(snake.dir == 1)
return;
snake.dir = 2;
}
else if(e.keyCode == 65 || e.keyCode == 37){ // left
if(snake.dir == 4)
return;
snake.dir = 3;
}
else if(e.keyCode == 68 || e.keyCode == 39){ //right
if(snake.dir == 3)
return;
snake.dir = 4;
}
}
主体循环以及绘图
2D游戏应该都是这样,游戏数据不停的的变化,界面也在不停的刷新。我们用setInterval来设立一个定时器,每隔一段时间刷新一下屏幕。再刷新的同时执行snake的move函数,这样就能让贪吃蛇不停的往前走了。当然最重要的,是要让这个后台数据变化在屏幕上显示出来。这里就用到了canvas的绘图技术了。
首先是开始游戏的函数。
function beginGame(level){
if(gameTimer)
clearInterval(gameTimer); //清空计时器
snake = new Snake(); //为贪吃蛇申请对象
obstacle = new Obstacle(); //为障碍物申请对象
gameTimer = setInterval(function(){ //设置定时器
snake.move(level);
draw(level); //绘图函数。
$("#score").val(snake.score); //更新成绩
$("#level").val(level);
},gameSpeed[level-1]);
}
var gameSpeed = [240,210,180,150]; // 游戏速度
接着draw函数。
function draw(level){
// 画布大小 720 x 480, 每个元素半径12
// X = 0 ~ 29 , Y = 0 ~ 19
ctxt.clearRect(0,0,canvas.width,canvas.height); //清空我们的画布
ctxt.beginPath(); //开始画
ctxt.rect(0,0,canvas.width,canvas.height);
ctxt.fillStyle = "black";
ctxt.closePath();
ctxt.fill(); //填充
for(i=0;i<snake.body.length;i++){ //绘制蛇身
ctxt.beginPath();
//arc(X,Y,Radius,StartAngle,endAngle,anticlockwise)
ctxt.arc(snake.body[i].x*24+12,snake.body[i].y*24+12,12,0,Math.PI*2,true); //画圆
ctxt.fillStyle = "green"; //颜色
ctxt.closePath();
ctxt.fill(); //填充
}
for(i=0;i<obstacle.body.length;i++){ //画食物(障碍物)
ctxt.beginPath();
ctxt.arc(obstacle.body[i].x*24+12,obstacle.body[i].y*24+12,12,0,Math.PI*2,true);
ctxt.fillStyle = "red";
ctxt.closePath();
ctxt.fill();
}
}
GameOver函数。
function GameOver(){
alert('GameOver!');
clearInterval(gameTimer); //为了保险,清空一下计数器
$("#score").val(0); //得分置为0
$("#level").val(1); //关卡置为1
snake.body = null; //蛇身置为空
}
接下来大家还可以为这个游戏加上音效,背景音乐,或者改造成其他版本的贪吃蛇,例如2048版贪吃蛇,相信如果你仔细学习了这个代码,扩充下去不难做到。
这个项目我已经放到了github上,欢迎fork
https://github.com/chaiwenjun000/webSnake