For fun, I have been working on this game I called Star Catcher. It's a very basic game where stars are falling and you try to catch as many of them in your bucket before you miss 10. It's a real mess of code that I just threw into one file, basically. The animations seem to be working fairly smooth for me on my computer.
How can I improve the smoothness of the animations on mobile? Something I have noticed is that when I just leave the stars to fall without moving the bucket by touch dragging, it seems to be very smooth. I started with the animations using jQuery animate and using the step function to check for star, bucket collisions. I then compared adding velocity.js, transit.js and gsap's jQuery plugin to see if that would help. I also compared having a full screen canvas that the stars were painted to, bucket as is DOM and then stars and bucket both painted on canvas.
I ended up going with gsap. No canvas and checking for collisions using requestAnimationFrame
, but still running into this issue of moving the bucket and the stars looking choppy only on my phone. Any ideas? Do you think converting the gsap animations from their jQuery plugin to their tweenmax or whatever would help? I've had similar issues with past projects (circlebattle.com). I did have plans to use cordova to convert -> app blah blah blah but as is the games potential for mobile is being held back. I understand that many are going to say you have to develop the app natively for it to really be responsive, but really? The game is very simple. Yes, it is a bit DOM-intensive with each star being its own element, but it seems like my phone should be able to handle this simplistic of game better. On my todo list is networking it so players can pair up and one person drop stars down and the other tries to catch them.
If you have any other ideas to improve the game in general, I am also open.
// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
var height, width;
var levelData = JSON.parse('[{"x":1,"time":941},{"x":8,"time":2399},{"x":20,"time":4096},{"x":35,"time":5759},{"x":51,"time":8153},{"x":69,"time":9817},{"x":78,"time":10399},{"x":61,"time":11367},{"x":46,"time":13029},{"x":27,"time":14585},{"x":15,"time":15545},{"x":41,"time":16423},{"x":62,"time":17735},...]');
var myTimeouts = [];
var inGame = false;
var gamePaused = false;
var starStartY = -50;
var starX, starY, bucketPos;
var imgPreloads = ['img/fireworks.gif', 'img/golden-star-sml.png'];
var engagement = 'click';
var starsFallen;
var starsCaught;
var mySocket;
var userObj;
var needtoshowhs = false;
var bucketTop, bucketBottom;
var mouseCount = 0;
var nocounts = [];
function Timer(fn, countdown) {
var ident, complete = false;
function _time_diff(date1, date2) {
return date2 ? date2 - date1 : new Date().getTime() - date1;
}
function cancel() {
window.clearTimeout(ident);
}
function pause() {
window.clearTimeout(ident);
total_time_run = _time_diff(start_time);
complete = total_time_run >= countdown;
}
function resume() {
ident = complete ? -1 : window.setTimeout(fn, countdown - total_time_run);
}
var start_time = new Date().getTime();
ident = window.setTimeout(fn, countdown);
return { clear: cancel, pause: pause, resume: resume };
}
var moveBucket = function(x) {
bucketPos = x;
// bucketTop.css("left", x + 'px');
// bucketBottom.css("left", x + 'px');
bucketTop.stop()
bucketTop.animate({left: x+ 'px'}, 5);
bucketBottom.stop();
bucketBottom.animate({left: x+ 'px'}, 5);
};
var showHighScores = function() {
mySocket.emit('getHS');
needtoshowhs = true;
};
var animateStar = function(starEl, forceDuration) {
starEl.animate({top: height}, {
easing: "linear",
duration: forceDuration || starEl.data('duration'),
complete: function() {
console.log('complete');
if (inGame && !gamePaused) {
var index = nocounts.indexOf($(this).data('duration'));
if (index == -1) {
starsFallen++;
$('#starsfallen').text(starsFallen);
if (starsFallen == 10) {
endOfGame(function() {
if (starsCaught > userObj.starsCaught) {
// mess, fadespeed, delay, displaylength, cb)
displayMessage('congrats - new personal best!<br>' + starsCaught + ' stars caught before 10 misses!', 'slow', 20, 3000, function() {
showHighScores();
});
mySocket.emit('sendScore', {
username: userObj.username,
handshake: userObj.handshake,
starsCaught: starsCaught
});
} else {
displayMessage('stars caught before 10 misses...' + starsCaught, 'slow', 20, 3000, function() {
showHighScores();
});
}
});
}
} else {
nocounts.splice(index, 1);
}
}
starEl.remove();
}
});
};
var dropStar = function(x) {
if (inGame && !gamePaused) {
var starEl = $('<img src="img/golden-star-sml.png" class="star">');
x = (x) ? width * x / 100 : Math.round(Math.random() * (width-50) );
starEl.css('left', x + 'px' );
starEl.css('top', starStartY + 'px');
$('#gamearea').append(starEl);
var duration = 1600 + Math.round(Math.random() * 2000) - starsCaught*3.5;
if (duration < 1500) {
starEl.addClass('fast-star');
}
starEl.data('duration', duration);
setTimeout(function() {
if (!gamePaused) {
animateStar(starEl);
} else {
starEl.remove();
}
}, 250);
}
}
var gameWinner = function() {
endOfGame(function() {
ga('send', 'winner');
$('body').append('<div id="fireworks"><h1></h1></div>').hide().fadeIn();
setTimeout(function() {
$('#fireworks h1').text('Congratulations');
$('#fireworks h1').animate({fontSize: '1000px'}, 6000, 'easeInCubic', function() {
$('#fireworks h1').text('1000 Points!!!!!');
$('#fireworks h1').css('font-size', '1px');
$('#fireworks h1').animate({fontSize: '2000px'}, 6000, 'easeInCubic', function() {
setTimeout(function() {
$('#fireworks').fadeOut('slow', function() {
startGame();
$('#fireworks').remove();
});
}, 2000);
});
});
}, 2500);
});
};
var pauseGame = function() {
$('.star').each(function() {
var curCoords = $(this).position();
var dur = $(this).data('duration');
nocounts.push($(this).data('duration'));
$(this).stop();
var pauseStar = $('<img src="img/golden-star-50.png" class="star star-paused">');
console.log(curCoords);
pauseStar.css('left', curCoords.left + 'px');
pauseStar.css('top', curCoords.top + 'px');
pauseStar.data('duration', dur);
$('#gamearea').append(pauseStar);
});
for (var i = 0; i < myTimeouts.length; i++) {
console.log('pausing');
myTimeouts[i].pause();
}
$('body').append('<h1 class="message">game paused<br>double ' + engagement + ' to resume</h1>');
gamePaused = !gamePaused;
};
var resumeGame = function() {
$('.message').remove();
console.log('resumeing')
$('.star').each(function() {
var newDuration = (height - $(this).position().top) / height * $(this).data('duration');
animateStar($(this), newDuration);
$(this).removeClass('star-paused');
});
for (var i = 0; i < myTimeouts.length; i++) {
myTimeouts[i].resume();
}
gamePaused = !gamePaused;
};
var endOfGame = function(cb) {
if (inGame) {
inGame = false;
$('#scoreboard').animate({top: '-200px'}, 2000, function() {
for (var i=0; i<myTimeouts.length; i++) {
myTimeouts[i].clear();
}
timesouts = [];
$('#bucket').hide();
$('.star').each(function() {
$(this).remove();
nocounts.push($(this).data('duration'));
});
cb();
});
}
};
var startGame = function() {
ga('send', 'startgame');
mySocket.emit('startgame');
$('#hs').fadeOut();
starsCaught = 0;
starsFallen = 0;
$('#starscaught').text(starsCaught);
$('#starsfallen').text(starsFallen);
$('#bucket').show();
$('.message').remove();
// handle levelData
levelData.forEach(function(star, index) {
myTimeouts.push(new Timer(function() {
dropStar(star.x);
myTimeouts.splice(index, 1);
}, star.time));
});
// got to end of level
myTimeouts.push(new Timer(function() {
endOfGame(function() {
ga('send', 'endlose');
$('body').append('<h1 class="message">Better luck next time.</h1>');
setTimeout(function() {
startGame();
}, 6000);
});
}, 559999)); // ~ 9 minutes
inGame = true;
};
var screenSaverStars = function() {
};
$(function() {
height = $(window).height();
width = $(window).width();
});
$(window).resize(function() {
height = $(window).height();
width = $(window).width();
});
var displayMessage = function(mess, fadespeed, delay, displaylength, cb) {
setTimeout(function() {
$('#directions h1').html(mess);
$('#directions').fadeIn(fadespeed);
if (displaylength) {
setTimeout(function() {
$('#directions').fadeOut();
if (cb) cb();
}, displaylength);
}
}, delay);
};
$(document).ready(function() {
imgPreloads.forEach(function(imgUrl) {
$('body').append('<img src="' + imgUrl + '" class="img-preload">');
});
$(window).bind(
'touchmove',
function(e) {
e.preventDefault();
}
);
var $dragging = null;
if ('ontouchstart' in document.documentElement) {
engagement = 'tap';
starStartY = -200;
$(document.body).on("touchstart", "#bucket", function (e) {
$dragging = $('#bucket');
if (!inGame) {
$('#hs').fadeOut();
$('#directions').fadeOut('slow', function() {
$('#scoreboard').animate({top: '10px'}, 2000, function() {
startGame();
});
});
}
});
$(document.body).on("touchmove", function(e) {
if ($dragging) {
moveBucket(e.originalEvent.touches[0].pageX-50);
}
mouseCount++;
});
$(document.body).on("doubletap", function(e) {
if (inGame) {
if (gamePaused) {
resumeGame();
} else {
pauseGame();
}
}
});
} else {
$(document.body).on("mousedown", "#bucket", function (e) {
$dragging = ($dragging) ? null : true;
if (!inGame) {
$('#hs').fadeOut();
$('#directions').fadeOut('slow', function() {
$('#scoreboard').animate({top: '10px'}, 2000, function() {
startGame();
});
});
}
});
$(document.body).on("mousemove", function(e) {
if ($dragging) {
moveBucket(e.pageX-50);
}
});
$(document.body).on("dblclick", function(e) {
if (inGame) {
if (gamePaused) {
resumeGame();
} else {
pauseGame();
}
}
});
}
$('#hs-mess').text(engagement + ' the bucket to start new game');
var checkForCollisions = function() {
$('.star').each(function() {
starX = $(this).position().left;
starY = $(this).position().top;
// check for bucket catch
// console.log(starX, starY, bucketPos)
if (starY > height - 10 - 86 - 10 && starY < height - 86) {
if (starX > bucketPos - 30 && starX < bucketPos + 100) {
nocounts.push($(this).data('duration'));
$(this).stop();
$(this).remove();
console.log(nocounts);
starsCaught++;
$('#starscaught').text(starsCaught);;
if (starsCaught === 1000) {
gameWinner();
}
}
}
});
requestAnimFrame(checkForCollisions);
};
$(window).load(function() {
requestAnimFrame(checkForCollisions);
// executes when complete page is fully loaded, including all frames, objects and images
$('#gamearea').fadeIn('slow', function() {
if (docCookies.hasItem('userStatus')) {
userObj = JSON.parse(docCookies.getItem('userStatus'));
mySocket.emit('verifyLogin', userObj);
console.log('verify', userObj);
$('#personal_best').text(userObj.starsCaught);
} else {
displayMessage('welcome to Star Catcher', 'slow', 200, 2000, function() {
setTimeout(function() {
$('#username').focus();
}, 1600);
if (engagement === 'click') {
displayMessage('<table><tr><td colspan=2><h1>create a username</h1></td></tr><tr><td><input type="text" id="username"></td><td><button id="createuser" type="submit">submit</button></td></tr><tr><td colspan=2 id="username-response"></td></tr></table>', 'fast', 1500);
} else {
displayMessage('<h1>create a username</h1><input type="text" id="username"><br><button id="createuser" type="submit">submit</button><div id="username-response"></div>', 'fast', 1500);
}
});
}
mySocket.emit('getHS');
});
$('#loading').remove();
});
$(window).on('blur', function() {
if (inGame && !gamePaused) {
console.log('window blur paused')
pauseGame();
}
});
$(document).on('click', '#createuser', function() {
console.log('click');
mySocket.emit('usernameSubmit', {
username: $('#username').val()
});
});
$(document).on('click', '#viewhs', function() {
$('#directions').fadeOut(2000, function() {
setTimeout(function() {
$('#hs').slideDown({
duration: 1800,
easing: 'linear'
});
}, 0);
});
});
$(document).on('keypress', '#username', function(event) {
if (event.which == 13) {
event.preventDefault();
$("#createuser").trigger('click');
}
});
// $(document.body).on("mouseup", function (e) {
// if ($dragging) $dragging = null;
// });
$('img').on('dragstart', function(event) { event.preventDefault(); });
mySocket = io();
mySocket.on('username-feedback', function(data) {
$('#username-response').removeClass('good bad');
$('#username-response').addClass(data.res);
$('#username-response').text('response: ' + data.msg);
if (data.res === 'good') {
$('#createuser').prop("disabled",true);
docCookies.setItem('userStatus', JSON.stringify({
username: $('#username').val(),
starsCaught: '0',
handshake: data.handshake
}), 31536e3, "/");
userObj = {
username: $('#username').val(),
starsCaught: '0',
handshake: data.handshake
};
$('#personal_best').text('0');
setTimeout(function() {
$('#directions').fadeOut('slow', function() {
displayMessage('the goal is to catch as many stars as possible before you miss 10', 'fast', 700, 1800, function() {
displayMessage(engagement + ' the bucket to start', 'slow', 500);
setTimeout(function() {
addBucket();
}, 500);
});
});
}, 1000);
}
});
mySocket.on('login-feedback', function(data) {
if (data.res) {
displayMessage('welcome back to Star Catcher,<br><span id="usr">' + userObj.username + '</span><hr>' + engagement + ' the bucket to start<br><button id="viewhs">...or view leaderboard</button>', 'slow', 200);
setTimeout(addBucket, 200);
} else {
displayMessage('hackz0r');
setTimeout(function() {
docCookies.removeItem('userStatus');
location.reload();
}, 4000);
}
});
mySocket.on('hsSuccess', function(data) {
userObj.starsCaught = data.starsCaught;
userObj.handshake = data.handshake;
docCookies.setItem('userStatus', JSON.stringify({
username: userObj.username,
starsCaught: data.starsCaught,
handshake: data.handshake
}), 31536e3, "/");
mySocket.emit('');
$('#personal_best').text(data.starsCaught);
});
mySocket.on('hs', function(data) {
console.log(JSON.stringify(data.scores));
$('#hs table tbody').empty();
for (var i = 0; i < 10; i++) {
$('#hs table tbody').append('<tr><td></td><td></td><td></td><td></td></tr>');
}
data.scores.forEach(function(score, i) {
debugger;
var theDate = (engagement === 'tap') ? score.dateset.substring(0,5) : score.dateset;
$('#hs table tbody tr').eq(i).html([i+1, score.username, theDate, score.starscaught].map(function(what) {
return "<td>" + what + "</td>";
}));
if (userObj && score.username === userObj.username) {
$('#hs table tbody tr').eq(i).children().addClass('me');
}
});
if (needtoshowhs) {
$('#hs').fadeIn();
needtoshowhs = false;
setTimeout(function() {
moveBucket(10);
$dragging = null;
setTimeout(function() {
$('#bucket').show();
}, 500);
}, 1200);
}
if (engagement === "tap") {
$('#hs td').addClass('mobilehstd');
}
})
});
var addBucket = function() {
var theBucket = $('<div id="bucket"><img src="img/topbucket.png" id="top"><img src="img/bottombucket.png" id="bottom"></div>').hide().fadeIn();
$('body').append(theBucket);
bucketTop = $('#top');
bucketBottom = $('#bottom');
}
duration<0
as I think that could mean many stars are placed but never move. \$\endgroup\$ – N74 Jan 21 '16 at 8:13if (starsFallen == 10) { endOfGame(function() {
is not how we do things here. Your question is really good, and the sooner you add all your code, the sooner I can complete my review! \$\endgroup\$ – Quill Jan 21 '16 at 8:31