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

Appkit: /simple_project.lua — code sample

Code

--[[

Makes use of `Appkit` to provide a basic project implementation:
  - Editor Test Level support
  - Standalone mode level loading
  - Creates and stores a single `World`, `Level`, `Unit` (Camera Unit)
  - Creates a "freecam" player that can be used to fly around the level

This project implementation will be used if `core/appkit/lua/main.lua` is specified as the boot script in the project's settings.ini file.

It provides hooks for extending behavior via `SimpleProject.extension_project`.
]]--

require 'core/appkit/lua/app'
require 'core/appkit/lua/player_util'
local LoadingScreen = require 'core/appkit/lua/loading_screen'

-- Cached Engine Modules
local Application = stingray.Application
local EntityManager = stingray.EntityManager
local Keyboard = stingray.Keyboard
local ResourcePackage = stingray.ResourcePackage
local Quaternion = stingray.Quaternion
local Unit = stingray.Unit
local Vector3 = stingray.Vector3
local World = stingray.World

Appkit.SimpleProject = Appkit.SimpleProject or {}
local SimpleProject = Appkit.SimpleProject

-- SimpleProject.config should be modified by the project-specific lua to override defaults.
SimpleProject.config = SimpleProject.config or {
    standalone_init_level_name = nil, -- Set this to a level resource name.
    camera_unit = "core/appkit/units/camera/camera",
    camera_index = 1,
    shading_environment = nil, -- Will override levels that have env set in editor.
    create_free_cam_player = true,
    free_cam_tracks_listener = true,
    exit_standalone_with_esc_key = true,
    stop_world_sounds_on_level_change = true,
    dont_capture_mouse_at_startup = false,
    viewport = "default",
    loading_screen_materials = nil, -- Enables a loading screen and transitions between the given materials.
    loading_screen_start_package = nil, -- If specified in combination with loading_screen_materials, loading screen will flush load this package at init time.
    loading_screen_end_package = nil, -- If specified in combination with loading_screen_materials, loading screen will load this in the background.
    loading_screen_shading_env = "core/stingray_renderer/environments/midday/midday" -- Controls the shading environment used by default by the loading screen.
}

-- These high level engine objects are created during SimpleProject.init() and
-- can be referenced by the user project to extend behavior.
SimpleProject.world = SimpleProject.world or nil
SimpleProject.level = SimpleProject.level or nil
SimpleProject.level_name = SimpleProject.level_name or nil
SimpleProject.loading_shading_environment = SimpleProject.loading_shading_environment or nil
SimpleProject.loading_start_package_resource = SimpleProject.loading_start_package_resource or nil
SimpleProject.loading_end_package_resource = SimpleProject.loading_end_package_resource or nil
SimpleProject.loading_packages_are_pending = SimpleProject.loading_packages_are_pending or false
SimpleProject.loading_material_index = SimpleProject.loading_material_index or 1
SimpleProject.camera_unit = SimpleProject.camera_unit or nil
SimpleProject.player = SimpleProject.player or nil
SimpleProject.script_component = SimpleProject.script_component or nil

-- SimpleProject.extension_project is a lua object which can extend behavior of
-- the SimpleProject. It should be set before SimpleProject.init is called.
-- The following functions can be implemented to extend behavior:
--     - function ExtensionProjectName.on_level_load_pre_flow()
--     - function ExtensionProjectName.on_level_shutdown_post_flow()
--     - function ExtensionProjectName.on_init_complete()
--     - function ExtensionProjectName.update(dt)
--     - function ExtensionProjectName.render()
--     - function ExtensionProjectName.shutdown()
SimpleProject.extension_project = SimpleProject.extension_project or nil

