Hi there I want to make a small android rhythm game but I have a performance problem. At the beginning of the scene the FPS is at 55 but after a while they drop to 30. It is important to mention that I not only use Phaser on my page, but also HTML elements, such as an audio tag, a video tag and several div tags. with flashy CSS colors, background and animations.
I need my game to be fluid, otherwise the user will be disappointed an surely will uninstall the app.
Please someone help me ?
Here is the HTML:
<html>
<head>
<title>Bear Harmony</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=640, height=360, initial-scale=1.0">
<link rel="icon" href="assets/images/icon.png">
<link rel="stylesheet" type="text/css" href="game/style.css">
<link rel="stylesheet" type="text/css" href="libs/Animate/animate.css">
<link rel="stylesheet" type="text/css" href="libs/Lyricer/lyricer.css">
<script type="text/javascript" src="libs/Phaser/phaser.js"></script>
<script type="text/javascript" src="libs/Phaser/plugins/SaveCPU.js"></script>
<script type="text/javascript" src="libs/JSZip/jszip.js"></script>
<script type="text/javascript" src="libs/Lyricer/lyricer.js"></script>
<script type="text/javascript" src="game/constants.js"></script>
<script type="text/javascript" src="game/game.js"></script>
<script type="text/javascript" src="game/boot.js"></script>
<script type="text/javascript" src="game/menu.js"></script>
<script type="text/javascript" src="game/load.js"></script>
<script type="text/javascript" src="game/play.js"></script>
<script type="text/javascript" src="game/read.js"></script>
<script type="text/javascript" src="game/make.js"></script>
<script type="text/javascript" src="game/score.js"></script>
<script type="text/javascript" src="game/loadZip.js"></script>
<script>
function devlog(txt) {
var div = document.getElementById('dev-console');
div.innerHTML += txt+"<br>";
div.scrollTo({top:div.scrollHeight,left:0,behavior: "smooth",});
}
function deverr(txt) {
var div = document.getElementById('dev-console');
div.innerHTML += '<span style="color:red">'+txt+'</span><br>';
div.scrollTo({
top:div.scrollHeight,
left:0,
behavior: "smooth",
});
}
function devprint(txt) {
var div = document.getElementById('dev-output');
div.innerHTML = txt;
}
window.onerror = function(details, file, line) {
var arrs = file.split("/");
var filename = arrs[arrs.length-1];
var message = "<h3>"+details+"<br>Line "+line+" of "+filename+"<h3/>";
deverr(message);
}
</script>
</head>
<body>
<div id="rotation"></div>
<div id="content" class="fullscreendiv">
<!--logo-->
<div id="logo"></div>
<!--div for phaser game-->
<div id="game"></div>
<!--video and audio-->
<video id="video" muted="muted" ></video>
<audio id="audio"></audio>
<!--hp bar, score and combo counter-->
<meter id="hp-bar" value="50" min="0" max="100" low="20" high="100" optimum="50"></meter>
<div id="score-counter" class="bhfont">0</div>
<div id="combo-counter" class="tsfont blink"></div>
<!--final score-->
<div id="score" class="bhfont hidden">
<h3 class="bhfont">Puntuación</h3>
<div class="bhfont" id="scores"></div>
<div id="scoreBkg"></div>
</div>
<!--main menu-->
<div id="menu">
<button ontouchend="showSelection()" style="position:absolute;top:10%;left:10%;width:40%;height:40%;" class="animate__animated animate__fast animate__backInRight">Seleccionar</button>
<button ontouchend="showOpenZip()" style="position:absolute;top:10%;left:55%;width:35%;height:40%;" class="animate__animated animate__fast animate__delay-100ms animate__backInRight">Abrir ZIP</button>
<button ontouchend="showEditor()" style="position:absolute;top:60%;left:10%;width:20%;height:30%;" class="animate__animated animate__fast animate__delay-200ms animate__backInLeft">Editor</button>
<button ontouchend="showScores()" style="position:absolute;top:60%;left:35%;width:55%;height:30%;" class="animate__animated animate__fast animate__delay-500ms animate__backInLeft">Puntuaciones</button>
</div>
<div id="select" class="bhfont hidden">
<h3 class="bhfont">Seleccionar Canción</h3>
<img id="songPreview">
<div class="dsfont" id="metainfo"></div>
<button ontouchend="prevSong()" style="position:absolute;top:25%;left:5%;width:10%;height:50%;"><</button>
<button ontouchend="nextSong()" style="position:absolute;top:25%;right:5%;width:10%;height:50%;">></button>
<button ontouchend="toMenu();hidediv('select')" style="position:absolute; bottom:10%;left:5%; width:40%; padding-left:15px;padding-right:15px">Volver</button>
<button ontouchend="startGame();hidediv('select')" style="position:absolute; bottom:10%;right:5%; width:40%; padding-left:15px;padding-right:15px">Jugar</button>
</div>
<div id="editor" class="bhfont hidden">
<h3 class="bhfont">Editor</h3>
<span class="dsfont">
<p>El editor te permite crear archivos ZIP que puedes abrir en el juego.
<p>El editor aún no está terminado por lo que podría resultar confuso el editar canciones.
</span>
<p>
<button ontouchend="toMenu();hidediv('editor')" style="position:absolute; bottom:10%;left:5%; width:40%; padding-left:15px;padding-right:15px">Volver</button>
<button ontouchend="openEditor()" style="position:absolute; bottom:10%;right:5%; width:40%; padding-left:15px;padding-right:15px">Ir al editor</button>
</div>
<div id="openzip" class="bhfont hidden">
<h3 class="bhfont">Abrir ZIP</h3>
<span class="dsfont">
<p>La función Abrir ZIP te permite abrir un paquete creado en el editor.
<p>Selecciona un archivo ZIP que hayas creado y luego pulsa JUGAR.
</span>
<p>
<input id="zipfile" type="file" accept="application/zip">
<button ontouchend="toMenu();hidediv('openzip')" style="position:absolute; bottom:10%;left:5%; width:40%; padding-left:15px;padding-right:15px">Volver</button>
<button ontouchend="readZip();" style="position:absolute; bottom:10%;right:5%; width:40%; padding-left:15px;padding-right:15px">Jugar</button>
</div>
<div id="zipLoadingScreen" class="bhfont hidden">
<div id="zipLoadingBkg"></div>
<div style="text-align:left; position:absolute; bottom:10%;left:5%; width:80%; padding-left:15px;padding-right:15px; z-index:top;">Cargando archivo...</div>
</div>
</div>
<div id="debug">
<div id="dev-output"></div>
<div id="dev-console"></div>
</div>
<div id="statsFrame">
<div id="statsFps">N/A</div>
</div>
</body>
</html>
and here is the JS of the main game scene
var key1,key2,key3,key4;
var aud,vid;
var hpTween,scoreTween;
var currentNft = 0;
var player = {
score: 0,
combo: 0,
health: 50,
perfect: 0,
great: 0,
good: 0,
bad: 0,
miss: 0
};
var internal = {
loadingFromZip: false,
gameOver: false,
};
// Play State
var Play = function () {
};
Play.prototype = {
preload: function(){
},
create: function(){
var ths = this;
game.input.circle = new Phaser.Circle(0,0,72);
this.batch = game.add.spriteBatch();
function createKey(x,y,c) {
key = game.add.sprite(x,y,'arrow_control',0);
key.anchor.setTo(.5,.5);
return key;
}
key1 = createKey(90,280,1);
key2 = createKey(242,280,2);
key3 = createKey(394,280,3);
key4 = createKey(546,280,4);
this.start();
},
update: function() {
devprint("Debug: "+game.time.fps+"FPS S:"+game.time.suggestedFps+" hp:"+player.health);
},
// start game
start: function(){
this.setAudioVideo();
this.addParticles();
playSound('ready',false,1.0);
playVoice('Miku_GameStart',14,0.3);
playVoice('Trance_Ready',2,0.5);
},
// sync audio with video
setAudioVideo: function() {
aud = $('audio');
vid = $('video');
var localAudioUrl = 'data/'+folder+'/'+files.music;
var localVideoUrl = 'data/'+folder+'/'+files.video;
var localPosterUrl = 'data/'+folder+'/'+files.background;
var zipAudioUrl = zipFile.music;
var zipVideoUrl = zipFile.video;
var zipPosterUrl = zipFile.background;
if (internal.loadingFromZip) {
aud.src = zipAudioUrl;
vid.src = zipVideoUrl;
vid.poster = zipPosterUrl;
$('scoreBkg').style.backgroundImage = 'url("'+zipPosterUrl+'")';
} else {
aud.src = localAudioUrl;
vid.src = localVideoUrl;
vid.poster = localPosterUrl;
$('scoreBkg').style.backgroundImage = 'url("'+localPosterUrl+'")';
}
aud.volume = volume;
aud.addEventListener('pause', function (){
vid.pause();
vid.currentTime = aud.currentTime;
});
aud.addEventListener('play', function (){
vid.play();
vid.currentTime = aud.currentTime;
});
aud.addEventListener('seeked', function (){
vid.currentTime = aud.currentTime;
});
game.time.events.add(2000, function (){
aud.play();
this.makeNotes();
},this);
},
// make notes
makeNotes: function(){
currentNft = defaultnft;
var ths = this;
player = {score:0,combo:0,health:50,perfect:0,great:0,good:0,bad:0,miss:0};
function addNote(code,nft){
var dist = 360;
const note = game.add.sprite({1:90,2:242,3:394,4:546}[code], -dist, 'notes', 'arrow_'+arrowcolor);
note.angle = {1:0,2:90,3:270,4:180}[code];
note.anchor.setTo(.5,.5);
note.inputEnabled = true;
note.code = code;
note.sendToBack();
note.input.priorityID = 100;
note.events.onInputOver.add(function(){
this.hitNote(note);
},ths);
note.checkWorldBounds = true;
note.events.onEnterBounds.add(function(){
note.events.onOutOfBounds.add(function (){
this.missNote();
note.kill();
},ths);
},ths);
ths.batch.addChild(note);
var t1 = game.add.tween(note);
t1.to({y:280},nft,"Linear",true,0);
t1.onComplete.add(function (){
var t2 = game.add.tween(note);
const nft2 = nft;
t2.to({y:280+(280+dist)},nft2,"Linear",false,0);
t2.start();
// optimizar
t2.onComplete.add(function (){
t2.stop();
});
t1.stop();
},ths);
}
for(var i=0;i<notes.length;i++){
const note = notes[i];
const num = i+1;
function getNft(time){
for(var i=0;i<nftchanges.length;i++){
var s = nftchanges[i];
var ns = nftchanges[i+1]||null;
var t = s[0]*1000;
var nt = (ns!=null)?ns[0]*1000:99999999999;
if (time>=t) return s[1];
}
};
game.time.events.add(((note.time*1000)-currentNft)+delay,function(){
addNote(note.code,getNft(note.time*1000));
});
}
for(var i=0;i<nftchanges.length;i++){
const nft = nftchanges[i];
game.time.events.add(((nft[0]*1000)-currentNft)+delay,function(){
currentNft = nft[1];
});
}
game.time.events.add(stars.first*1000, function (){
this.getFirstStar();
},this);
game.time.events.add(stars.second*1000, function (){
this.getSecondStar();
},this);
game.time.events.add(stars.third*1000, function (){
this.getThirdStar();
},this);
var endtime = ($('audio').duration*1000)-2000;
game.time.events.add(endtime,function(){
this.finishSong();
playSound('Perfect');
},this);
showdiv('hp-bar',true);
showdiv('score-counter',true,'bhfont animate__animated animate__faster animate__delay-200ms animate__fadeInDown');
this.updateHpBar();
this.updateScore();
},
finishSong: function (){
var s = game.add.sprite(640,0,'finish');
var del = 360;
var t1 = game.add.tween(s);
t1.to({x:0},del,"Linear",true,0);
var t2 = game.add.tween(s);
t2.to({x:-640},del,"Linear",false,1000);
t1.onComplete.add(function (){
t2.start();
vid.pause();
vid.load();
changeState('Score',600,0xFFFFFF);
});
hidediv('hp-bar');
hidediv('combo-counter');
hidediv('score-counter');
internal.loadingFromZip = false;
},
hitNote: function(note) {
var key = {1:key1,2:key2,3:key3,4:key4}[note.code];
var angle = note.angle;
if(!key.overlap(note))return;
this.judgeNote(280-note.y,note,key);
this.hitKey(key,note.x,angle);
this.showCombo();
note.kill();
},
judgeNote: function(diff,note,key){
var judge = "perfect";
var sumScore = 0;
if (diff>11) judge = "great";
if (diff>25) judge = "good";
if (diff>40) judge = "bad";
switch(judge){
case "perfect":
player.perfect++;
sumScore = 1000;
break;
case "great":
player.great++;
sumScore = 500;
break;
case "good":
player.good++;
sumScore = 100;
break;
case "bad":
player.bad++;
sumScore = 10;
player.combo = 0;
break;
}
player.combo++;
player.score += player.combo*sumScore;
this.updateScore();
this.healPlayer(3);
const s = game.add.sprite(gameWidth/2,gameHeight/3,"judges",judge);
s.anchor.setTo(.5,.5);
s.scale.setTo(.55,.55);
var del = currentNft/5;
var t1 = game.add.tween(s.scale);
t1.to({x:0.7,y:0.7},del/3,"Linear",true,0);
t1.yoyo(true);
t1.onComplete.add(function (){
t1.stop();
});
var t2 = game.add.tween(s.scale);
t2.to({x:1.5},del,"Linear",true,del);
t2.onComplete.add(function (){
t2.stop();
});
var t3 = game.add.tween(s);
t3.to({alpha:0},del,"Linear",true,del);
t3.onComplete.add(function (){
s.kill
t3.stop();
});
},
updateScore: function (){
$('score-counter').innerHTML = parse($('score-counter').innerHTML);
scoreTween= game.add.tween($('score-counter'));
scoreTween.to({innerHTML:player.score},120,"Linear",true,0);
scoreTween.onUpdateCallback(function(){
$('score-counter').innerHTML = parse($('score-counter').innerHTML).toFixed(0);
},this);
//animateCSS('score-counter','shineText');
},
showCombo: function (){
if (player.combo<2) return;
$('combo-counter').innerHTML = '<span style="font-size:24px">Combo </span><span class="kiaro" style="font-size:32px">x'+player.combo+'</span>';
//animateCSS('combo-counter','shineText');
if($('combo-counter').style.display=='none') $('combo-counter').style.display = 'block';
},
missNote: function(){
const s = game.add.sprite(gameWidth/2,gameHeight/3,"judges","miss");
s.anchor.setTo(.5,.5);
s.scale.setTo(.55,.55);
var del = currentNft/5;
var t2 = game.add.tween(s.scale);
t2.to({x:1.5},del,"Linear",true,del);
var t3 = game.add.tween(s);
t3.to({alpha:0},del,"Linear",true,del);
t3.onComplete.add(function(){
s.kill();
});
player.miss++;
player.combo = 0;
this.damagePlayer(10);
$('combo-counter').style.display = 'none';
if (player.health<=0) this.gameOver();
},
healPlayer: function(amount){
if (player.health == 100) return;
player.health += amount;
this.updateHpBar();
},
damagePlayer: function(amount){
if (player.health == 0) return;
player.health -= amount;
this.updateHpBar();
},
updateHpBar: function(){
this.GameOver = Play.prototype.gameOver;
if(player.health<0) {
player.health = 0;
this.GameOver();
}
if(player.health>100)player.health = 100;
if(player.health<=20&&!this.healing)$('hp-bar').setAttribute('class','meter_low');
else $('hp-bar').setAttribute('class','');
hpTween = game.add.tween($('hp-bar'));
hpTween.timeScale = 0.5;
hpTween.to({value:player.health},120,"Elastic",true,0);
},
gameOver: function(){
if (internal.gameOver) return;
internal.gameOver = true;
player.health = 0;
playSound('GameOver');
playVoice('Trance_GameOver',3,1);
this.finishSong();
},
// key events
hitKey: function(k,x,a) {
k.frame = 1;
k.scale.x = 1;
k.scale.y = 1;
var t1 = game.add.tween(k.scale);
t1.to({x:1.2,y:1.2},50,"Linear",true,50);
t1.yoyo(true);
t1.onComplete.add(function (){
k.frame = 0;
});
const note = game.add.sprite(x,280,'notes','arrow_'+arrowcolor);
note.angle = a;
note.anchor.setTo(.5,.5);
var shine_time = currentNft/3;
var t2 = game.add.tween(note.scale);
t2.to({x:2.5,y:2.5},shine_time,"Linear",true,50);
var t3 = game.add.tween(note);
t3.to({alpha:0},shine_time,"Linear",true,50);
t3.onComplete.add(note.kill);
var comboVoiceVolume = aud.volume/2;
switch (player.combo) {
case 100:
playSound('Trance_Combo_1',false,comboVoiceVolume);
break;
case 200:
playSound('Trance_Combo_2',false,comboVoiceVolume);
break;
case 300:
playSound('Trance_Combo_3',false,comboVoiceVolume);
break;
case 400:
playSound('Trance_Combo_4',false,comboVoiceVolume);
break;
case 500:
playSound('Trance_Combo_5',false,comboVoiceVolume);
break;
case 600:
playSound('Trance_Combo_6',false,comboVoiceVolume);
break;
case 700:
playSound('Trance_Combo_7',false,comboVoiceVolume);
break;
case 800:
playSound('Trance_Combo_8',false,comboVoiceVolume);
break;
case 900:
playSound('Trance_Combo_9',false,comboVoiceVolume);
break;
case 1000:
playSound('Trance_Combo_10',false,comboVoiceVolume);
break;
}
},
// in game -
getFirstStar: function () {
this.createStar(1);
playSound('magic1',false,0.3);
},
getSecondStar: function () {
this.createStar(2);
playSound('magic2',false,0.3);
},
getThirdStar: function () {
this.createStar(3);
playSound('magic3',false,0.3);
},
createStar: function (n) {
const s = game.add.sprite(gameWidth/2,gameHeight/3,"star_128");
s.anchor.setTo(.5,.5);
s.scale.setTo(2,2);
s.alpha = 0;
var del = currentNft/5;
var t1 = game.add.tween(s.scale);
t1.to({x:1.2,y:1.2},del/3,"Linear",true,0);
t1.yoyo(true);
var t2 = game.add.tween(s);
t2.to({alpha:1},del/3.7,"Linear",true,0);
var t3 = game.add.tween(s);
t3.to({alpha:0},del,"Linear",true,del);
t3.onComplete.add(function (){
s.kill
t3.stop();
});
const s2 = game.add.sprite({1:(gameWidth/2)-32,2:gameWidth/2,3:(gameWidth/2)+32}[n],40,"star_32");
s2.anchor.setTo(.5,.5);
var fx = game.add.sprite(s2.x,s2.y,'particle_click');
fx.anchor.setTo(.5,.5);
fx.alpha = 1;
fx.anim = fx.animations.add('anim',null,60,false,true);
fx.anim.killOnComplete = true;
fx.anim.play(60*2,false,true);
},
// particles
addParticles: function () {
back_emitter = game.add.emitter(game.world.centerX,-100,600);
back_emitter.makeParticles('particle_a');
back_emitter.maxParticleScale = 2.5;
back_emitter.minParticleScale = 0.5;
back_emitter.setYSpeed(20,100);
back_emitter.setXSpeed(-200,200);
back_emitter.gravity.y = 20;
back_emitter.alpha = 0.1;
back_emitter.width = game.world.width*1.3;
back_emitter.minRotation = 0;
back_emitter.maxRotation = 40;
back_emitter.start(false,3000,20);
click_emitter = game.add.emitter(0,0,100);
click_emitter.makeParticles('particle_b');
click_emitter.gravity = 0;
click_emitter.maxParticleScale = 0.8;
click_emitter.minParticleScale = 0.2;
var v = 30;
click_emitter.setYSpeed(v*-1,v);
click_emitter.setXSpeed(v*-1,v);
game.input.addMoveCallback(function(pointer,x,y){
click_emitter.x = pointer.x;
click_emitter.y = pointer.y;
if(pointer.duration>160)click_emitter.start(true,2000,null,1);
},this);
},
}