哈喽,作为一个资深的懒癌患者,我又好久没有写东西了,今天带来一个canvas小游戏。这个需求是要我写一个小游戏,完全模拟 易起玩 的游戏《保卫单身狗》。本来我是想偷懒,直接把他的案例copy过来的,结果发现他们代码压缩后竟然有900多k,感觉提取出来怪麻烦的,就打算自己写了。
提供一个基本案例 点此体验
(由于绑定的是touch事件,如果电脑打开,用chrome打开开发者模式的手机调试测试)
或者扫描下面二维码:
这个demo虽然是整体的模仿但是整体的思路其实来源于之前在慕课网看到的一篇教程 《h5游戏爱心鱼》 讲真,真的给我提供了很多的思路。
下面讲讲代码和踩到的坑,希望能帮到一些人,显示html:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>game test</title> <meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <!-- <meta name="viewport" content="user-scalable=no"/> --> <!-- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0" /> --> <meta name="apple-mobile-web-app-capable" content="yes"/> <meta name="apple-mobile-web-app-status-bar-style" content="black"/> <meta name="description" content=""> <meta name="keywords" content=""> <style> *{margin:0; padding:0;} body,html{ width:100%; height:100%; overflow: hidden; } canvas{ position:relative; } p{ position:absolute; left:10px; top:10px; z-index: 99; font-size:60px; color:red; } .ani-shake{ animation:shake .1s; -webkit-animation:shake .1s; } @keyframes shake{ 0% {left:-4px; top:-4px;} 33% {left:0px; top:0px;} 66% {left:4px; top:4px; } 100% { left:0px; top:0px; } } @-webkit-keyframes shake{ 0% {left:-4px; top:-4px;} 33% {left:0px; top:0px;} 66% {left:4px; top:4px; } 100% { left:0px; top:0px; } } </style> </head> <body> <p>分数:<span id="score">0</span></p> <canvas id="canvas" >纳尼?!!你的浏览器不支持canvas?你该换浏览器啦!</canvas> <img id="bg" src="images/bg.jpg" style="display:none" alt=""> <img id="hit" src="images/hit.png" style="display:none" alt=""> <img id="role" src="images/role.png" style="display:none" alt=""> <img id="role2" src="images/role-b.png" style="display:none" alt=""> <img id="star" src="images/star.png" style="display: none" alt=""> <img id="blood" src="images/blood.png" style="display: none" alt=""> <script src="js/star.js"></script> <script src="js/me.js" ></script> <script src="js/people.js"></script> <script src="js/main.js" ></script> </body> </html>
可以看到 html 里我直接把图片写入了html里,而不是在js里 new Image(); 这是因为在我测试的时候,发现如果是在js里创建的图片,在画布上画出时,在iphone手机下,图片是不会显示的。因为在js里创建图片,需要在图片onload之后才会显示,但是由于画布游戏实际上是类似帧动画。等于是在极短的时间内不停的重绘画布内容,如果把img创建写在js里,iphone手机是不会缓存的,于是就导致了图片一直无法显示,当然 安卓和pc是不会有这个问题的。
从上面的html可以看出 这个游戏主要的js有四个 一个是 main.js 用于创建画布和做动画的循环。 star.js 用于画游戏中间我们要保护的妹纸, me.js 用于画游戏里旋转的主人公,people.js用于画给妹纸献花的路人甲乙丙丁~
main.js
//window.onload = function(){ var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); var win_w = window.innerWidth; var win_h = window.innerHeight; var timmer; var isShaked = false; canvas.width = win_w; canvas.height = win_h; window.requestAnimFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame; var isPlaying = true; var radio = 1; if(typeof(requestAnimationFrame) == 'undefined'){ // 手机不支持requestAnimationFrame时将画布整体大小缩小一半 radio = 0.5; win_w = window.innerWidth*radio; win_h = window.innerHeight*radio; canvas.width = win_w; canvas.height = win_h; } var star = new star(); var Me = new me(); var people = new people(); function reset(){ Me.init(); star.init(); people.init(); document.getElementById('score').innerHTML = '0'; } function loop(){ // loop function ctx.clearRect(0,0,win_w,win_h); draw_bg(); Me.draw(); star.draw(); people.draw(); if(isPlaying && typeof(requestAnimationFrame) == 'function' ){ timmer = requestAnimationFrame(loop); } } if(typeof(requestAnimationFrame) == 'undefined' && isPlaying){ timmer = setInterval(loop,20);// 手机不支持requestAnimationFrame时用setInterval替代此时为了更好的渲染效果将画布整体大小缩小一半(不然卡到死) } function draw_bg(){ ctx.drawImage(document.getElementById('bg'),0,0,win_w,win_h); } function shake(){ if(!isShaked){ isShaked = true; canvas.className = 'ani-shake'; setTimeout(function(){ canvas.className = ''; },120) } isShaked = false; } document.addEventListener('touchstart',function(e){ e.preventDefault(); }) canvas.addEventListener('touchmove',function(e){ var touch = e.changedTouches[0]; Me.toX = touch.clientX; Me.toY = touch.clientY; Me.isMove = true; }); canvas.addEventListener('touchstart',function(e){ var touch = e.changedTouches[0]; Me.toX = touch.clientX; Me.toY = touch.clientY; Me.isMove = true; }); window.onload = function(){ loop(); } //}
main.js 主要规定了几个全局变量和一个reset(重新开始游戏)的函数。主要用于控制整个游戏绘制的循环和鼠标控制人物移动。(其实这个鼠标控制人物移动由于是控制me.js绘制的人物,其实放到me.js里会更贴切)。
me.js
var me = function(){ this.init(); } me.prototype.init = function(){ this.r = 50*radio; this.R = 100*radio; this.width = 250*radio; this.height = 200*radio; this.x = win_w/2; this.y = win_h/2 + this.r + this.height; this.speed = 20; // 规定移动速度 this.toX = 0; // 记录到达下一地点x位置 this.toY = 0; // 记录到达下一地点y位置 this.isMove = false; this.imgIndex = 0; this.pre = 2; // 每隔两帧图片切换一次 //this.draw(); } me.prototype.draw = function(){ if(this.isMove){ this.move(); } ctx.drawImage(document.getElementById('hit'),parseInt(this.imgIndex/this.pre)*this.width,0,this.width,this.height,this.x - this.width/2,this.y - this.height/2,this.width,this.height); this.imgIndex += 1; if(this.imgIndex > 7*this.pre){ this.imgIndex = 0; } } me.prototype.move = function(){ if(this.x < this.toX){ this.x = this.x > this.toX - this.speed ? this.toX : this.x + this.speed; }else{ this.x = this.x < this.toX + this.speed ? this.toX : this.x - this.speed; } if(this.y < this.toY){ this.y = this.y > this.toY - this.speed ? this.toY : this.y + this.speed; }else{ this.y = this.y < this.toY + this.speed ? this.toY : this.y - this.speed; } // 边界判断 if(this.x < this.width/2){ this.x = this.width/2; } if(this.x > win_w - this.width/2){ this.x = win_w - this.width/2; } if(this.y < this.height/2){ this.y = this.height/2; } if(this.y > win_h - this.height/2){ this.y = win_h - this.height/2; } // 中心区域判断 if(this.x > win_w/2 - this.R && this.x < win_w/2 + this.R ){ var r = Math.sqrt(Math.pow(win_w/2 - this.x,2) + Math.pow(win_h/2 - this.y,2) ); if(r < this.r + this.R){ var y = Math.sqrt(Math.pow(this.r+this.R,2) - Math.pow(win_w/2 - this.x,2) ); this.y = this.y < win_h/2 ? win_h/2 - y : win_h/2 + y; } } if(this.y > win_h/2 - this.R && this.y < win_h/2 + this.R ){ var r = Math.sqrt(Math.pow(win_w/2 - this.x,2) + Math.pow(win_h/2 - this.y,2) ); if(r < this.r + this.R){ var x = Math.sqrt(Math.pow(this.r+this.R,2) - Math.pow(win_h/2 - this.y,2) ); this.x = this.x < win_w/2 ? win_w/2 - x : win_w/2 + x; } } }
me.js 主要用于画游戏中移动的主人公。此处需要注意的就是不能超出边界的检测和中心区域的碰撞检测,碰撞检测我全部使用的是高中学的《勾股定理》。没办法,我也不会其他高深算法~哈哈。
star.js
var star = function(){ this.init(); } star.prototype.init = function(){ this.r = 200*radio; this.width = 130*radio; this.height = 200*radio; this.x = win_w/2; this.y = win_h/2; this.life = 150*radio; this.nowLife = 150*radio; } star.prototype.draw = function(){ ctx.save(); ctx.beginPath(); ctx.strokeStyle = "blue"; ctx.arc(this.x,this.y,this.r, 0, Math.PI*2, false); ctx.stroke(); ctx.closePath(); ctx.restore(); ctx.drawImage(document.getElementById('star'),this.x - this.width/2,this.y - this.height/2,this.width,this.height); this.drawLine(this.life,'#666'); this.drawLine(this.nowLife,'rgba(255,255,255,1)'); this.lose(); } star.prototype.drawLine = function(width,color){ // 绘制血条 ctx.save() ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 20 * radio; if(width != 0){ ctx.lineCap="round"; } ctx.moveTo(win_w/2 - this.life/2,win_h/2 - this.height/2 - 20); ctx.lineTo(win_w/2 - this.life/2 + width,win_h/2 - this.height/2 - 20); ctx.fill(); ctx.stroke(); ctx.closePath(); ctx.restore(); } star.prototype.lose = function(){ if(this.nowLife == 0){ alert('YOU LOSE !'); reset(); // 重新开始 } }
star.js 用于画中间需要守护的妹纸,关键操作就是对血量的判断,如果血量为0则游戏结束,重新开始游戏。
people.js
var people = function(){ this.init(); } people.prototype.init = function(){ this.width = 85*radio; this.height = 65*radio; this.R = 200*radio; this.r = 50*radio; this.num = 3; this.all = 30; this.x = []; this.y = []; this.speed = []; this.baseSpeed = 400; // 基础速度,值越小,速度越快 //this.x = []; // 记录鼠标位置x //this.y = []; // 记录鼠标位置y this.isTouch = []; // 判断是否和人物碰撞 this.success = []; // 判断是否到达终点 this.count = []; // 到达终点后计数 this.blood = []; // 击中后的血迹 this.showBlood = []; this.attack = 10; // 攻击力 this.interval = 50; // 设置到达终点切未被打飞的小人每n帧攻击一次 this.score = 0; this.getScort = 10; this.pre = 15; this.imgIndex = []; this.img = []; for(var i=0;i<this.all; i++){ this.born(i); } //this.draw(); } people.prototype.level = function(score){ // 难度设置 // if(this.score > 100 && this.sc ){ // 难度提升 // this.num = 4; // this.pre = 200; // } if(score > 150){ this.num = parseInt(score/150) + 3; this.baseSpeed = 400 - parseInt(score/200)*20; } } people.prototype.draw = function(){ this.level(this.score); for(var i=0;i<this.num;i++){ if(this.showBlood[i]){ // 先画血迹,避免血迹挡住人物 this.drowBlood(i,this.blood[i].x,this.blood[i].y); } ctx.drawImage(this.img[i],parseInt(this.imgIndex[i]/this.pre)*this.width,0,this.width,this.height,this.x[i]-this.width/2,this.y[i]-this.height/2,this.width,this.height); //ctx.fillRect(this.x[i]-this.width/2,this.y[i]-this.height/2,this.width,this.height); this.boom(i); this.add(i); this.imgIndex[i] += 1; if(this.imgIndex[i] > 2*this.pre){ this.imgIndex[i] = 0; } //console.log(this.x[i],) } } people.prototype.born = function(i){ var x = Math.random() - 0.5; if(x > 0){ this.x[i] = x * 1500 + win_w ; }else{ this.x[i] = x * 1500 } var y = Math.random() - 0.5; if(y>0){ this.y[i] = y*1500+win_h; this.img[i] = document.getElementById('role2'); }else{ this.y[i] = y*1500; this.img[i] = document.getElementById('role'); } this.speed[i] = {} // 每个人物速度不同 var interval = Math.random()*200 + this.baseSpeed; this.speed[i].x = (win_w/2 - this.x[i])/interval; this.speed[i].y = (win_h/2 - this.y[i])/interval; //this.x[i] = (Math.random() - 0.5) * 2000 + win_w/2; //this.y[i] = (Math.random() - 0.5) * 2000 + win_h/2; //this.x[i] = this.x[i]; //this.y[i] = this.y[i]; this.isTouch[i] = false; this.success[i] = false; this.count[i] = 0; this.showBlood[i] = false; this.blood[i] = {}; this.imgIndex[i] = 0; } people.prototype.boom = function(i){ var x = (win_w/2 - this.x[i]); var y = (win_h/2 - this.y[i]); var x1 = (Me.x - this.x[i]); var y1 = (Me.y - this.y[i]); if(Math.sqrt(x1*x1 + y1*y1) < this.r + Me.r){ // 碰撞检测人物之间 if(!this.isTouch[i]){ // 获取得分 this.score += this.getScort; document.getElementById('score').innerHTML = this.score; shake(); // 震动 this.blood[i] = { // 记录血迹位置 x: this.x[i], y: this.y[i], count: 0 } this.showBlood[i] = true; } this.isTouch[i] = true; } if(!this.isTouch[i]){ if(Math.sqrt(x*x + y*y) > this.R + this.r ){ this.x[i] += this.speed[i].x; this.y[i] += this.speed[i].y; }else{ if(!this.success[i]){ star.nowLife = star.nowLife > this.attack ? star.nowLife - this.attack : 0; this.success[i] = true; }else{ this.count[i] += 1; if(this.count[i] == this.interval){ this.count[i] = 0; star.nowLife = star.nowLife > this.attack ? star.nowLife - this.attack : 0; } } } }else{ this.x[i] -= this.speed[i].x*5; this.y[i] -= this.speed[i].y*5; } } people.prototype.add = function(i){ if(this.isTouch[i]){ if(this.x[i] < -this.width - 100 || this.x[i] > win_w+100 || this.y[i] < -this.height-100 || this.y[i] > win_h+100 ){ this.born(i); } } } people.prototype.drowBlood = function(i,x,y){ // 绘制血迹 var r = Math.sqrt(Math.pow(win_w/2-x,2) + Math.pow(win_h/2-y,2)); var deg = Math.acos( (x - win_w/2)/r); deg = y < win_h/2 ? deg : -deg; // 判断血液溅射方向 if( (y > win_h/2 && x < win_w/2) || (y < win_h/2 && x > win_w/2) ){ x = x < win_w/2 ? x+this.width/2 : x-this.width/2; y = y < win_h/2 ? y-this.height/2 : y+this.height/2; }else{ x = x < win_w/2 ? x-this.width/2 : x+this.width/2; y = y < win_h/2 ? y+this.height/2 : y-this.height/2; } ctx.save(); ctx.translate(x,y); ctx.rotate(-deg); ctx.drawImage(document.getElementById('blood'),0,0,330,81); ctx.translate(0,0); ctx.restore(); }
people.js 是游戏最复杂的部分,也是最核心的部分,这个js用于画出向妹纸求爱的小人,关键的碰撞检测比如打飞小人和对妹纸攻击减少血量,攻击后打飞,以及绘制血迹都在这个js里完成。因为小人的数量很多,所以实际上是维护了好几个数组。这里具体不好解释,大家可以去慕课看看之前我推荐的那篇《爱心鱼》的教程,当然我不是打广告的。。。只是觉得她课讲得挺清楚,对这些有帮助。
总之不要把游戏想的太难,这里用到数学的内容还是高中知识 勾股定理 和 三角函数 –当然也许是有更好的算法我不会,所以用不到。
然后。请不要吐槽游戏的ui除了背景和中间的妹纸是百度的,其他任务素材我是直接从 易起玩 拔过来的,想想只是做个demo不涉及到商业用途,应该不算侵权吧~
游戏github地址 https://github.com/mikoshu/games
这里再加一个小提示,我测试过,如果路人的角色一次性绘制超过30个,浏览器就会奔溃。。。所以尽量控制游戏难度的时候,不要让人物太多,可以通过提升人物移动速度来提升难度。
嘿嘿嘿,自从多说和网易云跟帖相继停止服务,我只能指望wordpress自带的评论系统了,也是心累!!!