canvas教程之《贪吃蛇》
本次我会带领大家用html5的新标签 canvas 来制作一个简单的贪吃蛇的游戏。本章学习需要了解canvas的基础知识和知晓如何使用canvas来画一个空心的方块。传送门:《canvas教程之初步使用》。
先睹为快
先让大家看到效果,才能有动力更好的学习下去,老师已经将贪吃蛇的游戏完成,并放在了vps上,下面放上地址:贪吃蛇
游戏的原理
要想制作 贪吃蛇 的游戏,我们首先要对这个游戏的原理有所了解。
- 游戏的目标:操作我们的蛇来吃掉更多的食物,是蛇变长,从而得到更高的分数。
- 游戏的对象:蛇,食物。
- 游戏的结束:蛇碰到可见区域的边缘,或则蛇头碰到自己(吃到自己)。
原理:其实就是我们变换两个游戏对象的状态。然后将他们的状态定时(帧率)显示在我们的canvas上。实际上这就是最简单的引擎。我们在这里使用js的setInterval函数来实现本游戏的主循环。
开始动手
我们先创建一个有canvas , 成绩显示标签,开始按钮 的html文档出来。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8"/>
<style>
body{background:#eeeeee;}
canvas{background:#ffffff;}
</style>
</head>
<body>
<!-- 我们要操作的canvas -->
<canvas id="gbcanvas" width="300" height="300"></canvas>
<br>
<!-- 显示分数的span #score -->
<span>your score : </span><span id="score">0</span>
<br>
<!-- 游戏操作提示 -->
<span>操作方法:W A D S</span>
<br>
<!-- 游戏开始的按钮,点击调用startGame()函数,该函数后面完善 -->
<button onclick="startGame();">Start Game</button>
<script type="text/javascript">
//我们的代码将在这里书写
</script>
</body>
</html>
第二步,我们把需要用到的东西都初始化以下:
var canvas = document.getElementById('gbcanvas'); // 这里通过gbCanvas获取canvas对象
var c = canvas.getContext('2d'); //这里通过canvas获取处理API的上下文context
var s = document.getElementById('score'); //把游戏的分数显示在这里
var mWidth = 300; //当前可视区域的宽,即canvas的宽
var mHeight = 300; //当前可视区域的高,即canvas的高
var unit = 5; //设定每个格子的边长
var mwid = mWidth / unit; //计算当前横向格子数量
var mhei = mHeight / unit; //计算当前竖向格子数量
var point = point = {x : 0 , y : 0}; //记录食物的坐标的变量初始化
var score = 0; //记录成绩的变量初始化
第三步,我们要创建最重要的“蛇”对象:
计划一下我们的“蛇”对象应该有以下几个方法 和 属性:
1.currOri : 蛇当前的前进方向,比如:right。
2.ori : 蛇的方向组,左右为一组,上下为一组,因为蛇不能直接改变方向为同组方向。
3.oriss : 所有允许的方向数组,值为left , down , right , up
4.mes : 蛇的身体数组,记录蛇身体每个点的坐标
5.add方法:用于吃一个食物后在身体最后追加一节。
6.move[ori]方法:向某个方向移动一格。例如moveUp,moveLeft。
7.move方法:由6调用,6是让头移动某个方向一格,然后调用此方法,让所有后面的身体跟着动。
8.changeOri方法:改变蛇当前的移动方向,即currOri的值,通过ori和oriss属性判断合法性。
9.canChangeOri方法,判断改变方向的合法性。由8调用。
10.isCrashSelf方法:判断头是否吃到身体。
11.isCrashWell方法:判断是否撞墙。
12handleAdd方法:自动处理吃东西,作用,判断当前头是否吃到食物,如果迟到则调用add方法。
代码与详尽解释:
//注意本对象,并不改变其在画布上的样子,只是负责改变状态,改变样子的另有方法
//蛇对象
var shake = {
startX : 3, //开始头x坐标
startY : 0, //开始头y坐标
currOri : 'right', //初始化方向
ori : [['left' , 'right'] , ['up' , 'down']], //相逆方向数组
oriss : ['left' , 'right' , 'up' , 'down'], //所有允许的方向,用来判断方向合法性,在canChangeOri方法
mes : [{x : 3 , y : 0} , {x : 2 , y : 0} , {x : 1 , y : 0}], //初始化蛇的身体坐标,初始长度3
//坐标为格子坐标非像素坐标
//添加一个身体的方法
add : function(){
//判断当前尾部方向
var last = this.mes[this.mes.length - 1]; //获取最后一个身体
var plast = this.mes[this.mes.length - 2]; //获取倒数第二个身体
var px = last.x - plast.x;
var py = last.y - plast.y; //根据计算最后两个身体的坐标差,来计算添加身体应在的方向
//计算新加元素位置
var newEle = {x : last.x + px , y : last.y + py}; //创建一个新身体
this.mes.push(newEle); //将新身体假如身体数组
},
//移动方向方法,下面几个方法类似,只是方向不同
moveup : function(){
var pre = this.mes[0]; //记录第一个身体,即头部的坐标
this.mes[0] = {x : pre.x , y : pre.y - 1}; //让头部的坐标像上移动一格
this.move(pre); //调用移动身体的方法
},
movedown : function(){
var pre = this.mes[0];
this.mes[0] = {x : pre.x , y : pre.y + 1};
this.move(pre);
},
moveleft : function(){
var pre = this.mes[0];
this.mes[0] = {x : pre.x - 1 , y : pre.y};
this.move(pre);
},
moveright : function(){
var pre = this.mes[0];
this.mes[0] = {x : pre.x + 1 , y : pre.y};
this.move(pre);
},
//移动身体
move : function(pre){//参数为之前第一个身体,即头部的位置对象
var tmp;
for(var i = 1 ; i < this.mes.length ; i++){ //遍历每一个身体节点
tmp = this.mes[i];
this.mes[i] = pre;
pre = tmp;
} //并且把每个节点的左边变化成前一个节点的坐标,达到依次向前的目的
},
//改变方向
changeOri : function(ori){
if(this.oriss.indexOf(ori) == -1){ //判断方向是否在允许方向内
return;
}
if(!this.canChangeOri(ori)){ //判断改变方向是否合法
return;
}
this.currOri = ori; //如果上面两个都通过,改变方向
},
//判断改变的方向是否合法
canChangeOri : function(ori){ //参数为方向字符串 例如:left
if(ori == this.currOri){ //判断方向是否为当前方向,如果是则无需操作
return false;
}
var oris = null;
for(var i in this.ori){ //判断是否改变方向为当前方向的逆方向
if(this.ori[i].indexOf(this.currOri) != -1){
oris = this.ori[i];
break;
}
}
if(oris.indexOf(ori) != -1){
return false;
}
return true;
},
//判断是否碰撞到了自己
isCrashSelf : function(){
var head = this.mes[0]; //获取头节点
for(var i = 1 ; i < this.mes.length ; i++){ //遍历身体节点
if(this.mes[i].x == head.x && this.mes[i].y == head.y){ //判断头结点是否碰撞身体
return true;
}
}
return false;
},
//判断是否撞墙
isCrashWell : function(width , height){ //参数为横竖的格子数量
var head = this.mes[0]; //获取头节点
if(head.x < 0 || head.y < 0){ //判断是否撞左上墙
return true;
}
if(head.x > (width - 1) || head.y > (height - 1)){ //判断是否撞右下墙
return true;
}
return false;
},
//处理吃东西
handleAdd : function(){
var head = this.mes[0]; //获取头节点
if(head.x == point.x && head.y == point.y){ //判断头节点是否碰撞食物节点,食物在外定义
this.add(); //调用添加身体
getPoint(); //生成一个节点
setPoint(); //画一个节点
score++; //加分
s.innerHTML = score; //显示分数
}
}
}
第四步,关于食物的代码,这里没有使用面向对象。
//生成点
function getPoint(){
point.x = Math.floor(Math.random(0 , mwid)*60);
point.y = Math.floor(Math.random(0 , mhei)*60);
}
//画点
function setPoint(){
c.rect(point.x * unit , point.y * unit , unit , unit);
}
第五步,画蛇,清屏的代码,画蛇的代码可以自己封装到蛇对象中去,很容易哦。
//画蛇
function setShake(){
for(var i = 0 ; i < shake.mes.length ; i++){
c.fullStyle = '#ffffff';
c.lineStyle = '#000000';
c.rect(shake.mes[i].x * unit , shake.mes[i].y * unit , unit ,unit);
}
c.stroke();
}
//清屏
function clear(){
c.clearRect(0 , 0 , mWidth , mHeight);
}
最后一步,开始游戏和监听键盘的代码哦。
//开始游戏
function startGame(){
clearInterval(window.looper); //终止游戏主循环
//初始化状态
shake.mes = [{x : 3 , y : 0} , {x : 2 , y : 0} , {x : 1 , y : 0}];
shake.currOri = 'right';
c.beginPath(); //开始画笔
getPoint(); //设置点
setPoint();
setShake(); //话蛇
//画
c.stroke();
//游戏主循环
window.looper = setInterval(function(){
var method = 'move' + shake.currOri + '()'; //调用方向函数
eval('shake.' + method); //执行方向方法
clear(); //清理屏幕
c.beginPath(); //开始绘制
shake.handleAdd(); //处理吃东西
setPoint(point); //设置点
setShake(); //画蛇
if(shake.isCrashWell(mwid , mhei)){ //是否撞墙,未使用是否吃自己。想用调用shake.isCrashSelf方法。
clearInterval(window.looper);
console.log('you die');
alert('you die , and your score is ' + score);
}
} , 200);
}
//键盘监听
window.onkeyup = function(key){
var ori = '';
switch(key.keyCode){
case 65:
ori = 'left';
break;
case 68:
ori = 'right';
break;
case 87:
ori = 'up';
break;
case 83:
ori = 'down';
break;
}
if(ori == ''){
return;
}
//改变蛇走向
shake.changeOri(ori);
}
贪吃蛇的代码实际上非常简单,没有用到复杂的矩阵变幻,每个动作都是在游戏住循环中绘制,把绘制图形,和操作对象状态分开来,实际上也算是最简单的一个游戏引擎。希望对大家有所启发,虽然代码详尽但是并没有带着大家一点一点的做,所以还是要认真的看代码才行,然后必须要实际写一些才可以。
留个小作业,完善这个贪吃蛇。
1.把食物封装成对象。
2.将绘制的方法也封装进对象中。这样只需要在游戏主循环中调用绘制方法即可。
希望本文对大家有所启发,good拜。