function SimpleProject.unload_level(level)
    if not level then return end

    local player = SimpleProject.player
    if player then
        Appkit.PlayerUtil.despawn_freecam(player)
    end

    local config = SimpleProject.config
    if stingray.Wwise and (config.stop_world_sounds_on_level_change == nil or config.stop_world_sounds_on_level_change == true) then
        stingray.WwiseWorld.stop_all(stingray.Wwise.wwise_world(SimpleProject.world))
    end

    local level_wrapper = Appkit.get_managed_level_wrapper(level)
    if not level_wrapper then
        print "Error in SimpleProject.unload_level: level is not managed by Appkit."
        return
    end
    level_wrapper:shutdown()

    local extension_project = SimpleProject.extension_project
    if extension_project and extension_project.on_level_shutdown_post_flow then
        extension_project.on_level_shutdown_post_flow()
    end

    Appkit.unmanage_level(level)

    local world = SimpleProject.world
    World.destroy_level(world, level)
    SimpleProject.level = nil
    SimpleProject.level_name = nil
end

-- level_name is optional. It is needed if the level_name is not the
-- same as the resource_name, as is the case for the editor Test Level.
function SimpleProject.load_level(resource_name, level_name)
    if not resource_name then
        print "Error in SimpleProject.load_level: no level resource name."
        return
    end

    local world = SimpleProject.world
    level = World.load_level(world, resource_name)
    if not level then
        print ("Error in SimpleProject.change_level. Failed to load level ", resource_name)
        return
    end

    SimpleProject.level = level
    SimpleProject.level_name = level_name or resource_name

    print ("Resource name: "..resource_name)
    print ("Level name: "..(level_name or "undefined"))

    local config = SimpleProject.config
    Appkit.manage_level(level, SimpleProject.level_name, config.shading_environment == nil)

    if config.shading_environment then
        SimpleProject.shading_environment_override = World.create_shading_environment(world, config.shading_environment)
        Appkit.set_shading_environment(world, SimpleProject.shading_environment_override, config.shading_environment)
    end

    if config.create_free_cam_player then
        SimpleProject.player = Appkit.PlayerUtil.spawn_free_cam_player(
            level,
            SimpleProject.camera_unit,
            config.camera_index,
            Appkit.get_editor_view_position() or Vector3(0, 0, 2),
            Appkit.get_editor_view_rotation() or Quaternion.identity(),
            Appkit.input_mapper,
            config.free_cam_tracks_listener
        )
    end

    local extension_project = SimpleProject.extension_project
    if extension_project and extension_project.on_level_load_pre_flow then
        extension_project.on_level_load_pre_flow()
    end

    -- The typical use case is to trigger the intiial flow Level Loaded node
    -- after lua project initialization is complete.
    Appkit.trigger_level_loaded_flow(level)
end

-- convenience function which unloads current level and loads given level
function SimpleProject.change_level(resource_name)
    if not resource_name then return end

    local level = SimpleProject.level
    SimpleProject.unload_level(level)

    SimpleProject.load_level(resource_name)
end

function SimpleProject.load_startup_level()
    -- Support Test Level and standalone initial level load. The Editor Test Level
    -- is saved as a temporary file named "__level_editor_test". The actual original
    -- level name is stored in lua Application sript data for reference. The
    -- Appkit stores the original name in Appkit.test_level_name and the temp resource
    -- name in Appkit.test_level_resource_name during Appkit.setup_application().
    -- Note that an untitled/unsaved level will have a Appkit.test_level_name of "".
    local standalone_init_level_name = SimpleProject.config.standalone_init_level_name
    local level_resource_name = Appkit.test_level_resource_name or standalone_init_level_name
    if level_resource_name then
        SimpleProject.load_level(level_resource_name, LEVEL_EDITOR_TEST_LEVEL_NAME or Appkit.test_level_name)
    else
        print "SimpleProject.load_startup_level: No level to load."
    end
end

