lettuce
Version:
Lettuce JS, Mini Mobile Framework for Romantic with DSL.
676 lines (600 loc) • 19.7 kB
JavaScript
/*
* MelonJS Game Engine
* Copyright (C) 2011 - 2013, Olivier BIOT
* http://www.melonjs.org
*
* Screens objects & State machine
*
*/
(function () {
/**
* A class skeleton for "Screen" Object <br>
* every "screen" object (title screen, credits, ingame, etc...) to be managed <br>
* through the state manager must inherit from this base class.
* @class
* @extends Object
* @memberOf me
* @constructor
* @see me.state
*/
me.ScreenObject = Object.extend(
/** @scope me.ScreenObject.prototype */
{
/** @ignore */
init: function () {},
/**
* Object reset function
* @ignore
*/
reset : function () {
// reset the game manager
me.game.reset();
// call the onReset Function
this.onResetEvent.apply(this, arguments);
},
/**
* destroy function
* @ignore
*/
destroy : function () {
// notify the object
this.onDestroyEvent.apply(this, arguments);
},
/**
* onResetEvent function<br>
* called by the state manager when reseting the object<br>
* this is typically where you will load a level, etc...
* to be extended
* @name onResetEvent
* @memberOf me.ScreenObject
* @function
* @param {} [arguments...] optional arguments passed when switching state
* @see me.state#change
*/
onResetEvent : function () {
// to be extended
},
/**
* onDestroyEvent function<br>
* called by the state manager before switching to another state<br>
* @name onDestroyEvent
* @memberOf me.ScreenObject
* @function
*/
onDestroyEvent : function () {
// to be extended
}
});
// based on the requestAnimationFrame polyfill by Erik Möller
(function () {
var lastTime = 0;
var frameDuration = 1000 / 60;
// get unprefixed rAF and cAF, if present
var requestAnimationFrame = me.agent.prefixed("requestAnimationFrame");
var cancelAnimationFrame = me.agent.prefixed("cancelAnimationFrame") ||
me.agent.prefixed("cancelRequestAnimationFrame");
if (!requestAnimationFrame || !cancelAnimationFrame) {
requestAnimationFrame = function (callback) {
var currTime = window.performance.now();
var timeToCall = Math.max(0, frameDuration - (currTime - lastTime));
var id = window.setTimeout(function () {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
cancelAnimationFrame = function (id) {
window.clearTimeout(id);
};
}
// put back in global namespace
window.requestAnimationFrame = requestAnimationFrame;
window.cancelAnimationFrame = cancelAnimationFrame;
}());
/**
* a State Manager (state machine)<p>
* There is no constructor function for me.state.
* @namespace me.state
* @memberOf me
*/
me.state = (function () {
// hold public stuff in our singleton
var api = {};
/*-------------------------------------------
PRIVATE STUFF
--------------------------------------------*/
// current state
var _state = -1;
// requestAnimeFrame Id
var _animFrameId = -1;
// whether the game state is "paused"
var _isPaused = false;
// list of screenObject
var _screenObject = {};
// fading transition parameters between screen
var _fade = {
color : "",
duration : 0
};
// callback when state switch is done
/** @ignore */
var _onSwitchComplete = null;
// just to keep track of possible extra arguments
var _extraArgs = null;
// store the elapsed time during pause/stop period
var _pauseTime = 0;
/**
* @ignore
*/
function _startRunLoop() {
// ensure nothing is running first and in valid state
if ((_animFrameId === -1) && (_state !== -1)) {
// reset the timer
me.timer.reset();
// start the main loop
_animFrameId = window.requestAnimationFrame(_renderFrame);
}
}
/**
* Resume the game loop after a pause.
* @ignore
*/
function _resumeRunLoop() {
// ensure game is actually paused and in valid state
if (_isPaused && (_state !== -1)) {
// reset the timer
me.timer.reset();
_isPaused = false;
}
}
/**
* Pause the loop for most screen objects.
* @ignore
*/
function _pauseRunLoop() {
// Set the paused boolean to stop updates on (most) entities
_isPaused = true;
}
/**
* this is only called when using requestAnimFrame stuff
* @param {Number} time current timestamp in milliseconds
* @ignore
*/
function _renderFrame(time) {
// update all game objects
me.game.update(time);
// render all game objects
me.game.draw();
// schedule the next frame update
if (_animFrameId !== -1) {
_animFrameId = window.requestAnimationFrame(_renderFrame);
}
}
/**
* stop the SO main loop
* @ignore
*/
function _stopRunLoop() {
// cancel any previous animationRequestFrame
window.cancelAnimationFrame(_animFrameId);
_animFrameId = -1;
}
/**
* start the SO main loop
* @ignore
*/
function _switchState(state) {
// clear previous interval if any
_stopRunLoop();
// call the screen object destroy method
if (_screenObject[_state]) {
// just notify the object
_screenObject[_state].screen.destroy();
}
if (_screenObject[state]) {
// set the global variable
_state = state;
// call the reset function with _extraArgs as arguments
_screenObject[_state].screen.reset.apply(_screenObject[_state].screen, _extraArgs);
// and start the main loop of the
// new requested state
_startRunLoop();
// execute callback if defined
if (_onSwitchComplete) {
_onSwitchComplete();
}
// force repaint
me.game.repaint();
}
}
/*
* PUBLIC STUFF
*/
/**
* default state ID for Loading Screen
* @constant
* @name LOADING
* @memberOf me.state
*/
api.LOADING = 0;
/**
* default state ID for Menu Screen
* @constant
* @name MENU
* @memberOf me.state
*/
api.MENU = 1;
/**
* default state ID for "Ready" Screen
* @constant
* @name READY
* @memberOf me.state
*/
api.READY = 2;
/**
* default state ID for Play Screen
* @constant
* @name PLAY
* @memberOf me.state
*/
api.PLAY = 3;
/**
* default state ID for Game Over Screen
* @constant
* @name GAMEOVER
* @memberOf me.state
*/
api.GAMEOVER = 4;
/**
* default state ID for Game End Screen
* @constant
* @name GAME_END
* @memberOf me.state
*/
api.GAME_END = 5;
/**
* default state ID for High Score Screen
* @constant
* @name SCORE
* @memberOf me.state
*/
api.SCORE = 6;
/**
* default state ID for Credits Screen
* @constant
* @name CREDITS
* @memberOf me.state
*/
api.CREDITS = 7;
/**
* default state ID for Settings Screen
* @constant
* @name SETTINGS
* @memberOf me.state
*/
api.SETTINGS = 8;
/**
* default state ID for user defined constants<br>
* @constant
* @name USER
* @memberOf me.state
* @example
* var STATE_INFO = me.state.USER + 0;
* var STATE_WARN = me.state.USER + 1;
* var STATE_ERROR = me.state.USER + 2;
* var STATE_CUTSCENE = me.state.USER + 3;
*/
api.USER = 100;
/**
* onPause callback
* @callback
* @name onPause
* @memberOf me.state
*/
api.onPause = null;
/**
* onResume callback
* @callback
* @name onResume
* @memberOf me.state
*/
api.onResume = null;
/**
* onStop callback
* @callback
* @name onStop
* @memberOf me.state
*/
api.onStop = null;
/**
* onRestart callback
* @callback
* @name onRestart
* @memberOf me.state
*/
api.onRestart = null;
/**
* @ignore
*/
api.init = function () {
// set the embedded loading screen
api.set(api.LOADING, new me.DefaultLoadingScreen());
};
/**
* Stop the current screen object.
* @name stop
* @memberOf me.state
* @public
* @function
* @param {Boolean} pauseTrack pause current track on screen stop.
*/
api.stop = function (music) {
// only stop when we are not loading stuff
if ((_state !== api.LOADING) && api.isRunning()) {
// stop the main loop
_stopRunLoop();
// current music stop
if (music === true) {
me.audio.pauseTrack();
}
// store time when stopped
_pauseTime = window.performance.now();
// publish the stop notification
me.event.publish(me.event.STATE_STOP);
// any callback defined ?
if (typeof(api.onStop) === "function") {
api.onStop();
}
}
};
/**
* pause the current screen object
* @name pause
* @memberOf me.state
* @public
* @function
* @param {Boolean} pauseTrack pause current track on screen pause
*/
api.pause = function (music) {
// only pause when we are not loading stuff
if ((_state !== api.LOADING) && !api.isPaused()) {
// stop the main loop
_pauseRunLoop();
// current music stop
if (music === true) {
me.audio.pauseTrack();
}
// store time when paused
_pauseTime = window.performance.now();
// publish the pause event
me.event.publish(me.event.STATE_PAUSE);
// any callback defined ?
if (typeof(api.onPause) === "function") {
api.onPause();
}
}
};
/**
* Restart the screen object from a full stop.
* @name restart
* @memberOf me.state
* @public
* @function
* @param {Boolean} resumeTrack resume current track on screen resume
*/
api.restart = function (music) {
if (!api.isRunning()) {
// restart the main loop
_startRunLoop();
// current music stop
if (music === true) {
me.audio.resumeTrack();
}
// calculate the elpased time
_pauseTime = window.performance.now() - _pauseTime;
// force repaint
me.game.repaint();
// publish the restart notification
me.event.publish(me.event.STATE_RESTART, [ _pauseTime ]);
// any callback defined ?
if (typeof(api.onRestart) === "function") {
api.onRestart();
}
}
};
/**
* resume the screen object
* @name resume
* @memberOf me.state
* @public
* @function
* @param {Boolean} resumeTrack resume current track on screen resume
*/
api.resume = function (music) {
if (api.isPaused()) {
// resume the main loop
_resumeRunLoop();
// current music stop
if (music === true) {
me.audio.resumeTrack();
}
// calculate the elpased time
_pauseTime = window.performance.now() - _pauseTime;
// publish the resume event
me.event.publish(me.event.STATE_RESUME, [ _pauseTime ]);
// any callback defined ?
if (typeof(api.onResume) === "function") {
api.onResume();
}
}
};
/**
* return the running state of the state manager
* @name isRunning
* @memberOf me.state
* @public
* @function
* @return {Boolean} true if a "process is running"
*/
api.isRunning = function () {
return _animFrameId !== -1;
};
/**
* Return the pause state of the state manager
* @name isPaused
* @memberOf me.state
* @public
* @function
* @return {Boolean} true if the game is paused
*/
api.isPaused = function () {
return _isPaused;
};
/**
* associate the specified state with a screen object
* @name set
* @memberOf me.state
* @public
* @function
* @param {Number} state State ID (see constants)
* @param {me.ScreenObject} so Instantiated ScreenObject to associate
* with state ID
* @example
* var MenuButton = me.GUI_Object.extend({
* "onClick" : function () {
* // Change to the PLAY state when the button is clicked
* me.state.change(me.state.PLAY);
* return true;
* }
* });
*
* var MenuScreen = me.ScreenObject.extend({
* onResetEvent: function() {
* // Load background image
* me.game.world.addChild(
* new me.ImageLayer("bg", 0, 0, "bg"),
* 0 // z-index
* );
*
* // Add a button
* me.game.world.addChild(
* new MenuButton(350, 200, { "image" : "start" }),
* 1 // z-index
* );
*
* // Play music
* me.audio.playTrack("menu");
* },
*
* "onDestroyEvent" : function () {
* // Stop music
* me.audio.stopTrack();
* }
* });
*
* me.state.set(me.state.MENU, new MenuScreen());
*/
api.set = function (state, so) {
_screenObject[state] = {};
_screenObject[state].screen = so;
_screenObject[state].transition = true;
};
/**
* return a reference to the current screen object<br>
* useful to call a object specific method
* @name current
* @memberOf me.state
* @public
* @function
* @return {me.ScreenObject}
*/
api.current = function () {
return _screenObject[_state].screen;
};
/**
* specify a global transition effect
* @name transition
* @memberOf me.state
* @public
* @function
* @param {String} effect (only "fade" is supported for now)
* @param {me.Color|String} color a CSS color value
* @param {Number} [duration=1000] expressed in milliseconds
*/
api.transition = function (effect, color, duration) {
if (effect === "fade") {
_fade.color = color;
_fade.duration = duration;
}
};
/**
* enable/disable transition for a specific state (by default enabled for all)
* @name setTransition
* @memberOf me.state
* @public
* @function
* @param {Number} state State ID (see constants)
* @param {Boolean} enable
*/
api.setTransition = function (state, enable) {
_screenObject[state].transition = enable;
};
/**
* change the game/app state
* @name change
* @memberOf me.state
* @public
* @function
* @param {Number} state State ID (see constants)
* @param {} [arguments...] extra arguments to be passed to the reset functions
* @example
* // The onResetEvent method on the play screen will receive two args:
* // "level_1" and the number 3
* me.state.change(me.state.PLAY, "level_1", 3);
*/
api.change = function (state) {
// Protect against undefined ScreenObject
if (typeof(_screenObject[state]) === "undefined") {
throw new me.Error("Undefined ScreenObject for state '" + state + "'");
}
_extraArgs = null;
if (arguments.length > 1) {
// store extra arguments if any
_extraArgs = Array.prototype.slice.call(arguments, 1);
}
// if fading effect
if (_fade.duration && _screenObject[state].transition) {
/** @ignore */
_onSwitchComplete = function () {
me.game.viewport.fadeOut(_fade.color, _fade.duration);
};
me.game.viewport.fadeIn(
_fade.color,
_fade.duration,
function () {
_switchState.defer(this, state);
}
);
}
// else just switch without any effects
else {
// wait for the last frame to be
// "finished" before switching
_switchState.defer(this, state);
}
};
/**
* return true if the specified state is the current one
* @name isCurrent
* @memberOf me.state
* @public
* @function
* @param {Number} state State ID (see constants)
*/
api.isCurrent = function (state) {
return _state === state;
};
// return our object
return api;
})();
})();