Appkit: /app.lua — code sample - Stingray Lua API Reference

Appkit: /app.lua — code sample

Code

--[[

`Appkit` provides management for Editor Test Level and basic application objects,
such as `Window`. It also exposes an interface to manage `World` objects, `Level`
objects, and arbitrary game objects.

The core usage of `Appkit` is to call `Appkit.setup_application` at Init time,
then `Appkit.Update`, `Appkit.Render`, and `Appkit.Shutdown` from the respective
engine Lua hooks.

-------------------------------------------------------------------------------
World Management
-------------------------------------------------------------------------------

    -- Registers a `World` to be managed by Appkit. The `world` will update
    -- each frame, and render each frame with any enabled cameras and its
    -- shading environment. See the Appkit.WorldWrapper lua for information
    -- on using WorldWrapper objects.
    local world_wrapper = Appkit.manage_world(Project.world)

    -- Gets a managed `WorldWrapper` from the Appkit for a given `World`, or
    -- nil if the world is not managed by appkit.
    local world_wrapper = Appkit.get_managed_world(Project.world)

-------------------------------------------------------------------------------
Level Management
-------------------------------------------------------------------------------

    -- Registers a `Level` to be managed by Appkit. The `Level` will update
    -- each frame. See the Appkit.LevelWrapper lua for information
    -- on using LevelWrapper objects.
    local Level_wrapper = Appkit.manage_Level(Project.Level)

    -- Gets a managed `LevelWrapper` from the Appkit for a given `Level`, or
    -- nil if the level is not managed by appkit.
    local level_wrapper = Appkit.get_managed_level(Project.level)

    -- Defines custom level unload and load functions for manaaged levels.
    -- This is used by the Appkit's Level / Change Level flow node and by
    -- Appkit.SimpleProject's change_level function. See the below Config
    -- section for default Appkit managed level load/unload behavior.
    Appkit.custom_unload_level_function = Project.unload_level
    Appkit.custom_load_level_function = Project.load_level

]]--

Appkit = Appkit or {}

-- Plugin modules
if stingray.GwNavWorld then -- Ensure Navigation runtime plugin is loaded in engine
    if pcall(function() require 'gwnav/lua/safe_require' end) then -- Use a pcall to ensure that Navigation editor plugin was activated in editor at data compile
        safe_require 'gwnav/lua/runtime/navflowcallbacks'
    end
end

require 'core/wwise/lua/wwise_flow_callbacks'
require 'core/humanik/lua/humanik_flow_callbacks'
require 'core/animation/lua/runtime/animationrequires'
require 'core/scaleform_studio/lua/scaleform_studio_flow_callbacks'

-- Appkit modules
require 'core/appkit/lua/appkit_flow_callbacks'
require 'core/appkit/lua/scaleform_studio_input'
local ComponentManager = require 'core/appkit/lua/component_manager'
local InputMapper = require 'core/appkit/lua/input_mapper'
local LevelWrapper = require 'core/appkit/lua/level_wrapper'
local Util = require 'core/appkit/lua/util'
local WorldWrapper = require 'core/appkit/lua/world_wrapper'

-- Cached modules
local Window = stingray.Window
local Keyboard = stingray.Keyboard
local Application = stingray.Application
local World = stingray.World
local Level = stingray.Level

--------------------------------------------------------------------------------
-- Config

-- Optional overrides used by the Appkit when changing level triggered via the
-- Appkit.set_pending_level_change function. The default behavior will:
--     a) Check if the level to unload is a managed level and shutdown the
--        LevelWrapper and unmanage the level.
--     b) Destroy the level to unload.
--     c) Create a new level using the same world as the unloaded level.
--     d) Manage the level.
Appkit.custom_unload_level_function = Appkit.custom_unload_level_function or nil
Appkit.custom_load_level_function = Appkit.custom_load_level_function or nil

--------------------------------------------------------------------------------
-- Main Appkit objects

Appkit.was_setup = Appkit.was_setup or false

-- Access to Editor's Test Level name. Will be nil if not currently running an
-- Editor Test Level or Appkit.setup_application() was not called.
Appkit.test_level_name = Appkit.test_level_name or nil
Appkit.test_level_resource_name = Appkit.test_level_resource_name or nil
Appkit.input_mapper = Appkit.input_mapper or nil

Appkit.managed_world = Appkit.managed_world or nil -- todo: multiple worlds
Appkit.managed_levels = Appkit.managed_levels or {}

Appkit.pending_level_change = Appkit.pending_level_change or {current_level = nil, next_level_resource_name = nil}
Appkit.boxed_editor_view_position = Appkit.boxed_editor_view_position or nil
Appkit.boxed_editor_view_rotation = Appkit.boxed_editor_view_rotation or nil

--------------------------------------------------------------------------------
-- External Interface

function Appkit.is_standalone()
    return LEVEL_EDITOR_TEST == nil
end

