HaxeFlixel/flixel

View on GitHub
flixel/FlxGame.hx

Summary

Maintainability
Test Coverage
package flixel;

import flixel.graphics.tile.FlxDrawBaseItem;
import flixel.system.FlxSplash;
import flixel.util.FlxArrayUtil;
import flixel.util.FlxDestroyUtil;
import flixel.util.typeLimit.NextState;
import openfl.Assets;
import openfl.Lib;
import openfl.display.Sprite;
import openfl.display.StageAlign;
import openfl.display.StageScaleMode;
import openfl.events.Event;
import openfl.filters.BitmapFilter;
#if desktop
import openfl.events.FocusEvent;
#end
#if FLX_POST_PROCESS
import flixel.effects.postprocess.PostProcess;
import openfl.display.OpenGLView;
#end
#if FLX_DEBUG
import flixel.system.debug.FlxDebugger;
#end
#if FLX_SOUND_TRAY
import flixel.system.ui.FlxSoundTray;
#end
#if FLX_FOCUS_LOST_SCREEN
import flixel.system.ui.FlxFocusLostScreen;
#end
#if FLX_RECORD
import flixel.math.FlxRandom;
import flixel.system.replay.FlxReplay;
#end

/**
 * `FlxGame` is the heart of all Flixel games, and contains a bunch of basic game loops and things.
 * It is a long and sloppy file that you shouldn't have to worry about too much!
 * It is basically only used to create your game object in the first place,
 * after that `FlxG` and `FlxState` have all the useful stuff you actually need.
 */
@:allow(flixel.FlxG)
class FlxGame extends Sprite
{
    /**
     * Framerate to use on focus lost. Default is `10`.
     */
    public var focusLostFramerate:Int = 10;

    #if FLX_RECORD
    /**
     * Flag for whether a replay is currently playing.
     */
    @:allow(flixel.system.frontEnds.VCRFrontEnd)
    public var replaying(default, null):Bool = false;

    /**
     * Flag for whether a new recording is being made.
     */
    @:allow(flixel.system.frontEnds.VCRFrontEnd)
    public var recording(default, null):Bool = false;
    #end

    #if FLX_SOUND_TRAY
    /**
     * The sound tray display container.
     */
    public var soundTray(default, null):FlxSoundTray;
    #end

    #if FLX_DEBUG
    /**
     * The debugger overlay object.
     */
    public var debugger(default, null):FlxDebugger;
    #end

    /**
     * Time in milliseconds that has passed (amount of "ticks" passed) since the game has started.
     */
    public var ticks(default, null):Int = 0;

    /**
     * Enables or disables the filters set via `setFilters()`.
     */
    public var filtersEnabled:Bool = true;

    /**
     * A flag for triggering the `preGameStart` and `postGameStart` "events".
     */
    @:allow(flixel.FlxIntroSplash)
    var _gameJustStarted:Bool = false;

    /**
     * Class type of the initial/first game state for the game, usually `MenuState` or something like that.
     */
    var _initialState:NextState;

    /**
     * Current game state.
     */
    var _state:FlxState;

    /**
     * Total number of milliseconds elapsed since game start.
     */
    var _total:Int = 0;

    /**
     * Time stamp of game startup. Needed on JS where `Lib.getTimer()`
     * returns time stamp of current date, not the time passed since app start.
     */
    var _startTime:Int = 0;

    /**
     * Total number of milliseconds elapsed since last update loop.
     * Counts down as we step through the game loop.
     */
    var _accumulator:Float;

    /**
     * Milliseconds of time since last step.
     */
    var _elapsedMS:Float;

    /**
     * Milliseconds of time per step of the game loop. e.g. 60 fps = 16ms.
     */
    var _stepMS:Float;

    /**
     * Optimization so we don't have to divide step by 1000 to get its value in seconds every frame.
     */
    var _stepSeconds:Float;

    /**
     * Max allowable accumulation (see `_accumulator`).
     * Should always (and automatically) be set to roughly 2x the stage framerate.
     */
    var _maxAccumulation:Float;

