DooMissO是本人使用JS、jQuery开发的一款弹幕射击游戏,现已推出1.8版
HTML DOM操作和jQuery代码混搭得比较厉害,后续几个版本会陆续修正统一
github仓库地址:https://github.com/Kagashino/doomisso-demo
试玩地址:https://kagashino.github.io/doomisso-demo/
这个不是CANVAS开发的游戏,所以游戏画布是一个600*600的DIV,包括各种按钮和界面,画布右侧显示各种游戏即时数据。附上HTML代码:
<div class="layout layout2">
<center>
<h2>游戏设置h2>
<h3>点击下方选项设置按键h3><br>
<h3 class="keySet"> 向上 按键:<span id="UPKeySet">span>h3>
<h3 class="keySet"> 向下 按键:<span id="DNKeySet">span>h3>
<h3 class="keySet"> 向左 按键:<span id="LTKeySet">span>h3>
<h3 class="keySet"> 向右 按键:<span id="RTKeySet">span>h3>
<h3 class="keySet"> 低速 按键:<span id="LSKeySet">span>h3>
<h3 class="keySet"> 还原默认按键h3>
<h3>难易度调节h3>
<span class="selectDiff" title="浅尝辄止的难度">简单span><span class="selectDiff" title="代表大多数玩家的水平">正常span><span class="selectDiff" title="难度越大,回报越高">疯狂span>
<input type="hidden" id="difficult" value="正常">
center>
<h1 class="returnMain btn-style">返回h1>
div>
<div class="layout layout3">
<center><h2>游戏说明h2>center>
<p class="howToPlay">
1、默认<strong>Enterstrong>键开始,<strong>上下左右strong>移动,<strong>shiftstrong>低速移动,同时显示擦弹范围和判定点(见3、),您在下一个版本可以在“游戏设置”中修改游戏操作按键;<br />
2、为了将来该游戏系统的布局计划,现在已开启<strong>自动开火strong>模式;<br />
3、擦弹范围是一个围绕着机体的<span class="gray">灰色的圈span>,当圈内有子弹时分数会加得很快,判定点只有机体中间一个小<span class="wdot">白点span>的范围,击中一次<span class="orange">生命值-10span>(注意:碰到飞行物不会触发碰撞);<br />
4、今后还会推出更丰富的游戏系统,敬请期待;<br />
5、If it ran into any bug, REFRESH can solve 99% problems.(有任何问题,请使用刷新大法);<br>
6、good luck, have fun!
p>
<img id="control" src="img/operate.png" />
<h1 class="returnMain btn-style">返回h1>
div>
<section id="gameScreen">
<div id="p_ship" class="player1">
<div id="laser">div>
<div id="p_spot">div>
div>
section>
<aside>
<button id="startBtn" >开始游戏button><br>
<button id="endTest" onclick="endGame()">结束游戏button><br><br>
<p>生命值:<span id="hitCount">50span>p><br>
<p>擦弹:<span id="zijiPos">0span>p><br>
<p>分数:<span id="score">0span>p><br><br>
<p>屏幕弹幕数 <span id="dmkcount">0span>p>
<h4>HTML5弹幕射击游戏 ver1.8 作者:Kagashinoh4>
<p>图片素材均来自恶魔之星p><br>
<p>历史版本 <br>
ver1.6.12.28 <br>
ver1.7.1.18 <br>
ver1.7.2.28 <br>
p><br><br>
aside>
CSS设置
body{
margin: 0;
padding: 0;
z-index: -1000;
}
section,p,aside{
margin: 0;padding: 0;
display: inline-block;
vertical-align: top;
}
aside{
width: 30%;
}
.gray{
border: 2px dashed #666;
color:#666;
}
.orange{
color: #f51;
}
.wdot{
border: dashed 1px gray;
color: #eee;
}
#gameScreen{
width: 600px;
height: 600px;
background: #336 url(../img/bg.png);
position: relative;
/*overflow: hidden;*/
z-index: 1;
}
#laser{
position: absolute;
width: 15px;
height: 600px;
background: transparent;
border: 2px #fff solid;
border-bottom: none;
left: 35px;
top: -560px;
display: none;
}
#hitCount{
}
.btn-style{
padding: 5px;
position: absolute;
font-weight: normal;
border:3px solid white;
color:white;
z-index: 1000;
}
.btn-style:hover{
cursor: pointer;
font-weight: bold;
box-shadow: 0 0 15px 5px white;
}
.btn-style::selection {
background: none;
}
.btn-style::-moz-selection {
background: none;
}
.keySet{
text-align:left;
width: 70%;
border:2px solid transparent;
}
.keySet:hover{
cursor: default;
border-color: #fff;
}
.keySet::selection {
background: none;
}
.keySet::-moz-selection {
background: none;
}
.layout{
width: 600px;
height: 600px;
color: #ffffff;
position: absolute;
z-index: 100;
}
.layout1{
display: block;
}
.layout2{
display: none;
}
.selectDiff{
padding: 5px;
border:solid 2px transparent;
}
.selectDiff:hover{
cursor: pointer;
text-shadow: 0px 0px 3px #fff;
}
.layout3{
display: none;
width: 550px;
top: 10px;
left: 30px;
}
.howToPlay{
margin-top: 20px;
}
.currDifficult {
position: absolute;
top: 150px;
left: 220px;
}
#startBtn2{
top: 180px;
left: 220px;
}
#gameConfig{
top: 250px;
left: 220px;
}
#readme{top: 320px;
left:220px;
}
.returnMain {
top: 500px;
left: 450px;
}
#control{
position: absolute;
left: 50px;
top: 300px;
z-index: 1000;
}
/***************游戏物体样式******************/
#p_ship{
width:90px;
height: 90px;
background: url(../img/fighterAll.png) 0px 0px no-repeat;
position: absolute;
top: 300px;
left: 250px;
z-index: 10;
}
#p_spot{
margin: 37px 37px;
width: 10px;
height: 10px;
background: #fff;
border: dotted 1px gray;
border-radius: 50%;
z-index: 100;
display:block;
}
#p_spot:after{
content: " ";
display: block;
width: 60px;
height: 60px;
border: dashed gray;border-radius: 50%;
position: relative;
top: -30px;
left: -28px;
}
.enemy1{
width: 60px;
height: 60px;
background: pink;
position: absolute;
}
.left{
background: url(../img/enemy1.png) no-repeat;
}
.right{
background: url(../img/enemy1.png) -60px 0px no-repeat;
}
.enemy2{
width: 60px;
height: 60px;
background: url(../img/enemy2.png) no-repeat;
position: absolute;
}
#theBoss{
width: 185px;
height: 208px;
background: url(../img/boss1.png) no-repeat;
position: absolute;
top: 85px;
z-index:5;
}
.damagedBoss{
background: url(../img/boss1dmg.png) no-repeat !important;
}
#bossHealthBar{
width: 200px;
height: 10px;
position: absolute;
top: 0;
left: -10px;
background: rgb(0,255,0);
display: block;
}
.bullet,.poolBlt{
width: 10px;
height: 10px;
background:red;
border-radius: 50%;
position: absolute;
z-index: 100;
}
脚本引用:
<script src="js/jquery.min.js">script>
<script src="js/keymap.js">script>
<script src="js/animateEasing.js">script>
<script src="js/bulletClass.js">script>
<script src="js/newscript.js">script>
<script src="js/objAction.js">script>
<script src="js/eventListener.js">script>
初始化需要用到的变量:
//键盘编码和对应条件判断变量
var keySt = {
UP: 38,
DN: 40,
LT: 37,
RT: 39,
LS: 16,
toUp: false,
toDown: false,
toLeft: false,
toRight: false,
toLSpeed: false,
toShow: false,
toPause: false,
}
//状态
var gameSt = {
playing: false, //游戏是否进行中
boss: false,//是否在打BOSS
graze : 0,//擦弹
hp : 50,//生命值
score : 0,//本轮得分
highScore: 0,//最高分,目前尚未用到
diff : 1 //难易度 0简单 1正常 2疯狂
}
/*定时器对象*/
var timer = {
general: null,//主定时器
enemyCreator: null,//敌机1生成器
enemy2Creator: null,//敌机2生成器
overflowOperator: null,//子弹溢出处理器
releaseBoss:null,
}
var boss = {
health:300,
position:[],
state:1
}
/*检查是否与服务器连接,如果是,则开启分数记录功能*/
var ajax = false;
//自机
var ziji = document.getElementById('p_ship');
var spot = document.getElementById('p_spot');//判定点
//画布
var $gameScreen = $('#gameScreen');
var $startBtn = $('#startBtn,#startBtn2');//两处可以控制开始的按钮
点击开始按钮执行初始化游戏
//初始化
function gameInit(){
gameSt.stop = keySt.toShow = false;
initDanmaku();
gameContinue();
}
开始游戏后关闭判定点显示,隐藏所有选单,gameProcess为生成敌人、boss等相关操作,同时必须先清除一次定时器,否则游戏进行到第二轮或以上,定时器会叠加执行
fireUp为开火行为,第一次延迟1毫秒,之后每200毫秒执行一次
function gameContinue(){
$('.layout').hide();
gameSt.playing = true;
gameProccess();
clearInterval(timer.general);
timer.general=null;
timer.openFire = setTimeout(fireUp,1);
timer.general = setInterval(zijiOperator,21);
}
弹幕在游戏一开始就生成,只是将其放在屏幕看不见的地方作为弹幕池以备用,需要时再发出请求,子弹击中或者飞出屏幕后再回收,弹幕池里共有512枚子弹,因为屏幕子弹数达到250左右就会出现明显的卡顿了,所以我在后面将活动的弹幕控制在150左右,理论上是用不完的。
/*生成弹幕池*/
function initDanmaku(){
var dmFrag = document.createDocumentFragment();//以文档碎片的形式添加弹幕
for(var i=0; i<511; i++){
var elem = document.createElement("p");
elem.id = "danmaku"+i;
elem.className = "poolBlt";
elem.title = "1";
resetObject(elem,"620px","620px")
dmFrag.appendChild(elem);
}
$gameScreen.append(dmFrag);
return true;
}
/*请求弹幕池*/
function requestDanmaku() {
if(gameSt.playing){
for(var i=0; i<511; i++){
if(document.getElementById("danmaku"+i).title==="1"){
document.getElementById("danmaku"+i).title = "2";
return i;
}
}
}
}
生成敌人和boss
/*游戏流程控制*/
function gameProccess(){
clearInterval(timer.overflowOperator);//防止Interval叠加,下同
clearInterval(timer.enemyCreator);
timer.overflowOperator = timer.enemyCreator = null;
if(gameSt.playing) {
createEnemy();//生成敌人
//回收子弹和横向敌人
timer.overflowOperator = setInterval(function () {
ammoOperating();
enemy1Operating();
}, 200)//每200毫秒回收一次飞出屏幕的敌机和弹幕
}
//游戏开始30秒后生成BOSS
timer.releaseBoss = setTimeout(BossInSight,30000)
}
生成BOSS和BOSS死亡后的回调,BOSS具体如何运动的,请参考第三节:
/*BOSS状态*/
function BossInSight() {
if(gameSt.playing && !gameSt.boss){
clearTimeout(timer.releaseBoss);//防止定时器叠加
console.log("Enemy Ultra Cruiser is Approaching!");
/*BOSS出现后,小怪不再生成*/
clearInterval(timer.enemy2Creator);
clearInterval(timer.enemyCreator);
timer.enemy2Creator = timer.enemyCreator = null;
gameSt.boss = true;//BOSS状态为真
drawBoss();//绘制BOSS
}
}
/*BOSS死亡后*/
function BossDie() {
$('#theBoss').removeClass('damagedBoss').stop().remove();
gameSt.boss = false;
boss.state = 1;
createEnemy();
setTimeout(BossInSight,30000);
}
游戏结束后执行:清除定时器,停止自机行为,删除屏幕上的所有弹幕和敌人,弹出主菜单:
function endGame(){
gameSt.playing = gameSt.boss = false;//游戏进行和打BOSS状态关闭
clearTimeout(timer.openFire);//先停火
alert("你的生命值被耗尽,游戏结束");
alert("最终得分:"+gameSt.score);
keySt.toUp = keySt.toDown = keySt.toLeft = keySt.toRight = keySt.toLSpeed = keySt.toFire = false;//停止移动
/*下次将这些对象属性进一步细分,以便能够遍历*/
clearInterval(timer.enemyCreator);
clearInterval(timer.overflowOperator);
clearInterval(timer.general);
clearTimeout(timer.enemy2Creator);
clearTimeout(timer.releaseBoss);
clearTimeout(bossAction);
for(item in timer){
timer[item]=null
}
/*删除所有子弹、敌机*/
$('.enemy1,.enemy2,.bullet,#theBoss,.poolBlt').stop().remove();
/*重置自机和数据*/
resetObject(ziji,"250px","300px");
resetData();
/*显示主菜单*/
$('.layout1').show();
}
//重置数据
function resetData(hp,score,graze){
gameSt.hp = hp || 50;
gameSt.score = score || 0;
gameSt.graze = graze || 0;
$('#hitCount').html(gameSt.hp);
$('#zijiPos').html(gameSt.graze);
$('#score').html(gameSt.score);
}
//重置对象CSS,传px
function resetObject(obj,x,y){
obj.style.left = x ;
obj.style.top = y;
}
开火时显示2根激光柱,并且执行歼灭逻辑(消灭正前方的敌人),为了节约性能,我把自机的武器做成了瞬发,即时用即时判断:
/*/////////////开火/////////////*/
function fireUp(){
eliminate();//歼灭敌人
$('#laser').show();
$('#laser').fadeOut(100);
if(gameSt.playing){
setTimeout(fireUp,250);
}
}
* 敌人被击中判定 */
function eliminate(){
var px = ziji.offsetLeft+43,py = ziji.offsetTop;//开火判定位置偏移到元素视觉中心
$('.theEnemy').each(function(){
var ex = this.offsetLeft+30,ey = this.offsetTop;
if(ex+35>px && ex-35100+gameSt.diff*100);
explode(0,ex,ey);//在被歼灭的位置绘制爆炸效果
$(this).remove();
}
});
/*打BOSS另算*/
if(gameSt.boss){
hitTheBoss(px,py)
}
}
zijiOperator是自机实现连续移动和碰撞的核心,每21毫秒判断执行一次,需要注意的是我们希望子弹元素与自机元素判断碰撞的位置是自机元素视觉中心,而非元素锚点位置(元素左上角),所以必须向右和向下偏移一定的位置:
function zijiOperator() {
var mvrg = keySt.toLSpeed? 4:10;//低速/高速状态下的移动距离
if(keySt.toLeft){
ziji.style.left=ziji.offsetLeft-mvrg+"px";
ziji.style.backgroundPositionX = "-90px";//自机左右移动会变换贴图样式
}
if(keySt.toRight){
ziji.style.left=ziji.offsetLeft+mvrg+"px";
ziji.style.backgroundPositionX = "-180px";
}
if(keySt.toUp){
ziji.style.top=ziji.offsetTop-mvrg+"px";
ziji.style.backgroundPositionX = "0px";
}
if(keySt.toDown){
ziji.style.top=ziji.offsetTop+mvrg+"px";
ziji.style.backgroundPositionX = "0px";
}
if(!keySt.toRight && !keySt.toLeft){
ziji.style.backgroundPositionX = "0px";
}
if(keySt.toLSpeed){
spot.style.visibility = "visible";//低速模式下显示判定点和擦弹圈
}else{
spot.style.visibility = "hidden";
}
if(gameSt.playing){
gameSt.score ++;
$('#score').text(gameSt.score);
}
limit(ziji);//防止自机飞出游戏屏幕
collision(ziji.offsetLeft+37,ziji.offsetTop+37);//传入自己XY坐标判断与子弹的碰撞,但是需要偏移到元素视觉中心
}
接下来处理碰撞与防溢出逻辑
/***********************防溢出**********************************/
function limit(ziji){/*对象*/
//上下溢出
ziji.style.top = ziji.offsetTop<-35?"-35px":ziji.offsetTop>550?"550px":ziji.style.top;
//左右溢出
ziji.style.left = ziji.offsetLeft<-35?"-35px":ziji.offsetLeft>550?"550px":ziji.style.left;
}
/************************自机碰撞判定***************************/
/*
*通过遍历所有子弹相对自机的距离判断是否碰撞
*判断区域是圆形
*擦弹加分
*中弹减生命,生命为0结束游戏
*/
function collision(x,y){
$('.bullet').each(function () {
var bltX = this.style.left.slice(0,-2)
var bltY = this.style.top.slice(0,-2)
if( Math.pow((x-bltX),2)+Math.pow((y-bltY),2)<750 ){
/*擦弹距离根号750*/
gameSt.graze++;
gameSt.score += parseInt(gameSt.graze/20);
$('#zijiPos').text(gameSt.graze);
}
if( Math.pow((x-bltX),2)+Math.pow((y-bltY),2)<150 ){
/*中弹距离约14左右*/
gameSt.hp-=10;
explode(1,bltX,bltY);//在中弹位置生成视觉效果
recycleBullet(this);//删除击中的子弹
$('#hitCount').text(gameSt.hp);//更新显示生命值
if(gameSt.hp<=0)
endGame();//gg
}
});
}
敌人和BOSS的生成,注意下面很多代码都出现了gameSt.diff变量,这个是难度系数对游戏数值的补正
/*生成敌人*/
function createEnemy(){
//生成敌人1和敌人2
timer.enemyCreator = setInterval(function(){
if($('.left').length==0 && $('.right').length==0){
var both = Math.random()>0.1?1:0;//是否两边出飞机
if(both){
drawEnemy1(1,2+gameSt.diff,4500 - gameSt.diff*500);
drawEnemy1(0,2+gameSt.diff,4500 - gameSt.diff*500);
} else {
/*单边出飞机*/
var lr = Math.random()>0.5?1:0;
drawEnemy1(lr,3+gameSt.diff*2,4500 - gameSt.diff*500);
}
}
timer.enemy2Creator = setTimeout(drawEnemy2,(Math.random()*1000+200))
},1000)
}
敌人1是从屏幕左上或右上飞入的战机,飞到一定距离发射2枚子弹,其中一枚是自机狙,敌机2是从屏幕顶部垂直飞下来的战机,降到一定高度发射开花弹,我们首先构造这两种敌人,在绘制函数中实例化
//构造横向飞行物
function Xobject(sign,speed,hp){
this.ox = sign?-60:620//初始位置(左还是右)
this.x = sign?660:-90;//结束位置与初始位置相对
this.oy = parseInt(Math.random()*100);//初始高度
this.y = parseInt(Math.random()*100);//到达目的地的高度
this.hp = hp || 10;//这个貌似没用
this.speed = speed || 3000;
}
//构造纵向飞行物
function Yobject(hp,speed){
this.x = Math.random()*500+10 || 300;//随机出现在顶端
this.oy = -30;
this.y = -60;
this.hp = hp || 10;
this.speed = speed || 1000;
}
//绘制横向飞行物,0为左,1为右
function drawEnemy1(lr,howmany,speed){
var obj = new Xobject(lr,speed)//左边还是右边生成
var frag = document.createDocumentFragment()
for(var i = 0; ivar div = $('');
div.addClass("theEnemy");
div.addClass(lr?'enemy1 left':'enemy1 right');
div.css({
left:obj.ox + "px",
top:obj.oy + "px"
});
div.appendTo($(frag));
}
$(frag).appendTo($gameScreen);
for(var i = 0; i/*判断左右。。。。这里还需要再完善*/
if(lr){
var eachObj = $('.left:eq('+i+')');
eachObj.delay(speed/5*i).animate({left: obj.x+'px', top: obj.y+'px'},obj.speed,"linear");
}
else{
var eachObj = $('.right:eq('+i+')')
eachObj.delay(speed/5*i).animate({left: obj.x+'px', top: obj.y+'px'},obj.speed,"linear");
}
}
}
//敌人1开火与回收
function enemy1Operating(){
$('.enemy1').each(function(){
var obj = $(this);
var objX = obj.offset().left, objY = obj.offset().top;
/*指定一个随机范围的位置,如果敌人1到达指定位置即开火*/
var ranL = parseInt(50+Math.random()*10);
var ranR = parseInt(550-Math.random()*10);
if(objX>ranL && objXif(obj.css('opacity')=="1"){
/*发射完毕后试图隐身撤离*/
doLaunch(objX,objY+20);//发射直线弹
doLaunch(objX,objY+20,1);
obj.css('opacity','0.7');
}
}
if(objX>650 || objX<-80){
$(this).remove();
}
})
}
//敌人2行为
function drawEnemy2(howmany,speed){
var obj2 = new Yobject(howmany,speed);
/*从屏幕顶端随机X坐标下落*/
var ranY = Math.random()*100+50;
var div = $('').addClass('enemy2').addClass('theEnemy');
div.css({
top: obj2.y + 'px',
left: obj2.x + 'px'
});
div.appendTo($gameScreen);
/*2层动画,先下落,再发射,再向上逃离,并删除*/
div.animate({top: ranY+'px'},obj2.speed,"linear",function(){
if(!$(div).is(':hidden')){
blossomLaunch(div[0].offsetLeft+10,div[0].offsetTop+20,"linear",0,1);//发射开花弹
}
}).animate({top: '-100px'},obj2.speed,"linear",function(){
div.remove();
});
}
BOSS是一个血超厚的敌人,随机在屏幕上半部分乱飞,并且发射有规则的弹幕
/*绘制BOSS*/
function drawBoss(){
$('#theBoss').remove();
var div = $('').attr('id','theBoss');//创建一个BOSS元素
resetObject(div[0],"200px","-200px");//设置BOSS起始位置
boss.health = 150+100*gameSt.diff;//定义BOSS血量
div.appendTo($gameScreen);
$('').appendTo($('#theBoss'));//给BOSS添加生命条
/*首先让BOSS从初始位置下降到屏幕中*/
div.animate({top:"50px",left:"200px"},1500,"linear",function () {
blossomLaunch(290,130,"easeOutQuad",0,1,1);//初见杀
bossAction();//BOSS开始乱跑
})
}
/*Boss行动*/
function bossAction() {
var bossX = Math.floor(Math.random()*400) , bossY = Math.floor(Math.random()*100);//随机BOSS飞行位置
/*BOSS移动到目的地后才发射弹幕*/
$('#theBoss').animate({
top: bossY+"px",
left: bossX+"px"
},1000,function () {
if(gameSt.boss){
/*根据BOSS形态,分别发射2组不同的弹幕*/
switch (boss.state){
case 1:{
blossomLaunch(bossX+90,bossY+80,"linear",1,1+gameSt.diff,$('#theBoss'));//开花弹
OutLaunch(gameSt.diff*5);//召唤支援火力
};break;
case 2:{
pentagonStar(ziji.offsetLeft+37,ziji.offsetTop+37,80,5+2*gameSt.diff,1)
pentagonStar(ziji.offsetLeft+37,ziji.offsetTop-13,240,5+2*gameSt.diff,2)//用五角星围住玩家
};break;
default:{
blossomLaunch(bossX+90,bossY+80,"linear",1,1+gameSt.diff,$('#theBoss'))//默认是螺旋弹
};
}
}
})
if(gameSt.boss){
setTimeout(bossAction,3500+gameSt.diff*500);//重复这个函数
}
}
四、弹幕算法
本游戏中暂时没有拐弯飞行的子弹,所有的子弹都是直线飞行
核心实现:请求弹幕池,定义初始坐标,用animate发射到目的地坐标去,
先定义最简单的,单个子弹直线发射函数,自机狙指的是朝玩家当前方向射去的子弹:
//直线发射,snipe为自机狙
function doLaunch(x,y,snipe,direct){
var toXY = [];
var blt1 = $('#danmaku' + requestDanmaku());//请求弹幕池中不在动画序列中的子弹的,返回ID值
/*如果不是自机狙,则子弹落点随机*/
if(!snipe){
toXY = [direct?direct:parseInt(Math.random()*800),650];
} else {
toXY = snipeCoord(x,y);//这里返回的是子弹-玩家连线的延长线终点坐标,终点确保能飞出屏幕外以被子弹回收器回收
}
shootIt(blt1,x,y,toXY[0],toXY[1],2500-gameSt.diff*250,"linear",0);//只要朝着给定的坐标射去就好了
}
开花弹逻辑(包括螺旋弹)
落点位置是CircleMatrix使用函数调用后返回的一圈圆形离散坐标,setTimeout不能用for循环实现,我这里根据变量作用域链原理去执行有限次数的setTimeout
/*
*开花弹
*参数:起始X,起始Y坐标,速度曲线,是否螺旋发射,几层圆圈(多为定义螺旋弹的圈数),发射本体是否还存在
*/
function blossomLaunch(x,y,ease,spiral,lay,byWho){
var ammo2Speed = 8000-400*gameSt.diff;//飞行速度
var ammo2Number = parseInt(Math.random()*20+5)*gameSt.diff;//多少颗子弹组成的圆圈
var mtx = new Array;//声明发射坐标数组
for(var i=0;i//有几层就请求多少圈圆形坐标
}
var mtxRest = mtx.length-1;//螺旋发射弹幕调用timeout次数
/*内部定义螺旋发射弹幕*/
function spiralLaunch() {
var bltSpiral = $('#danmaku' + requestDanmaku());
shootIt(bltSpiral,x,y,mtx[mtxRest][1],mtx[mtxRest][0],ammo2Speed,ease);//发射
mtxRest--;
if(mtxRest>=0 && byWho){
/*如果发射本体不存在了或者子弹发射完毕,螺旋弹幕将不再继续发射*/
setTimeout(spiralLaunch,10)
}
}
if(spiral){
spiralLaunch();//螺旋发射
} else {
for(var i = mtx.length-1; i>=0; i--){
var blt2 = $('#danmaku' + requestDanmaku());
shootIt(blt2,x,y,mtx[i][1],mtx[i][0],ammo2Speed,ease)
}//正常发射
}
}
CircleMatrix函数:
function CircleMatrix(num) {/*传入多少颗*/
var len = num || 20;/*默认20颗*/
var arr = [];
for(var i=0; i1291*Math.sin(2*Math.PI*i/len),1291*Math.cos(2*Math.PI*i/len)]);
}
return arr;
}
从屏幕外360度随机飞入的自机狙:
/*外部自机狙*/
function OutLaunch(num) {
var delay = Math.floor(Math.random()*500+100);//不同时发射
var rest = num;
delayLaunch();
//内部定义执行的timeout
function delayLaunch() {
/*随机一个屏幕外的坐标*/
var ranX,ranY;
do{
ranX = Math.floor(Math.random()*630-15);
ranY = Math.floor(Math.random()*630-15);
} while(ranX>-10 && ranX<610 && ranY>-10 && ranY<610)
doLaunch(ranX,ranY,1);//直线发射
rest--;
if(rest>0){
setTimeout(delayLaunch,delay)
}
}
五角星弹幕
逻辑比之前的复杂,游戏中我选用了反向扩张的五角星:实际上还有另外3种发射方式:
*五角星*/
/*
*起始X,起始Y,宽度,每条边子弹数量,运动模式:
*1. 正向扩张
* 2. 反向扩张
* 3. 自机狙
* 其他 竖直砸下
* */
function pentagonStar(x,y,size,num,mode) {
var coord = fillPentagon(size,x,y,num);//获取五角星初始坐标
var coolen = coord.length;
for(var i=0;iif( Math.pow((ziji.offsetLeft+37-coord[i][0]),2)+Math.pow((ziji.offsetTop+37-coord[i][1]),2)>1000 ){
var blt3 = $('#danmaku' + requestDanmaku());
var toXY = [];
switch(mode){
case 1:{
if(i>=0 && i5){
toXY[0] = 0;
toXY[1] = -650;
}else if(i>=coolen/5 && i*2/5){
toXY[0] = 650*Math.tan(Math.PI/5)/// + coord[i][0];
toXY[1] = 650;
} else if(i>=coolen*2/5 && i*3/5){
toXY[0] = -650;
toXY[1] = -650*Math.tan(Math.PI/5)// - coord[i][0];
} else if(i>=coolen*3/5 && i*4/5){
toXY[0] = 650;
toXY[1] = -650*Math.tan(Math.PI/5)// - coord[i][0];
} else {
toXY[0] = -650;
toXY[1] = 650*Math.tan(Math.PI/5);
}
toXY[0] = "+=" + toXY[0];
toXY[1] = "+=" + toXY[1];
};break;
case 2:{
if(i>=0 && i5){
toXY[0] = 0;
toXY[1] = 650;
}else if(i>=coolen/5 && i*2/5){
toXY[0] = -650*Math.tan(Math.PI/5) // + coord[i][0];
toXY[1] = -650;
} else if(i>=coolen*2/5 && i*3/5){
toXY[0] = 650;
toXY[1] = 650*Math.tan(Math.PI/5) //+ coord[i][0];
} else if(i>=coolen*3/5 && i*4/5){
toXY[0] = -650;
toXY[1] = 650*Math.tan(Math.PI/5) //+ coord[i][0];
} else {
toXY[0] = 650;
toXY[1] = -650*Math.tan(Math.PI/5) //- coord[i][0];
}
toXY[0] = "+=" + toXY[0];
toXY[1] = "+=" + toXY[1];
};break;
case 3:{
toXY = snipeCoord();
};break;
default:{
toXY[0] = "+=0";
toXY[1] = "+=650";
};
}
blt3.hide();//一开始不显示
shootIt(blt3,coord[i][0],coord[i][1],toXY[0],toXY[1],5000-400*gameSt.diff,"linear",2500-gameSt.diff*200);//先绑定发射动画
blt3.delay(i*20).fadeIn(1,function () {});//加入延迟使每颗五角星一个个按轨迹出现
}
}
}
如何返回一个离散的正五角星坐标?
需要传入五角星横边中点坐标、五角星的边长和每条边要生成多少枚子弹。先定下五角星五个角的坐标ABCDE,再分别给五条边AD\AC\CE\EB\BD填充一定数量的子弹的坐标:
实现原理:
/* 构造五角星的五个端点坐标 */
/*传入边长,O点相对于画布X,Y的位置*/
function Pentagon(a,x,y) {
var pt = [
[-a-a*Math.sin(Math.PI/10)+x,y],//-a-a*sin18°,0
[a+a*Math.sin(Math.PI/10)+x,y],//a+a*sin18°,0
[-a*Math.cos(Math.PI/5)+x,
a*Math.cos(Math.PI/5)*(1/Math.tan(Math.PI/10))-a*Math.cos(Math.PI/10)+y],//-a*cos36°,-a*cos36°*cot18°+a*cos18°
[x,-a*Math.cos(Math.PI/10)+y],//0,a*cos18°
[a*Math.cos(Math.PI/5)+x,
a*Math.cos(Math.PI/5)*(1/Math.tan(Math.PI/10))-a*Math.cos(Math.PI/10)+y]//a*cos36°,-a*cos36°*cot18°+a*cos18°
];//这里是个[[x1,y1],[x2,y2],[x3,y3],[x4,y4],[x5,y5]]结构的二位数组
return pt;//返回这五个点的坐标
}
/*连线五角星*/
function fillPentagon(a,x,y,num) {
a = a || 120;
num = num || 7+2*gameSt.diff;
var pt = Pentagon(a,x,y);//获取五角星端点
var arr = new Array();
for(var i=0; i<5; i++){
var j = 0;
var len = pt[(i+1)%5][0]-pt[i][0];
while(Math.abs(j)<Math.abs(len)){
arr.push(
[pt[i][0]+j,pt[i][1]+j*(pt[(i+1)%5][1]-pt[i][1])/len]
)
j += len/num;
}
}
return arr;//这下返回的就是一个离散五角星点了
}
shootIt是将执行的动画序列封装起来,以便不同类型的弹幕调用
/*参数依次是:要发射的子弹对象,起始横坐标,起始纵坐标,目的地横坐标,目的地纵坐标,飞行速度,速度曲线,延迟发射时间*/
function shootIt($obj,x,y,toX,toY,speed,ease,delay) {
speed = speed || 2500-gameSt.diff*250;
ease = ease || "linear"
$obj.addClass('bullet');
$obj.css({
left: x+"px",
top: y+"px"
});
function delayShoot() {
if($obj.attr("title")==2){
$obj.animate({top: toY + "px", left: toX + "px"},speed,ease);
}
}
if(!delay){
$obj.animate({top: toY + "px", left: toX + "px"},speed,ease);
} else {
setTimeout(delayShoot,delay)//不会出现for循环setTimeout问题了
}
}
五、事件监听器
给自机绑定键盘事件
//Bind KeyBoardEvent
window.onkeydown = function(event){
var event=event||window.event;//兼容
var code = event.keyCode || event.which;
//console.log(event.key)//获取按键名称
/*开控制台*/
if(code==38 || code == 40){
event.preventDefault();
}//防止滚屏
switch(code){
case 13:{ /*ENTER*/
if(!gameSt.playing){
gameInit();
}
};break;
case keySt.LS : {keySt.toLSpeed=true; } ;break;//You can check Keycode at Keymap.js at this path
case keySt.LT : {keySt.toLeft=true; } ;break;
case keySt.UP : {keySt.toUp=true; } ;break;
case keySt.RT : {keySt.toRight=true; } ;break;
case keySt.DN : {keySt.toDown=true; } ;break;
// case keySt.FR : {keySt.tofire=true; fireUp() } ;break;
case 80 : pauseTheGame() ;break;
}
}
window.onkeyup = function(event){
var event=event||window.event;
var code = event.keyCode || event.which;
switch(code){
case keySt.LS : keySt.toLSpeed=false ;break;//Release Key to break the loop
case keySt.LT : keySt.toLeft=false ;break;
case keySt.UP : keySt.toUp=false ;break;
case keySt.RT : keySt.toRight=false ;break;
case keySt.DN : keySt.toDown=false ;break;
// case keySt.FR : keySt.toFire = false ;break;
}
}
给按钮绑定鼠标点击事件
/*This is for Mouse-click Test for Creating barrage*/
/*鼠标点击游戏画面可以测试弹幕*/
$gameScreen.click(function (e) {
var X = e.clientX,Y=e.clientY;
// pentagonStar(X,Y,80,7+2*gameSt.diff,1)
// pentagonStar(X,Y-50,240,7+2*gameSt.diff,2)
//OutLaunch(9);
//blossomLaunch(X,Y,"linear",1,3,1)
//doLaunch(X,Y,1,0)
})
$('#readme').click(function () {
$('.layout1').hide();
$('.layout3').show();
})
$('#gameConfig').click(function () {
$('.layout1').hide();
$('.layout2').show();
})
$('.returnMain').click(function () {
$('.layout').hide();
$('.layout1').show();
})
/* 难易度选择 */
$('.selectDiff').click(function () {
switch(this.innerText){
case "简单":gameSt.diff = 0;break;
case "正常":gameSt.diff = 1;break;
case "疯狂":gameSt.diff = 2;break;
default: console.log(this.innerText);break;
}
$('#difficult').val(this.innerText);
$('.currDiff').text($('#difficult').val());
$('.selectDiff').css({ borderColor:"transparent" });
$(this).css({ borderColor:"#fff" });
})
/*这个日后开发*/
$('.keySet').click(function(){
alert('developing');
})