function SimpleProject.init()

    if LEVEL_EDITOR_TEST and not LEVEL_EDITOR_TEST_READY then
        print("Waiting for test level initialization...")
        return
    end

    local config = SimpleProject.config

    -- Set load_level and unload_level overrides. This is needed so that the
    -- Appkit Change Level flow node can properly init and shutdown the SimpleProject
    -- levels.
    Appkit.custom_unload_level_function = SimpleProject.unload_level
    Appkit.custom_load_level_function = SimpleProject.load_level
    Appkit.setup_application()

    local world = Application.new_world()
    SimpleProject.world = world
    local world_wrapper = Appkit.manage_world(world, config.viewport)

    SimpleProject.script_component = EntityManager.script_component(world)

    -- Create a default camera. This can be retrieved and manipulated
    -- by flow or lua. It is also given to the Player in change_level.
    local camera_unit = World.spawn_unit(world, config.camera_unit)
    SimpleProject.camera_unit = camera_unit
    -- We need the default camera to render the world to draw levels or 2d UI.
    world_wrapper:set_camera_enabled(Unit.camera(camera_unit, 1), camera_unit, true)

    if not SimpleProject.init_loading_screen() then
        SimpleProject.load_startup_level()
    end

    local extension_project = SimpleProject.extension_project
    if extension_project and extension_project.on_init_complete then
        extension_project.on_init_complete()
    end

    if stingray.Window and not SimpleProject.config.dont_capture_mouse_at_startup then
        stingray.Window.set_clip_cursor(true)
        stingray.Window.set_mouse_focus(true)
    end
end

-- Attempts to start the loading splash screen. Returns true if there is a loading
-- screen to show, false if not. This may trigger the loading of packages associated
-- with the loading screen.
function SimpleProject.init_loading_screen()
    local config = SimpleProject.config

    -- Do not show loading screen in Editor Test Level mode or if we have no materials
    local materials = config.loading_screen_materials
    local has_materials = materials and #materials > 0
    if Appkit.test_level_resource_name or not has_materials then
        -- But do load the loading screen end package, if specified.
        if config.loading_screen_end_package then
            local package = Application.resource_package(config.loading_screen_end_package)
            ResourcePackage.load(package)
            ResourcePackage.flush(package)
            SimpleProject.loading_end_package_resource = package
        end
        return false
    end

    -- Init shading environment for loading screen portion.
    local world = SimpleProject.world
    local shading_environment_name = config.loading_screen_shading_env
    if Application.can_get("shading_environment", shading_environment_name or "") then
        local shading_environment_override = World.create_shading_environment(world, shading_environment_name)
        Appkit.set_shading_environment(world, shading_environment_override, shading_environment_name)
        SimpleProject.loading_shading_environment = shading_environment_override
    else
        print ("Error. SimpleProject.init_loading_screen. Cannot get shading environment resource: ", shading_environment_name)
        return false
    end

    -- Fully load start package, if configured. This is blocking.
    local package_name = config.loading_screen_start_package
    if package_name then
        local package = Application.resource_package(package_name)
        SimpleProject.loading_start_package_resource = package
        ResourcePackage.load(package)
        ResourcePackage.flush(package)
    end

    local material_name = materials[SimpleProject.loading_material_index]
    if not Application.can_get("material", material_name) then
        print ("Error. SimpleProject.init_loading_screen. Cannot get material: ", material_name)
        return false
    end

    LoadingScreen.init(world, material_name, SimpleProject.on_loading_screen_finished)

    -- Start loading background package, if configured.
    package_name = config.loading_screen_end_package
    if package_name then
        local package = Application.resource_package(package_name)
        SimpleProject.loading_end_package_resource = package
        ResourcePackage.load(package)
        SimpleProject.loading_packages_are_pending = true
    else
        LoadingScreen.allow_fade_out()
    end

    return true
end

function SimpleProject.on_loading_screen_finished()
    LoadingScreen.destroy()

    -- Support multiple images
    local materials = SimpleProject.config.loading_screen_materials
    local material_index = SimpleProject.loading_material_index
    if material_index < #materials then
        material_index = material_index + 1
        SimpleProject.loading_material_index = material_index

        LoadingScreen.init(SimpleProject.world, materials[material_index], SimpleProject.on_loading_screen_finished)
        if not SimpleProject.loading_packages_are_pending then
            LoadingScreen.allow_fade_out()
        end
        return
    end

    -- Done showing loading screens, load startup level
    SimpleProject.load_startup_level()

    -- And cleanup
    local shading_environment = SimpleProject.loading_shading_environment
    if shading_environment then
        World.destroy_shading_environment(SimpleProject.world, shading_environment)
        SimpleProject.loading_shading_environment = nil
    end
    if SimpleProject.destroy_package(SimpleProject.loading_start_package_resource) then
        SimpleProject.loading_start_package_resource = nil
    end