-- Returns the viewport position from the Editor, if available for example when
-- run as an editor Test Level.
function Appkit.get_editor_view_position()
    if Appkit.boxed_editor_view_position then return Appkit.boxed_editor_view_position:unbox() end
    return nil
end

-- Returns the viewport rotation from the Editor, if available for example when
-- run as an editor Test Level.
function Appkit.get_editor_view_rotation()
    if Appkit.boxed_editor_view_rotation then return Appkit.boxed_editor_view_rotation:unbox() end
    return nil
end

-- Main initialization hook for the appkit. Must be called to use the Appkit. Note
-- this is automatically called by BasicProject, the default fallback project
-- that is used if the user project does not implement the main engine lua hooks.
function Appkit.setup_application()
    -- Window does not exist on some platforms
    if Window and Util.use_touch() then
        Window.set_show_cursor(true)
    end

    Appkit.input_mapper = InputMapper()

    --Support for the Editor Test Level
    if LEVEL_EDITOR_TEST then
        Appkit.test_level_resource_name = "__level_editor_test"
        Appkit.test_level_name = Application.get_data([[LevelEditing]], [[level_resource_name]])
        if not Appkit.test_level_name then
            Appkit.test_level_name = "" -- untitled, unsaved level
        end
        if Application.has_data("LevelEditor", "camera", "position") then
            Appkit.boxed_editor_view_position = stingray.Vector3Box(Application.get_data("LevelEditor", "camera", "position"))
        end
        if Application.has_data("LevelEditor", "camera", "rotation") then
            Appkit.boxed_editor_view_rotation = stingray.QuaternionBox(Application.get_data("LevelEditor", "camera", "rotation"))
        end

        if stingray.Application.can_get("package", Appkit.test_level_resource_name) then
            Application.autoload_resource_package(Appkit.test_level_resource_name)
        end

        if LEVEL_EDITOR_TEST_LEVEL_NAME then
            if stingray.Application.can_get("package", LEVEL_EDITOR_TEST_LEVEL_NAME) then
                Application.autoload_resource_package(Appkit.test_level_resource_name)
            end
        end

    else
        if Window then
            Window.set_focus()
            Window.set_mouse_focus(true)
        end
    end

    if Appkit.plugins then
        for k,v in pairs(Appkit.plugins) do
            if k.init then k:init() end
        end
    end

    Appkit.was_setup = true
end

local function update_pending_level_change()
    if not Appkit.pending_level_change.next_level_resource_name then
        return
    end

    local current_level = Appkit.pending_level_change.current_level
    local world = Level.world(current_level)

    -- unload current level
    if Appkit.custom_unload_level_function then
        Appkit.custom_unload_level_function(current_level)
    else
        local level_wrapper = Appkit.get_managed_level_wrapper(current_level)
        if level_wrapper then
            level_wrapper:shutdown()
            Appkit.unmanage_level(current_level)
        end
        World.destroy_level(world, current_level)
    end
    Appkit.pending_level_change.current_level = nil

    -- load next level
    if Appkit.custom_load_level_function then
        Appkit.custom_load_level_function(Appkit.pending_level_change.next_level_resource_name)
    else
        local next_level = World.load_level(world, Appkit.pending_level_change.next_level_resource_name)
        Appkit.manage_level(next_level, true)
    end
    Appkit.pending_level_change.next_level_resource_name = nil
end

-- Must be called to use the Appkit. Note
-- this is automatically called by BasicProject, the default fallback project
-- that is used if the user project does not implement the main engine lua hooks.
function Appkit.update(dt)

    Appkit.input_mapper:update(dt)

    -- update editor Test Level support
    if LEVEL_EDITOR_TEST then
        local f5_pressed = Keyboard.pressed(Keyboard.button_id('f5'))
        local esc_pressed = Keyboard.pressed(Keyboard.button_id('esc'))
        if f5_pressed or esc_pressed or (Window and (Window.is_closing() or Window.id() == 0)) then
            print("Stopping test level.")
            Application.console_send { type = 'stop_testing' }
            return
        end
    end

    update_pending_level_change()

    ComponentManager.update_managers(dt, ComponentManager.update_pre_world)

    if scaleform then
        -- Update advances all active scenes
        scaleform.Stingray.update()

        if Appkit.Util.use_touch() then
            Appkit.Scaleform.send_touch_input()
        end

        if Appkit.Util.is_pc() then
            Appkit.Scaleform.send_mouse_input()
            Appkit.Scaleform.send_keyboard_input()
        end
    end

    -- Make a local copy of managed levels to avoid issues with modifying
    -- the table during iteration.
    local managed = {}
    for k,v in pairs(Appkit.managed_levels) do managed[k] = v end
    for _, level_wrapper in pairs(managed) do
        level_wrapper:update(dt)
    end

    local managed_world = Appkit.managed_world
    if managed_world then
        managed_world:update(dt)
    end

    ComponentManager.update_managers(dt, ComponentManager.update_post_world)

    if Appkit.plugins then
        for k,v in pairs(Appkit.plugins) do
            if k.update then k:update(dt) end
        end
    end
