使用JavaScript实现网页版Pongo设计思路及源代码分享
作者:ChenReason
1.游戏背景介绍(写在前面的废话):
五月初的某天,看到某网推荐了这款游戏,Pongo,看着还不错的样子就用ipad下下来试玩了下,玩了两局感觉还错挺过瘾的,因为是手欠类游戏嘛大家懂的。
但是没一会发现游戏在ipad似乎有些bug,玩一会就会卡住然后只能强退了,真是揪心,记录还等着破呢。
怎么办?玩游戏不如玩自己的游戏的念头又邪恶的出现了,然后就把pad丢给了朋友虐心去,我默默回到电脑前开始动手自己写个不会卡的。
大概两小时吧,写出了基本框架,然后扔sinaapp里试了下效果基本能玩就洗洗睡了。
第二天醒来因为周末没事就花了些时间设计了下界面,同时不幸自己测出了一些比较严重的bug,最后又花了些时间给改掉了。
最后游戏取名”Pongo+“(手机党点我即玩),电脑端暂时不支持,并顺便在Github上上传了源码并去掉了提交成绩模块。
2.游戏试玩网址:
Pongo+(仅限移动端):http://mypongo.sinaapp.com/
github开源(欢迎fork让游戏更好):https://github.com/ChenReason/pongo/blob/gh-pages/index.html
3.游戏规则玩法:
点击屏幕会改变挡板的运动方向,点击一次挡板方向相应改变一次,目的是为了能刚好挡住四处滚动的小球不让其跑出大圆外。时间越长越好!最后可提交自己的成绩进行排名!
4.游戏所用技术:
HTML、CSS、JavaScript、Canvas、PHP
5.游戏设计思路:
a)运用Canvas将游戏的主界面画出,底部为一单色长方形,上面覆盖一个大圆,大圆上再绘制小圆及挡板,挡板中部还有一个大小为1px的超级小圆(作实现碰撞检测)。
b)小圆运动方向一共有8个分别为上、下、左、右、左上、左下、右上和右下。
c)挡板的运动方向只有两个,顺时针和逆时针。
d)碰撞检测未涉及到引擎的使用,而是根据小圆与挡板中部的超级小圆进行距离判断,从而实现简陋的碰撞检测。
e)小球碰撞后反弹的方向确定,利用常识列举,共8种情况。
6.游戏实现难点:
a)碰撞检测。
b)定时器setInterval的清除时机以及是否清楚彻底。
c)定时器周期长短与游戏体验的关系。
d)Android与IOS设备性能不同导致的游戏流畅度问题。
7.游戏现有问题:
a)由于碰撞检测是比较两圆的圆心距,且涉及到定时器的使用,因此由于定时器间隔极短导致在肉眼所见的一次碰撞背后其实已经发生了数十次碰撞,由此会导致小球最后实际的反弹方向与现实的物理定理有所不同,经过优化,出现的概率已经较低,但仍未能避免,因此有些玩家会发现小圆若没有很准地撞在挡板正中央则可能导致游戏失败。
b)由于函数过于冗长,运行效率较低,再加上使用定时器,因此在Andorid与iOS或其他移动端上的游戏体验不尽相同(整体来说iOS由于Android)。
c)排行榜并未实现自动实时更新。(数据库还不会用)
8.游戏界面预览:
(图1为初版,图2去掉了按钮,图3为最终版,图4为排行榜)
图1
图2
图3
9.游戏JavaScript部分源代码:
var ifingame=0;
var maxgrade=0,grade=0;
var grade1,grade2;
var nickname;
var gamespeed=1.4;//小球速度
var linespeed=Math.PI/95; //跟踪线速度
var crashdistancefaild=-7;//碰撞检测参数
var crashdistancesucc=15
var fantanjuli=7;
var themaxgradeline=12.1;
function getCookie1(nickname)
{
if (document.cookie.length>0)
{
c_start=document.cookie.indexOf(nickname + "=")
if (c_start!=-1)
{
c_start=c_start + nickname.length+1;
c_end=document.cookie.indexOf(",",c_start);
if (c_end==-1)
c_end=document.cookie.length;
return unescape(document.cookie.substring(c_start,c_end));
}
}
return ""
}
function getCookie2(mymaxgrade)
{
if (document.cookie.length>0)
{
c_start=document.cookie.indexOf(mymaxgrade + "=")
if (c_start!=-1)
{
c_start=c_start + mymaxgrade.length+1;
c_end=document.cookie.indexOf(";",c_start);
if (c_end==-1)
c_end=document.cookie.length;
return unescape(document.cookie.substring(c_start,c_end));
}
}
return ""
}
function setCookie(nickname,value,mymaxgrade,maxgrade,expiredays)
{
var exdate=new Date()
exdate.setDate(exdate.getDate()+expiredays)
document.cookie=nickname+ "=" +escape(value)+"," + mymaxgrade + "=" + escape(maxgrade) + ((expiredays==null) ? "" : "; expires="+exdate.toGMTString());
}
function checkCookie()
{
nickname=getCookie1('nickname');
maxgrade=parseInt(getCookie2('mymaxgrade'));
if(isNaN(maxgrade)==true)
{
maxgrade=0;
}
if (nickname!=null && nickname!="")
{
alert('欢迎'+nickname+'回来!'+'\n'+"如果喜欢请分享一下哈~");
}
else
{
nickname=prompt('请输入你的昵称:(名字太长上榜可是会显示不完整的哦)',"")
if (nickname!=null && nickname!="")
{
var maxgradestring=maxgrade.toString();
setCookie('nickname',nickname,'mymaxgrade',maxgradestring,365);
}
}
}
var objpane=document.getElementById("pane");
var ctxpane=objpane.getContext("2d");
ctxpane.translate(150,150);//必备 画布中心点平移
function sendmail()
{
if(grade2>themaxgradeline)
var max_grade=grade2;
window.location.href='index.php?max_grade='+max_grade+'&nick_name='+nickname;
/* {
<?php
$grade=$_GET['max_grade'];
$nickname=$_GET['nick_name'];
$mail = new SaeMail();
$ret = $mail->quickSend( 'reasonpongo@163.com' , $grade , $nickname ,'reasonpongo@163.com' , 'mypongo' );
$mail->clean();
?>
}*/
alert(nickname+"你的成绩为:"+grade2+"提交成功~");
}
var gamedirection={
shang : 1,
xia : 5,
zuo : 7,
you : 3,
zuoshang: 8,
zuoxia : 6,
youshang: 2,
youxia : 4,
clock : 0,
anticlock: 9,
};//方向
var canvas={
width : 300,
height: 300,
};//画布
var bigcircle = {//大圆参数
x : 0, //圆心的x轴坐标值
y : 0, //圆心的y轴坐标值
r : 150, //圆的半径
c : 'rgb(255,255,255)',
};//大圆
var smallcircle = {//小圆参数
x : 0, //圆心的x轴坐标值
y : 0, //圆心的y轴坐标值
r : 12, //圆的半径
c : 'rgb(204,105,106)',
direction : gamedirection.xia,
};//小圆
var line = {//挡板线的参数
x : 0, //圆心的x轴坐标值
y : 0, //圆心的y轴坐标值
r : 150 , //圆弧的半径
start:(Math.PI/2-Math.PI/16),
end : (Math.PI/2+Math.PI/16),
c : 'rgb(55,55,55)',
direction: gamedirection.anticlock,
};//跟踪线
var dot = {//跟踪点参数
x : (bigcircle.r*Math.cos(line.start+Math.PI/16)),//以大圆为原点
y : (bigcircle.r*Math.sin(line.start+Math.PI/16)),
r : 1,
}//跟踪点
function changelinedirection()
{
if(line.direction==gamedirection.clock)
{
line.direction=gamedirection.anticlock;
}
else
{
line.direction=gamedirection.clock;
}
}
function getdistance(){
var distance=Math.sqrt((smallcircle.x)*(smallcircle.x )+(smallcircle.y )*(smallcircle.y ));
return distance;
}//返回小球与大圆中心距离平方 getdistance()
function ifgameover(){//判断是否出界
if((getdistance() - bigcircle.r)>5)
return true;
else
return false;
} //判断游戏是否结束 ifgameover()
function ifcrash(){ //碰撞检测
var dx = dot.x-smallcircle.x;
var dy = dot.y-smallcircle.y;
var dd=Math.sqrt(dx*dx+dy*dy);
if(dd< crashdistancesucc)
return true;
else
return false;
}//碰撞检测 ifcrash()
function randomback()
{
var x=Math.floor(Math.random()*3);
switch (smallcircle.direction){
case gamedirection.shang:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.xia;
smallcircle.y=smallcircle.y+fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.zuoxia;
smallcircle.x=smallcircle.x-fantanjuli;
smallcircle.y=smallcircle.y+fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.youxia;
smallcircle.x=smallcircle.x+fantanjuli;
smallcircle.y=smallcircle.y+fantanjuli;
break;
default:
break;
} break;
}
case gamedirection.xia:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.shang;
smallcircle.y=smallcircle.y-fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.zuoshang;
smallcircle.x=smallcircle.x-fantanjuli;
smallcircle.y=smallcircle.y-fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.youshang;
smallcircle.x=smallcircle.x+fantanjuli;
smallcircle.y=smallcircle.y-fantanjuli;
break;
default:
break;
} break;
}
case gamedirection.zuo:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.you;
smallcircle.x=smallcircle.x+fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.youshang;
smallcircle.x=smallcircle.x+fantanjuli;
smallcircle.y=smallcircle.y-fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.youxia;
smallcircle.x=smallcircle.x+fantanjuli;
smallcircle.y=smallcircle.y+fantanjuli;
break;
default:
break;
} break;
}
case gamedirection.you:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.zuo;
smallcircle.x=smallcircle.x-fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.zuoxia;
smallcircle.x=smallcircle.x-fantanjuli;
smallcircle.y=smallcircle.y+fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.zuoshang;
smallcircle.x=smallcircle.x-fantanjuli;
smallcircle.y=smallcircle.y-fantanjuli;
break;
default:
break;
} break;
}
case gamedirection.zuoshang:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.youxia;
smallcircle.x=smallcircle.x+fantanjuli;
smallcircle.y=smallcircle.y+fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.xia;
smallcircle.y=smallcircle.y+fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.you;
smallcircle.x=smallcircle.x+fantanjuli;
break;
default:
break;
} break;
}
case gamedirection.zuoxia:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.youshang;
smallcircle.x=smallcircle.x+fantanjuli;
smallcircle.y=smallcircle.y-fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.shang;
smallcircle.y=smallcircle.y-fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.you;
smallcircle.x=smallcircle.x+fantanjuli;
break;
default:
break;
} break;
}
case gamedirection.youshang:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.zuoxia;
smallcircle.x=smallcircle.x-fantanjuli;
smallcircle.y=smallcircle.y+fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.zuo;
smallcircle.x=smallcircle.x-fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.xia;
smallcircle.y=smallcircle.y+fantanjuli;
break;
default:
break;
} break;
}
case gamedirection.youxia:
{
switch (x)
{
case 0:
smallcircle.direction=gamedirection.zuoshang;
smallcircle.x=smallcircle.x-fantanjuli;
smallcircle.y=smallcircle.y-fantanjuli;
break;
case 1:
smallcircle.direction=gamedirection.zuo;
smallcircle.x=smallcircle.x-fantanjuli;
break;
case 2:
smallcircle.direction=gamedirection.shang;
smallcircle.y=smallcircle.y-fantanjuli;
break;
default:
break;
} break;
}
default:
{
break;
}
}
}//小球随机反向 randomback()
function smallcircledirection()
{
switch (smallcircle.direction){ //根据小球方向做移动
case gamedirection.shang:
{
smallcircle.y=smallcircle.y-gamespeed;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
case gamedirection.xia:
{
smallcircle.y=smallcircle.y+gamespeed;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
case gamedirection.zuo:
{
smallcircle.x=smallcircle.x-gamespeed;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
case gamedirection.you:
{
smallcircle.x=smallcircle.x+gamespeed;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
case gamedirection.zuoshang:
{
smallcircle.x=smallcircle.x-gamespeed*0.8;
smallcircle.y=smallcircle.y-gamespeed*0.8;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
case gamedirection.zuoxia:
{
smallcircle.x=smallcircle.x-gamespeed*0.8;
smallcircle.y=smallcircle.y+gamespeed*0.8;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
case gamedirection.youshang:
{
smallcircle.x=smallcircle.x+gamespeed*0.8;
smallcircle.y=smallcircle.y-gamespeed*0.8;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
case gamedirection.youxia:
{
smallcircle.x=smallcircle.x+gamespeed*0.8;
smallcircle.y=smallcircle.y+gamespeed*0.8;
grade++;
if(grade>maxgrade)
{
maxgrade=grade;
newrecoder();
}
addone();
break;
}
default:
{
break;
}
}
}//小球移动 smallcircledirection()
/*画出底部圆*/
ctxpane.beginPath(); //大圆
ctxpane.arc(bigcircle.x,bigcircle.y,bigcircle.r,0,Math.PI*2,true);
ctxpane.fillStyle = bigcircle.c;
ctxpane.fill();
ctxpane.closePath();
/*画出底部追踪线条*/
ctxpane.beginPath();
ctxpane.lineWidth=6;
ctxpane.strokeStyle = line.c;
ctxpane.arc(line.x, line.y, line.r, line.start, line.end,false);
ctxpane.stroke();
ctxpane.closePath();
function tapme()//tapme
{
ctxpane.beginPath();
ctxpane.strokeStyle="rgb(255,222,195)";
ctxpane.font = "80px Papyrus";
ctxpane.strokeText('TAP',-95,30);
ctxpane.fillStyle="rgb(255,205,105)";
ctxpane.font = "35px Papyrus";
ctxpane.fillText('me',70,30);
ctxpane.closePath();
}
function newrecoder()
{
ctxpane.beginPath();
ctxpane.fillStyle="rgb(255,0,0)";
ctxpane.font = "18px Papyrus";
ctxpane.fillText("New!",58,80);
ctxpane.closePath();
}
function addone()
{
grade1=(grade/150).toFixed(1);
grade2=(maxgrade/150).toFixed(1);
var say1="now";
var say2="best"
ctxpane.beginPath();
ctxpane.strokeStyle="rgb(250,222,185)";
ctxpane.font = "60px Papyrus";
ctxpane.strokeText(grade1,-45,-60);
ctxpane.strokeText(grade2,-45,100);
ctxpane.fillStyle="rgb(255,0,100)";
ctxpane.font = "15px Papyrus";
ctxpane.fillText(say1,58,-60);
ctxpane.fillStyle="rgb(255,0,100)";
ctxpane.font = "15px Papyrus";
ctxpane.fillText(say2,58,100);
ctxpane.closePath();
}
function movetest(){
if(ifgameover())
{
ifingame=0;
if(maxgrade>parseInt(getCookie2('mymaxgrade')))
{
setCookie('nickname',nickname,'mymaxgrade',maxgrade.toString(),365);
}
clearInterval(timer);
tapme();
}
else
{
if(ifcrash())
{
randomback();
}
ctxpane.clearRect(-150,-150,300,300); //清屏
ctxpane.beginPath(); //大圆
ctxpane.arc(bigcircle.x,bigcircle.y,bigcircle.r,0,Math.PI*2,true);
ctxpane.fillStyle = bigcircle.c;
ctxpane.fill();
ctxpane.closePath();
if(line.direction==gamedirection.clock) //跟踪线顺时针
{
line.start=line.start + linespeed;
line.end=line.end +linespeed;
ctxpane.beginPath();
ctxpane.lineWidth=4;
ctxpane.strokeStyle = line.c;
ctxpane.arc(line.x, line.y, line.r, line.start, line.end,false);
ctxpane.stroke();
ctxpane.closePath();
}
if(line.direction==gamedirection.anticlock) //跟踪逆顺时针
{
line.start=line.start - linespeed;
line.end=line.end -linespeed;
ctxpane.beginPath();
ctxpane.lineWidth=4;
ctxpane.strokeStyle = line.c;
ctxpane.arc(line.x, line.y, line.r, line.start, line.end,false);
ctxpane.stroke();
ctxpane.closePath();
}
dot.x=bigcircle.r*Math.cos(line.start+Math.PI/32) //跟踪点
dot.y=bigcircle.r*Math.sin(line.start+Math.PI/32)
ctxpane.beginPath();//线上跟踪点
ctxpane.arc(dot.x,dot.y,dot.r,0,Math.PI*2,true);
ctxpane.fillStyle = smallcircle.c;
ctxpane.fill();
ctxpane.closePath();
smallcircledirection();//小圆
ctxpane.save();
ctxpane.beginPath();
ctxpane.arc(smallcircle.x,smallcircle.y,smallcircle.r,0,Math.PI*2,true);
ctxpane.fillStyle = smallcircle.c;
ctxpane.fill();
ctxpane.closePath();
ctxpane.restore();
}
}//主函数
///////////////////////////////////////////
tapme();
var timer;
function startgame(){//开始游戏
if(ifingame==0)
{
ifingame=1;
grade=0;
var xx=Math.floor(Math.random()*8);
/* switch(xx)
{
case 0:
smallcircle.direction=gamedirection.shang;
break;
case 1:
smallcircle.direction=gamedirection.xia;
break;
case 2:
smallcircle.direction=gamedirection.zuo;
break;
case 3:
smallcircle.direction=gamedirection.you;
break;
case 4:
smallcircle.direction=gamedirection.zuoshang;
break;
case 5:
smallcircle.direction=gamedirection.zuoxia;
break;
case 6:
smallcircle.direction=gamedirection.youshang;
break;
case 7:
smallcircle.direction=gamedirection.youxia;
break;
default:
break;
}*/
smallcircle.direction=gamedirection.xia;
smallcircle.x=smallcircle.y=0;
line.start=Math.PI/2-Math.PI/26;
line.end=Math.PI/2+Math.PI/26;
line.direction=gamedirection.anticlock;
clearInterval(timer);
timer=setInterval(movetest,10);
}
}//开始游戏 startgame()
function opentop()
{
window.location="http://pongotop.sinaapp.com";
}
10.写在最后
这纯属又是一个自娱自乐,写完后的第三天因为开始忙着投简历找实习就没空再管,扔到朋友圈让朋友玩去了。这一个月过去了再重新看这游戏,感觉它不该就这样死掉,本人没什么技术,做得很拙略,因此发出这篇文字希望能帮到一些对pongo感兴趣的朋友,再者就是希望如果有这方面的高手看到了能够给予赐教,一切疑惑和赐教都欢迎给我留言,谢谢!