    /**
     * Whether the game lost focus.
     */
    var _lostFocus:Bool = false;

    /**
     * The filters array to be applied to the game.
     */
    var _filters:Array<BitmapFilter>;

    #if (desktop && lime_legacy)
    /**
     * Ugly workaround to ensure consistent behaviour between flash and cpp
     * (the focus event should not fire when the game starts up!)
     */
    var _onFocusFiredOnce:Bool = false;
    #end

    #if FLX_FOCUS_LOST_SCREEN
    /**
     * The "focus lost" screen.
     */
    var _focusLostScreen:FlxFocusLostScreen;
    #end

    /**
     * Mouse cursor.
     */
    @:allow(flixel.FlxG)
    @:allow(flixel.system.frontEnds.CameraFrontEnd)
    var _inputContainer:Sprite;

    #if FLX_SOUND_TRAY
    /**
     * Change this after calling `super()` in the `FlxGame` constructor
     * to use a customized sound tray based on `FlxSoundTray`.
     */
    var _customSoundTray:Class<FlxSoundTray> = FlxSoundTray;
    #end

    #if FLX_FOCUS_LOST_SCREEN
    /**
     * Change this after calling `super()` in the `FlxGame` constructor
     * to use a customized screen which will be show when the application lost focus.
     */
    var _customFocusLostScreen:Class<FlxFocusLostScreen> = FlxFocusLostScreen;
    #end

    /**
     * Whether the splash screen should be skipped.
     */
    var _skipSplash:Bool = false;

    #if desktop
    /**
     * Should we start fullscreen or not? This is useful if you want to load fullscreen settings from a
     * `FlxSave` and set it when the game starts, instead of having it hard-set in your `Project.xml`.
     */
    var _startFullscreen:Bool = false;
    #end

    /**
     * If a state change was requested, the new state object is stored here until we switch to it.
     */
    var _nextState:NextState;

    /**
     * A flag for keeping track of whether a game reset was requested or not.
     */
    var _resetGame:Bool = false;

    #if FLX_RECORD
    /**
     * Container for a game replay object.
     */
    @:allow(flixel.system.frontEnds.VCRFrontEnd)
    var _replay:FlxReplay;

    /**
     * Flag for whether a playback of a recording was requested.
     */
    @:allow(flixel.system.frontEnds.VCRFrontEnd)
    var _replayRequested:Bool = false;

    /**
     * Flag for whether a new recording was requested.
     */
    @:allow(flixel.system.frontEnds.VCRFrontEnd)
    var _recordingRequested:Bool = false;
    #end

    #if FLX_POST_PROCESS
    /**
     * `Sprite` for postprocessing effects
     */
    var postProcessLayer:Sprite = new Sprite();

    /**
     * Post process effects active on the `postProcessLayer`.
     */
    var postProcesses:Array<PostProcess> = [];
    #end

    /**
     * Instantiate a new game object.
     *
     * @param gameWidth        The width of your game in pixels. If `0`, the `Project.xml` width is used.
     *                         If the demensions don't match the `Project.xml`, 
     *                         [`scaleMode`](https://api.haxeflixel.com/flixel/system/scaleModes/index.html)
     *                         will determine the actual display size of the game.
     * @param gameHeight       The height of your game in pixels. If `0`, the `Project.xml` height is used.
     *                         If the demensions don't match the `Project.xml`, 
     *                         [`scaleMode`](https://api.haxeflixel.com/flixel/system/scaleModes/index.html)
     *                         will determine the actual display size of the game.
     * @param initialState     A constructor for the initial state, ex: `PlayState.new` or `()->new PlayState()`.
     *                         Note: Also allows `Class<FlxState>` for backwards compatibility.
     * @param updateFramerate  How frequently the game should update. Default is 60 fps.
     * @param drawFramerate    Sets the actual display / draw framerate for the game. Default is 60 fps.
     * @param skipSplash       Whether you want to skip the flixel splash screen with `FLX_NO_DEBUG`.
     * @param startFullscreen  Whether to start the game in fullscreen mode (desktop targets only).
     *
     * @see [scale modes](https://api.haxeflixel.com/flixel/system/scaleModes/index.html)
     */
    public function new(gameWidth = 0, gameHeight = 0, ?initialState:InitialState, updateFramerate = 60, drawFramerate = 60, skipSplash = false,
            startFullscreen = false)
    {
        super();

        #if desktop
        _startFullscreen = startFullscreen;
        #end

        // Super high priority init stuff
        _inputContainer = new Sprite();

        if (gameWidth == 0)
            gameWidth = FlxG.stage.stageWidth;
        if (gameHeight == 0)
            gameHeight = FlxG.stage.stageHeight;

        // Basic display and update setup stuff
        FlxG.init(this, gameWidth, gameHeight);

        FlxG.updateFramerate = updateFramerate;
        FlxG.drawFramerate = drawFramerate;
        _accumulator = _stepMS;
        _skipSplash = skipSplash;

        #if FLX_RECORD
        _replay = new FlxReplay();
        #end

        // Then get ready to create the game object for real
        _initialState = (initialState == null) ? FlxState.new : initialState.toNextState();

        addEventListener(Event.ADDED_TO_STAGE, create);
    }