end

-- Must be called to use the Appkit. Note
-- this is automatically called by BasicProject, the default fallback project
-- that is used if the user project does not implement the main engine lua hooks.
function Appkit.render(window)
    local managed_world = Appkit.managed_world
    if managed_world then
        managed_world:render(window)
    end
end

-- Appkit shutdowns managed objects so call it before destroying things like levels, worlds
-- which were given to Appkit to manage. Must be called to use the Appkit. Note
-- this is automatically called by BasicProject, the default fallback project
-- that is used if the user project does not implement the main engine lua hooks.
function Appkit.shutdown()
    if Appkit.plugins then
        for k,v in pairs(Appkit.plugins) do
            if k.shutdown then k:shutdown() end
        end
    end

    for level, level_wrapper in pairs(Appkit.managed_levels) do
        ComponentManager.notify_managers_level_shutdown(level)
        level_wrapper:shutdown()
    end
    Appkit.managed_levels = {}

    ComponentManager.shutdown_managers()

    local managed_world = Appkit.managed_world
    if managed_world then
        managed_world:shutdown()
        Appkit.managed_world = nil
    end
end

-- Appkit will handle world update, camera update, render, debug Print To Screen functionality.
-- you can pass an optional viewport template parameter used for this world
function Appkit.manage_world(world, viewport)
    if not world then
        print "Error in Appkit.manage_world. world is nil."
        return
    end
    local world_wrapper = WorldWrapper(world, viewport)
    Appkit.managed_world = world_wrapper

    if Appkit.plugins then
        for k,v in pairs(Appkit.plugins) do
            if k.update then k:manage_world(world) end
        end
    end

    return world_wrapper
end

-- Returns an Appkit.WorldWrapper or nil if world is not managed
-- todo multiple worlds
function Appkit.get_managed_world_wrapper(world)
    local managed_world = Appkit.managed_world
    if managed_world.world == world then return managed_world end
    return nil
end

function Appkit.set_shading_environment(world, env, name)
    local managed_world = Appkit.managed_world
    if managed_world and world == managed_world.world then
        managed_world:set_shading_environment(env, name)
    else
        World.set_shading_environment(world, env, name)
    end
end

-- Appkit will handle shading environment init, level background init, and
-- trigger of per-frame Level Update flow node.
-- level_name necessary for lightmap loading. It should match the original level
-- file resource name (not the test level resource name).
function Appkit.manage_level(level, level_name, should_set_shading_env)
    if not level then
        print "Error in Appkit.manage_level. level is nil."
        return
    end
    local level_wrapper = LevelWrapper(level, level_name, (should_set_shading_env == nil) or should_set_shading_env)
    Appkit.managed_levels[level] = level_wrapper
    return level_wrapper
end

function Appkit.unmanage_level(level)
    local managed_level = Appkit.managed_levels[level]
    if managed_level then Appkit.managed_levels[level] = nil end
end

-- Returns an Appkit.LevelWrapper. Will return nil if level is not being managed by Appkit.
function Appkit.get_managed_level_wrapper(level)
    return Appkit.managed_levels[level]
end

function Appkit.manage_level_object(level, type, object)
    local level_wrapper = Appkit.managed_levels[level]
    if level_wrapper then
        level_wrapper:manage_object(type, object)
    else
        print "Error in Appkit.manage_level_object. Given level is not managed by appkit."
    end
end

function Appkit.unmanage_level_object(level, type, object)
    local level_wrapper = Appkit.managed_levels[level]
    if level_wrapper then
        level_wrapper:unmanage_object(type, object)
    else
        print "Error in Appkit.unmanage_level_object. Given level is not managed by appkit."
    end
end

-- Triggers the Level Loaded flow node in the level flow. This is exposed as a
-- separate call to allow the Project to determine when a level is considered
-- "loaded". Note ths is automatically called by the Appkit default BasicProject, when used.
function Appkit.trigger_level_loaded_flow(level)
    local level_wrapper = Appkit.managed_levels[level]
    if level_wrapper then
        level_wrapper:trigger_level_loaded_flow()
    else
        Level.trigger_level_loaded(level)
    end
end

-- Triggers a level change from the given current_level to the next level.
-- This is used by the Appkit's "Change Level" flow node.
function Appkit.set_pending_level_change(current_level, next_level_resource_name)
    if not current_level then
        print "Error in Appkit.set_pending_level_change: passed in current_level is nil."
        return
    end
    Appkit.pending_level_change.current_level = current_level
    Appkit.pending_level_change.next_level_resource_name = next_level_resource_name
end

function Appkit.register_plugin(plugin)
    if Appkit.plugins == nil then
        Appkit.plugins = {}
    end

    if Appkit.plugins[plugin] == nil then
        Appkit.plugins[plugin] = true

        if Appkit.was_setup and plugin.init then
            plugin:init()
        end
    end
end

return Appkit