diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7396634 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Haole Zheng + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d1cb94 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Pacman 吃豆游戏 + +- 演示分支:gh-pages + +- 代码发布分支:master + +- 项目演示(DEMO)地址:http://passer-by.com/pacman/ + +### 版权 +本游戏由 [passer-by.com](http://passer-by.com/) 制作,请尊重作者,引用请注明来源。 + +功能 + +- [x] 地图绘制 +- [x] 玩家控制 +- [x] NPC根据玩家坐标实时自动寻径 +- [x] 吃豆积分系统 +- [x] 能量豆功能 +- [ ] 特殊物品记分 +- [ ] 多关卡 diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000..96f4db1 Binary files /dev/null and b/favicon.png differ diff --git a/game.js b/game.js new file mode 100644 index 0000000..1704e09 --- /dev/null +++ b/game.js @@ -0,0 +1,418 @@ +'use strict'; +/* +* 小型游戏引擎 +*/ + +// requestAnimationFrame polyfill +if (!Date.now) +Date.now = function() { return new Date().getTime(); }; +(function() { + 'use strict'; + var vendors = ['webkit', 'moz']; + for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) { + var vp = vendors[i]; + window.requestAnimationFrame = window[vp+'RequestAnimationFrame']; + window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame'] || window[vp+'CancelRequestAnimationFrame']); + } + if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy + || !window.requestAnimationFrame || !window.cancelAnimationFrame) { + var lastTime = 0; + window.requestAnimationFrame = function(callback) { + var now = Date.now(); + var nextTime = Math.max(lastTime + 16, now); + return setTimeout(function() { callback(lastTime = nextTime); }, + nextTime - now); + }; + window.cancelAnimationFrame = clearTimeout; + } +}()); + +function Game(id,params){ + var _ = this; + var settings = { + width:960, //画布宽度 + height:640 //画布高度 + }; + var _extend = function(target,settings,params){ + params = params||{}; + for(var i in settings){ + target[i] = params[i]||settings[i]; + } + return target; + }; + _extend(_,settings,params); + var $canvas = document.getElementById(id); + $canvas.width = _.width; + $canvas.height = _.height; + var _context = $canvas.getContext('2d'); //画布上下文环境 + var _stages = []; //布景对象队列 + var _events = {}; //事件集合 + var _index=0, //当前布景索引 + _hander; //帧动画控制 + //活动对象构造 + var Item = function(params){ + this._params = params||{}; + this._id = 0; //标志符 + this._stage = null; //与所属布景绑定 + this._settings = { + x:0, //位置坐标:横坐标 + y:0, //位置坐标:纵坐标 + width:20, //宽 + height:20, //高 + type:0, //对象类型,0表示普通对象(不与地图绑定),1表示玩家控制对象,2表示程序控制对象 + color:'#F00', //标识颜色 + status:1, //对象状态,0表示未激活/结束,1表示正常,2表示暂停,3表示临时,4表示异常 + orientation:0, //当前定位方向,0表示右,1表示下,2表示左,3表示上 + speed:0, //移动速度 + //地图相关 + location:null, //定位地图,Map对象 + coord:null, //如果对象与地图绑定,需设置地图坐标;若不绑定,则设置位置坐标 + path:[], //NPC自动行走的路径 + vector:null, //目标坐标 + //布局相关 + frames:1, //速度等级,内部计算器times多少帧变化一次 + times:0, //刷新画布计数(用于循环动画状态判断) + timeout:0, //倒计时(用于过程动画状态判断) + control:{}, //控制缓存,到达定位点时处理 + update:function(){}, //更新参数信息 + draw:function(){} //绘制 + }; + _extend(this,this._settings,this._params); + }; + Item.prototype.bind = function(eventType,callback){ + if(!_events[eventType]){ + _events[eventType] = {}; + $canvas.addEventListener(eventType,function(e){ + var position = _.getPosition(e); + _stages[_index].items.forEach(function(item){ + if(Math.abs(position.x-item.x) + + + Pac-Man . 吃豆游戏 + + + + +
+ 不支持画布 +

按[空格]暂停或继续

+ +
+ + + +
+ +
+ + diff --git a/index.js b/index.js new file mode 100644 index 0000000..db7326d --- /dev/null +++ b/index.js @@ -0,0 +1,557 @@ +//主程序,业务逻辑 +(function(){ + var _DATA = [ //地图数据 + [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); + } + }); + //版权信息 + 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){ //当没有物品的时候,进入结束画面 + game.nextStage(); + } + }else if(stage.status==3){ //场景临时状态 + 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-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; jthis.coord.x){ + this.orientation = 0; + }else if(this.vector.xthis.coord.y){ + this.orientation = 1; + }else if(this.vector.y80||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为正常状态,则置为临时状态 + 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){ //玩家正常状态 + 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; + } + }); + })(); + //结束画面 + (function(){ + var stage = game.createStage(); + //游戏结束 + 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(); +})();