    /**
     * Sets the filter array to be applied to the game.
     */
    public function setFilters(filters:Array<BitmapFilter>):Void
    {
        _filters = filters;
    }

    /**
     * Used to instantiate the guts of the flixel game object once we have a valid reference to the root.
     */
    function create(_):Void
    {
        if (stage == null)
            return;

        removeEventListener(Event.ADDED_TO_STAGE, create);

        _startTime = getTimer();
        _total = getTicks();

        #if desktop
        FlxG.fullscreen = _startFullscreen;
        #end

        // Set up the view window and double buffering
        stage.scaleMode = StageScaleMode.NO_SCALE;
        stage.align = StageAlign.TOP_LEFT;
        stage.frameRate = FlxG.drawFramerate;

        addChild(_inputContainer);

        #if FLX_POST_PROCESS
        if (OpenGLView.isSupported)
            addChild(postProcessLayer);
        #end

        // Creating the debugger overlay
        #if FLX_DEBUG
        debugger = new FlxDebugger(FlxG.stage.stageWidth, FlxG.stage.stageHeight);
        addChild(debugger);
        #end

        // No need for overlays on mobile.
        #if !mobile
        // Volume display tab
        #if FLX_SOUND_TRAY
        soundTray = Type.createInstance(_customSoundTray, []);
        addChild(soundTray);
        #end

        #if FLX_FOCUS_LOST_SCREEN
        _focusLostScreen = Type.createInstance(_customFocusLostScreen, []);
        addChild(_focusLostScreen);
        #end
        #end

        // Focus gained/lost monitoring
        #if (desktop && openfl <= "4.0.0")
        stage.addEventListener(FocusEvent.FOCUS_OUT, onFocusLost);
        stage.addEventListener(FocusEvent.FOCUS_IN, onFocus);
        #else
        stage.addEventListener(Event.DEACTIVATE, onFocusLost);
        stage.addEventListener(Event.ACTIVATE, onFocus);
        #end

        // Instantiate the initial state
        resetGame();
        switchState();

        if (FlxG.updateFramerate < FlxG.drawFramerate)
            FlxG.log.warn("FlxG.updateFramerate: The update framerate shouldn't be smaller" + " than the draw framerate, since it can slow down your game.");

        // Finally, set up an event for the actual game loop stuff.
        stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);

        // We need to listen for resize event which means new context
        // it means that we need to recreate BitmapDatas of dumped tilesheets
        stage.addEventListener(Event.RESIZE, onResize);

        // make sure the cursor etc are properly scaled from the start
        resizeGame(FlxG.stage.stageWidth, FlxG.stage.stageHeight);