end

function SimpleProject.update_loading_screen(dt)
    if LoadingScreen.update(dt) then
        if Keyboard.pressed(Keyboard.button_id('enter')) then
            SimpleProject.finish_loading_screen()
            SimpleProject.load_startup_level()
        elseif SimpleProject.loading_packages_are_pending then
            local package = SimpleProject.loading_end_package_resource
            if package and ResourcePackage.has_loaded(package) then
                ResourcePackage.flush(package)
                LoadingScreen.allow_fade_out()
                SimpleProject.loading_packages_are_pending = false
            end
        end
    end
end

function SimpleProject.destroy_package(package)
    if not package then return false end
    if ResourcePackage.has_loaded(package) then
        ResourcePackage.unload(package)
    end
    Application.release_resource_package(package)
    return true;
end

function SimpleProject.finish_loading_screen()
    LoadingScreen.destroy()
    if SimpleProject.destroy_package(SimpleProject.loading_start_package_resource) then
        SimpleProject.loading_start_package_resource = nil
    end
    local package = SimpleProject.loading_end_package_resource
    if package then
        if not ResourcePackage.has_loaded(package) then
            ResourcePackage.flush(package)
        end
        SimpleProject.loading_packages_are_pending = false
    end

    local shading_environment = SimpleProject.loading_shading_environment
    if shading_environment then
        World.destroy_shading_environment(SimpleProject.world, shading_environment)
        SimpleProject.loading_shading_environment = nil
    end
end

function SimpleProject.destroy_loading_screen_packages()
    if SimpleProject.destroy_package(SimpleProject.loading_start_package_resource) then
        SimpleProject.loading_start_package_resource = nil
    end
    if SimpleProject.destroy_package(SimpleProject.loading_end_package_resource) then
        SimpleProject.config.loading_end_package_resource = nil
    end
    SimpleProject.loading_packages_are_pending = false
end

function SimpleProject.update(dt)

    if LEVEL_EDITOR_TEST and not LEVEL_EDITOR_TEST_READY then return end

    Appkit.update(dt)

    SimpleProject.update_loading_screen(dt)

    local extension_project = SimpleProject.extension_project
    if extension_project and extension_project.update then
        extension_project.update(dt)
    end

    local script_component = SimpleProject.script_component
    if script_component then
        script_component:broadcast("update", dt)
    end

    if SimpleProject.config.exit_standalone_with_esc_key and Keyboard.pressed(Keyboard.button_id('esc')) and Appkit.is_standalone() then
        Application.quit()
    end
end

function SimpleProject.render(window)

    if LEVEL_EDITOR_TEST and not LEVEL_EDITOR_TEST_READY then return end

    local extension_project = SimpleProject.extension_project
    -- don't call Appkit.render if Project.render() returns a non-nil, non-false value
    if extension_project and extension_project.render and extension_project.render(window) then
        return
    end

    Appkit.render(window)
end

function SimpleProject.shutdown()

    if LEVEL_EDITOR_TEST and not LEVEL_EDITOR_TEST_READY then return end

    local extension_project = SimpleProject.extension_project
    if extension_project and extension_project.shutdown then
        extension_project.shutdown()
    end

    local world = SimpleProject.world
    if world == nil then
        print "Error in SimpleProject.shutdown. No world."
        return
    end

    local level = SimpleProject.level
    if level then
        SimpleProject.unload_level(level)
    end

    Appkit.shutdown() -- Appkit shutdowns managed objects so call it first

    LoadingScreen.destroy()

    Application.release_world(world) -- destroying the world destroys its units as well
    SimpleProject.world = nil

    -- Destroy packages after releasing the world, to wait for destruction of
    -- non-level units that may be referencing package content unloaded in the
    -- process.
    SimpleProject.destroy_loading_screen_packages()

    SimpleProject.camera_unit = nil
    SimpleProject.script_component = nil
    -- extra project shutdown post world destruction
    if extension_project and extension_project.post_world_shutdown then
        extension_project.post_world_shutdown()
    end
end

return SimpleProject