JS+HTML實現(xiàn)經(jīng)典游戲吃豆人
前言
很早就知道canvas,當時一直沒應(yīng)用到,最近閑來無事就順便寫寫看。吃豆游戲可以說是我們80,90后共同的回憶錄,小時候常常在學(xué)習(xí)機上玩,所以也就有了強烈的欲望去寫。為了寫這個游戲,看了很多吃豆人游戲的相關(guān)信息,包括游戲規(guī)則,歷史,其它相關(guān)游戲設(shè)計。又去了各個小游戲平臺玩了下,找些靈感?。。【瓦B最簡單的配色,也是反復(fù)斟酌幾個小時確定的方案……哈哈,不容易啊代碼分兩部分,游戲引擎和游戲主程序,引擎用來類的定義,管理動畫場景,事件定義等等……游戲主程序主要組織業(yè)務(wù)邏輯。算面向?qū)ο缶幊贪桑兪止ご蛟焓悄腥司统怨馑卸棺?!!
項目結(jié)構(gòu)

因需要四個文件即可實現(xiàn),index.html 游戲主頁,index.js 主頁控制,game.js 游戲控制,favicon.png 圖標。代碼簡單,易懂。后續(xù)小伙伴們還可以自己加入音樂!可以部署到服務(wù)器上,也可用瀏覽器直接打開index.html玩耍?。?!
HTML代碼
<html>
<head>
<meta charset="utf8">
<title>Pac-Man . 吃豆游戲</title>
<link rel="shortcut icon" href="favicon.png" rel="external nofollow" >
<style>
body{background-color: #292929}
*{padding:0;margin:0;}
.wrapper{
width: 960px;
margin:0 auto;
line-height:36px;
text-align:center;
color:#999;
}
canvas{display:block;background: #000;}
.mod-botton{
height: 32px;
padding: 15px 0;
text-align: center;
}
</style>
</head>
<body>
<div class="wrapper">
<canvas id="canvas" width="960" height="640">不支持畫布</canvas>
<p>按[空格]暫停或繼續(xù)</p>
</div>
<script src="game.js"></script>
<script src="index.js"></script>
</body>
</html>JS代碼
//主程序,業(yè)務(wù)邏輯
(function(){
var _DATA = [ //地圖數(shù)據(jù)
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],
[1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],
[1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,0,1],
[1,0,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,0,1],
[1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,1],
[1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,0,1,1,1,2,2,1,1,1,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,0,1,2,2,2,2,2,2,1,0,1,1,0,1,1,1,1,1,1],
[0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,2,2,1,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,1,1,0,1,1,0,1,2,2,2,2,2,2,1,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],
[1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],
[1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1],
[1,1,1,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,1,1],
[1,1,1,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,1,1],
[1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1],
[1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
],
_GOODS = { //能量豆
'1,3':1,
'26,3':1,
'1,23':1,
'26,23':1
},
_COS = [1,0,-1,0],
_SIN = [0,1,0,-1],
_COLOR = ['#F00','#F93','#0CF','#F9C'],//紅,橙,
_LIFE = 3,
_SCORE = 0; //得分
var game = new Game('canvas');
//啟動頁
(function(){
var stage = game.createStage();
//logo
stage.createItem({
x:game.width/2,
y:game.height*.45,
width:100,
height:100,
frames:3,
draw:function(context){
var t = Math.abs(5-this.times%10);
context.fillStyle = '#FFE600';
context.beginPath();
context.arc(this.x,this.y,this.width/2,t*.04*Math.PI,(2-t*.04)*Math.PI,false);
context.lineTo(this.x,this.y);
context.closePath();
context.fill();
context.fillStyle = '#000';
context.beginPath();
context.arc(this.x+5,this.y-27,7,0,2*Math.PI,false);
context.closePath();
context.fill();
}
});
//游戲名
stage.createItem({
x:game.width/2,
y:game.height*.6,
draw:function(context){
context.font = 'bold 42px Helvetica';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillStyle = '#FFF';
context.fillText('Pac-Man',this.x,this.y);
}
});
//版權(quán)信息
stage.createItem({
x:game.width-12,
y:game.height-5,
draw:function(context){
context.font = '14px Helvetica';
context.textAlign = 'right';
context.textBaseline = 'bottom';
context.fillStyle = '#AAA';
context.fillText('? passer-by.com',this.x,this.y);
}
});
//事件綁定
stage.bind('keydown',function(e){
switch(e.keyCode){
case 13:
case 32:
game.nextStage();
break;
}
});
})();
//游戲主程序
(function(){
var stage,map,beans,player,times;
stage = game.createStage({
update:function(){
var stage = this;
if(stage.status==1){ //場景正常運行
items.forEach(function(item){
if(map&&!map.get(item.coord.x,item.coord.y)&&!map.get(player.coord.x,player.coord.y)){
var dx = item.x-player.x;
var dy = item.y-player.y;
if(dx*dx+dy*dy<750&&item.status!=4){ //物體檢測
if(item.status==3){
item.status = 4;
_SCORE += 10;
}else{
stage.status = 3;
stage.timeout = 30;
}
}
}
});
if(JSON.stringify(beans.data).indexOf(0)<0){ //當沒有物品的時候,進入結(jié)束畫面
game.nextStage();
}
}else if(stage.status==3){ //場景臨時狀態(tài)
if(!stage.timeout){
_LIFE--;
if(_LIFE){
stage.resetItems();
}else{
game.nextStage();
return false;
}
}
}
}
});
//繪制地圖
map = stage.createMap({
x:60,
y:10,
data:_DATA,
cache:true,
draw:function(context){
context.lineWidth = 2;
for(var j=0; j<this.y_length; j++){
for(var i=0; i<this.x_length; i++){
var value = this.get(i,j);
if(value){
var code = [0,0,0,0];
if(this.get(i+1,j)&&!(this.get(i+1,j-1)&&this.get(i+1,j+1)&&this.get(i,j-1)&&this.get(i,j+1))){
code[0]=1;
}
if(this.get(i,j+1)&&!(this.get(i-1,j+1)&&this.get(i+1,j+1)&&this.get(i-1,j)&&this.get(i+1,j))){
code[1]=1;
}
if(this.get(i-1,j)&&!(this.get(i-1,j-1)&&this.get(i-1,j+1)&&this.get(i,j-1)&&this.get(i,j+1))){
code[2]=1;
}
if(this.get(i,j-1)&&!(this.get(i-1,j-1)&&this.get(i+1,j-1)&&this.get(i-1,j)&&this.get(i+1,j))){
code[3]=1;
}
if(code.indexOf(1)>-1){
context.strokeStyle=value==2?"#FFF":"#09C";
var pos = this.coord2position(i,j);
switch(code.join('')){
case '1100':
context.beginPath();
context.arc(pos.x+this.size/2,pos.y+this.size/2,this.size/2,Math.PI,1.5*Math.PI,false);
context.stroke();
context.closePath();
break;
case '0110':
context.beginPath();
context.arc(pos.x-this.size/2,pos.y+this.size/2,this.size/2,1.5*Math.PI,2*Math.PI,false);
context.stroke();
context.closePath();
break;
case '0011':
context.beginPath();
context.arc(pos.x-this.size/2,pos.y-this.size/2,this.size/2,0,.5*Math.PI,false);
context.stroke();
context.closePath();
break;
case '1001':
context.beginPath();
context.arc(pos.x+this.size/2,pos.y-this.size/2,this.size/2,.5*Math.PI,1*Math.PI,false);
context.stroke();
context.closePath();
break;
default:
var dist = this.size/2;
code.forEach(function(v,index){
if(v){
context.beginPath();
context.moveTo(pos.x,pos.y);
context.lineTo(pos.x-_COS[index]*dist,pos.y-_SIN[index]*dist);
context.stroke();
context.closePath();
}
});
}
}
}
}
}
}
});
//物品地圖
beans = stage.createMap({
x:60,
y:10,
data:_DATA,
frames:8,
draw:function(context){
for(var j=0; j<this.y_length; j++){
for(var i=0; i<this.x_length; i++){
if(!this.get(i,j)){
var pos = this.coord2position(i,j);
context.fillStyle = "#F5F5DC";
if(_GOODS[i+','+j]){
context.beginPath();
context.arc(pos.x,pos.y,3+this.times%2,0,2*Math.PI,true);
context.fill();
context.closePath();
}else{
context.fillRect(pos.x-2,pos.y-2,4,4);
}
}
}
}
}
});
//得分
stage.createItem({
x:690,
y:100,
draw:function(context){
context.font = 'bold 28px Helvetica';
context.textAlign = 'left';
context.textBaseline = 'bottom';
context.fillStyle = '#C33';
context.fillText('SCORE',this.x,this.y);
context.font = '28px Helvetica';
context.textAlign = 'left';
context.textBaseline = 'top';
context.fillStyle = '#FFF';
context.fillText(_SCORE,this.x+12,this.y);
}
});
//狀態(tài)文字
stage.createItem({
x:690,
y:320,
frames:25,
draw:function(context){
if(stage.status==2&&this.times%2){
context.font = '24px Helvetica';
context.textAlign = 'left';
context.textBaseline = 'center';
context.fillStyle = '#09F';
context.fillText('PAUSE',this.x,this.y);
}
}
});
//生命值
stage.createItem({
x:705,
y:540,
width:30,
height:30,
draw:function(context){
for(var i=0;i<_LIFE-1;i++){
var x=this.x+40*i,y=this.y;
context.fillStyle = '#FFE600';
context.beginPath();
context.arc(x,y,this.width/2,.15*Math.PI,-.15*Math.PI,false);
context.lineTo(x,y);
context.closePath();
context.fill();
}
}
});
//NPC
for(var i=0;i<4;i++){
stage.createItem({
width:30,
height:30,
orientation:3,
color:_COLOR[i],
location:map,
coord:{x:12+i,y:14},
vector:{x:12+i,y:14},
type:2,
frames:10,
speed:1,
timeout:Math.floor(Math.random()*120),
update:function(){
var new_map;
if(this.status==3&&!this.timeout){
this.status = 1;
}
if(!this.coord.offset){ //到達坐標中心時計算
if(this.status==1){
if(!this.timeout){ //定時器
new_map = JSON.parse(JSON.stringify(map.data).replace(/2/g,0));
var id = this._id;
items.forEach(function(item){
if(item._id!=id&&item.status==1){ //NPC將其它所有還處于正常狀態(tài)的NPC當成一堵墻
new_map[item.coord.y][item.coord.x]=1;
}
});
this.path = map.finder({
map:new_map,
start:this.coord,
end:player.coord
});
if(this.path.length){
this.vector = this.path[0];
}
}
}else if(this.status==3){
new_map = JSON.parse(JSON.stringify(map.data).replace(/2/g,0));
var id = this._id;
items.forEach(function(item){
if(item._id!=id){
new_map[item.coord.y][item.coord.x]=1;
}
});
this.path = map.finder({
map:new_map,
start:player.coord,
end:this.coord,
type:'next'
});
if(this.path.length){
this.vector = this.path[Math.floor(Math.random()*this.path.length)];
}
}else if(this.status==4){
new_map = JSON.parse(JSON.stringify(map.data).replace(/2/g,0));
this.path = map.finder({
map:new_map,
start:this.coord,
end:this._params.coord
});
if(this.path.length){
this.vector = this.path[0];
}else{
this.status = 1;
}
}
//是否轉(zhuǎn)變方向
if(this.vector.change){
this.coord.x = this.vector.x;
this.coord.y = this.vector.y;
var pos = map.coord2position(this.coord.x,this.coord.y);
this.x = pos.x;
this.y = pos.y;
}
//方向判定
if(this.vector.x>this.coord.x){
this.orientation = 0;
}else if(this.vector.x<this.coord.x){
this.orientation = 2;
}else if(this.vector.y>this.coord.y){
this.orientation = 1;
}else if(this.vector.y<this.coord.y){
this.orientation = 3;
}
}
this.x += this.speed*_COS[this.orientation];
this.y += this.speed*_SIN[this.orientation];
},
draw:function(context){
var isSick = false;
if(this.status==3){
isSick = this.timeout>80||this.times%2?true:false;
}
if(this.status!=4){
context.fillStyle = isSick?'#BABABA':this.color;
context.beginPath();
context.arc(this.x,this.y,this.width*.5,0,Math.PI,true);
switch(this.times%2){
case 0:
context.lineTo(this.x-this.width*.5,this.y+this.height*.4);
context.quadraticCurveTo(this.x-this.width*.4,this.y+this.height*.5,this.x-this.width*.2,this.y+this.height*.3);
context.quadraticCurveTo(this.x,this.y+this.height*.5,this.x+this.width*.2,this.y+this.height*.3);
context.quadraticCurveTo(this.x+this.width*.4,this.y+this.height*.5,this.x+this.width*.5,this.y+this.height*.4);
break;
case 1:
context.lineTo(this.x-this.width*.5,this.y+this.height*.3);
context.quadraticCurveTo(this.x-this.width*.25,this.y+this.height*.5,this.x,this.y+this.height*.3);
context.quadraticCurveTo(this.x+this.width*.25,this.y+this.height*.5,this.x+this.width*.5,this.y+this.height*.3);
break;
}
context.fill();
context.closePath();
}
context.fillStyle = '#FFF';
if(isSick){
context.beginPath();
context.arc(this.x-this.width*.15,this.y-this.height*.21,this.width*.08,0,2*Math.PI,false);
context.arc(this.x+this.width*.15,this.y-this.height*.21,this.width*.08,0,2*Math.PI,false);
context.fill();
context.closePath();
}else{
context.beginPath();
context.arc(this.x-this.width*.15,this.y-this.height*.21,this.width*.12,0,2*Math.PI,false);
context.arc(this.x+this.width*.15,this.y-this.height*.21,this.width*.12,0,2*Math.PI,false);
context.fill();
context.closePath();
context.fillStyle = '#000';
context.beginPath();
context.arc(this.x-this.width*(.15-.04*_COS[this.orientation]),this.y-this.height*(.21-.04*_SIN[this.orientation]),this.width*.07,0,2*Math.PI,false);
context.arc(this.x+this.width*(.15+.04*_COS[this.orientation]),this.y-this.height*(.21-.04*_SIN[this.orientation]),this.width*.07,0,2*Math.PI,false);
context.fill();
context.closePath();
}
}
});
}
items = stage.getItemsByType(2);
//主角
player = stage.createItem({
width:30,
height:30,
type:1,
location:map,
coord:{x:13.5,y:23},
orientation:2,
speed:2,
frames:10,
update:function(){
var coord = this.coord;
if(!coord.offset){
if(this.control.orientation!='undefined'){
if(!map.get(coord.x+_COS[this.control.orientation],coord.y+_SIN[this.control.orientation])){
this.orientation = this.control.orientation;
}
}
this.control = {};
var value = map.get(coord.x+_COS[this.orientation],coord.y+_SIN[this.orientation]);
if(value==0){
this.x += this.speed*_COS[this.orientation];
this.y += this.speed*_SIN[this.orientation];
}else if(value<0){
this.x -= map.size*(map.x_length-1)*_COS[this.orientation];
this.y -= map.size*(map.y_length-1)*_SIN[this.orientation];
}
}else{
if(!beans.get(this.coord.x,this.coord.y)){ //吃豆
_SCORE++;
beans.set(this.coord.x,this.coord.y,1);
if(_GOODS[this.coord.x+','+this.coord.y]){ //吃到能量豆
items.forEach(function(item){
if(item.status==1||item.status==3){ //如果NPC為正常狀態(tài),則置為臨時狀態(tài)
item.timeout = 450;
item.status = 3;
}
});
}
}
this.x += this.speed*_COS[this.orientation];
this.y += this.speed*_SIN[this.orientation];
}
},
draw:function(context){
context.fillStyle = '#FFE600';
context.beginPath();
if(stage.status!=3){ //玩家正常狀態(tài)
if(this.times%2){
context.arc(this.x,this.y,this.width/2,(.5*this.orientation+.20)*Math.PI,(.5*this.orientation-.20)*Math.PI,false);
}else{
context.arc(this.x,this.y,this.width/2,(.5*this.orientation+.01)*Math.PI,(.5*this.orientation-.01)*Math.PI,false);
}
}else{ //玩家被吃
if(stage.timeout) {
context.arc(this.x,this.y,this.width/2,(.5*this.orientation+1-.02*stage.timeout)*Math.PI,(.5*this.orientation-1+.02*stage.timeout)*Math.PI,false);
}
}
context.lineTo(this.x,this.y);
context.closePath();
context.fill();
}
});
//事件綁定
stage.bind('keydown',function(e){
switch(e.keyCode){
case 13: //回車
case 32: //空格
this.status = this.status==2?1:2;
break;
case 39: //右
player.control = {orientation:0};
break;
case 40: //下
player.control = {orientation:1};
break;
case 37: //左
player.control = {orientation:2};
break;
case 38: //上
player.control = {orientation:3};
break;
}
});
})();
//結(jié)束畫面
(function(){
var stage = game.createStage();
//游戲結(jié)束
stage.createItem({
x:game.width/2,
y:game.height*.35,
draw:function(context){
context.fillStyle = '#FFF';
context.font = 'bold 48px Helvetica';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('GAME OVER',this.x,this.y);
}
});
//記分
stage.createItem({
x:game.width/2,
y:game.height*.5,
draw:function(context){
context.fillStyle = '#FFF';
context.font = '20px Helvetica';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('FINAL SCORE: '+(_SCORE+50*Math.max(_LIFE-1,0)),this.x,this.y);
}
});
//事件綁定
stage.bind('keydown',function(e){
switch(e.keyCode){
case 13: //回車
case 32: //空格
_SCORE = 0;
_LIFE = 3;
var st = game.setStage(1);
st.reset();
break;
}
});
})();
game.init();
})();游戲截圖


操作說明 ,按鍵盤上下左右鍵移動 ,按鍵盤空格鍵暫停!
到此這篇關(guān)于JS+HTML實現(xiàn)經(jīng)典游戲吃豆人的文章就介紹到這了,更多相關(guān)JS吃豆人游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript 控制 html元素 顯示/隱藏實現(xiàn)代碼
javascript 控制 html元素 顯示/隱藏,下面是一些逐步的實現(xiàn)原理。2009-09-09
Firefox下設(shè)為主頁的JavaScript代碼
在IE下,我們可以輕松使用以下代碼來實現(xiàn)設(shè)置主頁 但在firefox我們需要做的更多2008-09-09
Bootstrap 網(wǎng)格系統(tǒng)布局詳解
在平面設(shè)計中,網(wǎng)格是一種由一系列用于組織內(nèi)容的相交的直線(垂直的、水平的)組成的結(jié)構(gòu)(通常是二維的)。這篇文章主要介紹了Bootstrap 網(wǎng)格系統(tǒng)布局,需要的朋友可以參考下2017-03-03
JS實現(xiàn)統(tǒng)計字符串中字符出現(xiàn)個數(shù)及最大個數(shù)功能示例
這篇文章主要介紹了JS實現(xiàn)統(tǒng)計字符串中字符出現(xiàn)個數(shù)及最大個數(shù)功能,結(jié)合實例形式分析了javascript字符串遍歷、統(tǒng)計相關(guān)操作技巧,需要的朋友可以參考下2018-06-06
JavaScript中Object.prototype.toString方法的原理
這篇文章主要介紹了JavaScript中Object.prototype.toString方法的原理的相關(guān)資料,需要的朋友可以參考下2016-02-02