        Assets.addEventListener(Event.CHANGE, FlxG.bitmap.onAssetsReload);
    }

    function onFocus(_):Void
    {
        #if flash
        if (!_lostFocus)
            return; // Don't run this function twice (bug in standalone flash player)
        #end

        #if (desktop && lime_legacy)
        // make sure the on focus event doesn't fire on startup
        if (!_onFocusFiredOnce)
        {
            _onFocusFiredOnce = true;
            return;
        }
        #end

        #if mobile
        // just check if device orientation has been changed
        onResize(_);
        #end

        _lostFocus = false;
        FlxG.signals.focusGained.dispatch();
        _state.onFocus();

        if (!FlxG.autoPause)
            return;

        #if FLX_FOCUS_LOST_SCREEN
        if (_focusLostScreen != null)
            _focusLostScreen.visible = false;
        #end

        #if FLX_DEBUG
        debugger.stats.onFocus();
        #end

        stage.frameRate = FlxG.drawFramerate;
        #if FLX_SOUND_SYSTEM
        FlxG.sound.onFocus();
        #end
        FlxG.inputs.onFocus();
    }

    function onFocusLost(event:Event):Void
    {
        #if next
        if (event != null && event.target != FlxG.stage)
            return;
        #end

        #if flash
        if (_lostFocus)
            return; // Don't run this function twice (bug in standalone flash player)
        #end

        _lostFocus = true;
        FlxG.signals.focusLost.dispatch();
        _state.onFocusLost();

        if (!FlxG.autoPause)
            return;

        #if FLX_FOCUS_LOST_SCREEN
        if (_focusLostScreen != null)
            _focusLostScreen.visible = true;
        #end

        #if FLX_DEBUG
        debugger.stats.onFocusLost();
        #end

        stage.frameRate = focusLostFramerate;
        #if FLX_SOUND_SYSTEM
        FlxG.sound.onFocusLost();
        #end
        FlxG.inputs.onFocusLost();
    }

    @:allow(flixel.FlxG)
    function onResize(_):Void
    {
        var width:Int = FlxG.stage.stageWidth;
        var height:Int = FlxG.stage.stageHeight;

        #if !flash
        if (FlxG.renderTile)
            FlxG.bitmap.onContext();
        #end

        resizeGame(width, height);
    }

    function resizeGame(width:Int, height:Int):Void
    {
        FlxG.resizeGame(width, height);

        _state.onResize(width, height);

        FlxG.cameras.resize();
        FlxG.signals.gameResized.dispatch(width, height);

        #if FLX_DEBUG
        debugger.onResize(width, height);
        #end

        #if FLX_FOCUS_LOST_SCREEN
        if (_focusLostScreen != null)
            _focusLostScreen.draw();
        #end

        #if FLX_SOUND_TRAY
        if (soundTray != null)
            soundTray.screenCenter();
        #end

        #if FLX_POST_PROCESS
        for (postProcess in postProcesses)
            postProcess.rebuild();
        #end
    }

    /**
     * Handles the `onEnterFrame` call and figures out how many updates and draw calls to do.
     */
    function onEnterFrame(_):Void
    {
        ticks = getTicks();
        _elapsedMS = ticks - _total;
        _total = ticks;

        #if FLX_SOUND_TRAY
        if (soundTray != null && soundTray.active)
            soundTray.update(_elapsedMS);
        #end

        if (!_lostFocus || !FlxG.autoPause)
        {
            if (FlxG.vcr.paused)
            {
                if (FlxG.vcr.stepRequested)
                {
                    FlxG.vcr.stepRequested = false;
                }
                else if (_nextState == null) // don't pause a state switch request
                {
                    #if FLX_DEBUG
                    debugger.update();
                    // If the interactive debug is active, the screen must
                    // be rendered because the user might be doing changes
                    // to game objects (e.g. moving things around).
                    if (debugger.interaction.isActive())
                    {
                        draw();
                    }
                    #end
                    return;
                }
            }

            if (FlxG.fixedTimestep)
            {
                _accumulator += _elapsedMS;
                _accumulator = (_accumulator > _maxAccumulation) ? _maxAccumulation : _accumulator;

                while (_accumulator >= _stepMS)
                {
                    step();
                    _accumulator -= _stepMS;
                }
            }
            else
            {
                step();
            }

            #if FLX_DEBUG
            FlxBasic.visibleCount = 0;
            #end

            draw();

            #if FLX_DEBUG
            debugger.stats.visibleObjects(FlxBasic.visibleCount);
            debugger.update();
            #end
        }
    }

    /**
     * Internal method to create a new instance of `_initialState` and reset the game.
     * This gets called when the game is created, as well as when a new state is requested.
     */
    inline function resetGame():Void
    {
        FlxG.signals.preGameReset.dispatch();

        #if FLX_DEBUG
        _skipSplash = true;
        #end
        
        if (_skipSplash)
        {
            _nextState = _initialState;
            _gameJustStarted = true;
        }
        else
        {
            _nextState = ()->new FlxIntroSplash(_initialState);
            _skipSplash = true; // only play it once
        }

        FlxG.reset();

        FlxG.signals.postGameReset.dispatch();
    }

    /**
     * If there is a state change requested during the update loop,
     * this function handles actual destroying the old state and related processes,
     * and calls creates on the new state and plugs it into the game object.
     */
    function switchState():Void
    {
        // Basic reset stuff
        FlxG.cameras.reset();
        FlxG.inputs.onStateSwitch();
        #if FLX_SOUND_SYSTEM
        FlxG.sound.destroy();
        #end

        FlxG.signals.preStateSwitch.dispatch();

        #if FLX_RECORD
        FlxRandom.updateStateSeed();
        #end

        // Destroy the old state (if there is an old state)
        if (_state != null)
            _state.destroy();

        // we need to clear bitmap cache only after previous state is destroyed, which will reset useCount for FlxGraphic objects
        FlxG.bitmap.clearCache();

        // Finally assign and create the new state
        _state = _nextState.createInstance();
        _state._constructor = _nextState;
        _nextState = null;

        if (_gameJustStarted)
            FlxG.signals.preGameStart.dispatch();

        FlxG.signals.preStateCreate.dispatch(_state);

        _state.create();

        if (_gameJustStarted)
            gameStart();

        #if FLX_DEBUG
        debugger.console.registerObject("state", _state);
        #end

        FlxG.signals.postStateSwitch.dispatch();
    }

    function gameStart():Void
    {
        FlxG.signals.postGameStart.dispatch();
        _gameJustStarted = false;
    }

    /**
     * This is the main game update logic section.
     * The `onEnterFrame()` handler is in charge of calling this
     * the appropriate number of times each frame.
     * This block handles state changes, replays, all that good stuff.
     */
    function step():Void
    {
        // Handle game reset request
        if (_resetGame)
        {
            resetGame();
            _resetGame = false;
        }

        handleReplayRequests();

        #if FLX_DEBUG
        // Finally actually step through the game physics
        FlxBasic.activeCount = 0;
        #end

        update();

        #if FLX_DEBUG
        debugger.stats.activeObjects(FlxBasic.activeCount);
        #end
    }

    function handleReplayRequests():Void
    {
        #if FLX_RECORD
        // Handle replay-related requests
        if (_recordingRequested)
        {
            _recordingRequested = false;
            _replay.create(FlxRandom.getRecordingSeed());
            recording = true;

            #if FLX_DEBUG
            debugger.vcr.recording();
            FlxG.log.notice("Starting new flixel gameplay record.");
            #end
        }
        else if (_replayRequested)
        {
            _replayRequested = false;
            _replay.rewind();
            FlxG.random.initialSeed = _replay.seed;

            #if FLX_DEBUG
            debugger.vcr.playingReplay();
            #end

            replaying = true;
        }
        #end
    }

    /**
     * This function is called by `step()` and updates the actual game state.
     * May be called multiple times per "frame" or draw call.
     */
    function update():Void
    {
        if (!_state.active || !_state.exists)
            return;

        if (_nextState != null)
            switchState();

        #if FLX_DEBUG
        if (FlxG.debugger.visible)
            ticks = getTicks();
        #end

        updateElapsed();

        FlxG.signals.preUpdate.dispatch();

        updateInput();

        #if FLX_POST_PROCESS
        if (postProcesses[0] != null)
            postProcesses[0].update(FlxG.elapsed);
        #end

        #if FLX_SOUND_SYSTEM
        FlxG.sound.update(FlxG.elapsed);
        #end
        FlxG.plugins.update(FlxG.elapsed);

        _state.tryUpdate(FlxG.elapsed);

        FlxG.cameras.update(FlxG.elapsed);
        FlxG.signals.postUpdate.dispatch();

        #if FLX_DEBUG
        debugger.stats.flixelUpdate(getTicks() - ticks);
        #end

        #if FLX_POINTER_INPUT
        var len = FlxG.swipes.length;
        while(len-- > 0)
        {
            final swipe = FlxG.swipes.pop();
            if (swipe != null)
                swipe.destroy();
        }
        #end

        filters = filtersEnabled ? _filters : null;
    }

    function updateElapsed():Void
    {
        if (FlxG.fixedTimestep)
        {
            FlxG.elapsed = FlxG.timeScale * _stepSeconds; // fixed timestep
        }
        else
        {
            FlxG.elapsed = FlxG.timeScale * (_elapsedMS / 1000); // variable timestep

            var max = FlxG.maxElapsed * FlxG.timeScale;
            if (FlxG.elapsed > max)
                FlxG.elapsed = max;
        }
    }

    function updateInput():Void
    {
        #if FLX_RECORD
        if (replaying)
        {
            _replay.playNextFrame();

            if (FlxG.vcr.timeout > 0)
            {
                FlxG.vcr.timeout -= _stepMS;

                if (FlxG.vcr.timeout <= 0)
                {
                    if (FlxG.vcr.replayCallback != null)
                    {
                        FlxG.vcr.replayCallback();
                        FlxG.vcr.replayCallback = null;
                    }
                    else
                    {
                        FlxG.vcr.stopReplay();
                    }
                }
            }

            if (replaying && _replay.finished)
            {
                FlxG.vcr.stopReplay();

                if (FlxG.vcr.replayCallback != null)
                {
                    FlxG.vcr.replayCallback();
                    FlxG.vcr.replayCallback = null;
                }
            }

            #if FLX_DEBUG
            debugger.vcr.updateRuntime(_stepMS);
            #end
        }
        else
        {
            FlxG.inputs.update();
        }
        #else
        FlxG.inputs.update();
        #end

        #if FLX_RECORD
        if (recording)
        {
            _replay.recordFrame();

            #if FLX_DEBUG
            debugger.vcr.updateRuntime(_stepMS);
            #end
        }
        #end
    }

    /**
     * Goes through the game state and draws all the game objects and special effects.
     */
    function draw():Void
    {
        if (!_state.visible || !_state.exists)
            return;

        #if FLX_DEBUG
        if (FlxG.debugger.visible)
            ticks = getTicks();
        #end

        FlxG.signals.preDraw.dispatch();

        if (FlxG.renderTile)
            FlxDrawBaseItem.drawCalls = 0;

        #if FLX_POST_PROCESS
        if (postProcesses[0] != null)
            postProcesses[0].capture();
        #end

        FlxG.cameras.lock();

        if (FlxG.plugins.drawOnTop)
        {
            _state.draw();
            FlxG.plugins.draw();
        }
        else
        {
            FlxG.plugins.draw();
            _state.draw();
        }

        if (FlxG.renderTile)
        {
            FlxG.cameras.render();

            #if FLX_DEBUG
            debugger.stats.drawCalls(FlxDrawBaseItem.drawCalls);
            #end
        }

        FlxG.cameras.unlock();

        FlxG.signals.postDraw.dispatch();

        #if FLX_DEBUG
        debugger.stats.flixelDraw(getTicks() - ticks);
        #end
    }

    inline function getTicks()
    {
        return getTimer() - _startTime;
    }

    dynamic function getTimer():Int
    {
        // expensive, only call if necessary
        return Lib.getTimer();
    }
}

private class FlxIntroSplash extends FlxSplash
{
    override function startOutro(onOutroComplete:() -> Void)
    {
        FlxG.game._gameJustStarted = true;
        super.startOutro(onOutroComplete);
    }
}