' @module BGE
namespace BGE
' Main Game Engine class which runs everything
' The main game loop is as follows:
' 1. Update - For each GameEntity:
' - Process Input from roku remote
' - Process Audio Events (https://developer.roku.com/en-ca/docs/references/brightscript/events/roaudioplayerevent.md)
' - Process ECP input (https://developer.roku.com/en-ca/docs/developer-program/debugging/external-control-api.md)
' - Process URL events (https://developer.roku.com/en-ca/docs/references/brightscript/events/rourlevent.md)
' - Runs the Entity's onUpdate() function
' - Moves Entity based on velocity
' 2. Collisions - For each GameEntity:
' - Run Entity's onPreCollision() function
' - For any collisions, runs Entity's onCollision() function
' - Runs entity's onPostCollision() function
' (At any time, the Entity in question may Delete() itself. Before every entity interaction, the entity is checked to make sure it is still valid
' in case it was deleted in the last interaction)
'
' 3. Draw - For each GameEntity, sorted by zIndex
' - Runs the Entity onDrawBegin() function
' - For each drawable in the Entity, run the draw() function
' - Run the Entity onDrawEnd() function
'
' 4. Draw all debug items in game space (e.g. colliders, screen safe zones, etc).
'
' 5. UI - For the tree of widgets in the UI Container
' - Run onUpdate()
' - Run draw()
'
' 6. Debug UI - Draw all debug windows in the tree of the Debug UI
'
class Game
' ****BEGIN - For Internal Use, Do Not Manually Alter****
private debugging = {
draw_colliders: false,
draw_safe_zones: false,
colliders_color: BGE.RGBAtoRGBA(&hFF, 0, 0, 0.8),
safe_action_zone_color: BGE.RGBAtoRGBA(0, &hFF, 0, 0.2),
safe_title_zone_color: BGE.RGBAtoRGBA(0, 0, &hFF, 0.2),
limit_frame_rate: 0,
show_debug_ui: false,
draw_debugUi_to_screen: false
}
private canvas_is_screen = false
private background_color = BGE.RGBAtoRGBA(0, 0, 0, 0.8)
private running = true
private paused = false
private sortedEntities = []
private buttonHeld = -1
private buttonHeldTime = 0
private inputEntityId as string = invalid
private currentInputEntityId = invalid
private dt = 0
private totalRunTime = 0
private FakeDT = invalid
private dtTimer = CreateObject("roTimespan")
private pauseTimer = CreateObject("roTimespan")
private buttonHeldTimer = CreateObject("roTimespan")
private garbageCollectionTimer = CreateObject("roTimespan")
private currentID = 0
private shouldUseIntegerMovement = false
private enableAudioGuideSuppression = true
private emptyBitmap = CreateObject("roBitmap", {width: 1, height: 1, AlphaEnable: false})
private device = CreateObject("roDeviceInfo")
private urlTransfers = {}
private url_port = CreateObject("roMessagePort")
private ecp_input_port = CreateObject("roMessagePort")
private ecp_input = CreateObject("roInput")
private compositor = CreateObject("roCompositor")
private filesystem = CreateObject("roFileSystem")
private screen_port = CreateObject("roMessagePort")
private audioPlayer = CreateObject("roAudioPlayer")
private music_port = CreateObject("roMessagePort")
private fontRegistry = CreateObject("roFontRegistry")
private screen as object = invalid
private canvas as Canvas = invalid
private nextGameEntityId as integer = 0
private secondsBetweenGarbageCollection = 1
private lastGarbageCollection as object = invalid
private roomChangedThisFrame as boolean = false
private roomChangeDetails as object = {}
' ****END - For Internal Use, Do Not Manually Alter****
' Reference to the current room in play
currentRoom as Room = invalid
' Any special arguments for the current room
currentRoomArgs as object = {}
' All of the GameEntities <entityName> => GameEntity[]
Entities as object = {}
' All static variables for a given object type
Statics as object = {}
' The room definitions by name (the room creation functions)
Rooms as object = {}
' The interface definitions by name
Interfaces as object = {}
' The loaded bitmaps by name
Bitmaps as object = {}
' The loaded sounds by name
Sounds as object = {}
' The loaded fonts by name
Fonts as object = {}
' Container for all UI
private gameUi as BGE.UI.UiContainer
' Container for Debug UI
private debugUi as BGE.Debug.DebugWindow
' Constructor for GameEngine
'
' @param {integer} canvas_width - Width of the canvas the game is drawn to
' @param {integer} canvas_height - Height of the canvas the game is drawn to
' @param {boolean} [canvas_as_screen_if_possible=false] - If true, the game will draw to the roScreen directly if the canvas dimensions are the same as the screen dimensions, this improves performance but makes it so you can't do various canvas manipulations (such as screen shake or taking screenshots)
' @return {void}
function new(canvas_width as integer, canvas_height as integer, canvas_as_screen_if_possible = false as boolean) as void
' ############### Create Initial Object - Begin ###############
' Set up the screen
m.canvas = new Canvas(canvas_width, canvas_height)
m.setUpScreen(canvas_width, canvas_height, canvas_as_screen_if_possible)
' Set up the audioPlayer
m.audioPlayer.SetMessagePort(m.music_port)
' Set up the input port
m.ecp_input.SetMessagePort(m.ecp_input_port)
' Register all fonts in package
m.setUpFonts()
m.lastGarbageCollection = RunGarbageCollector()
' ############### Create Initial Object - End ###############
end function
' Sets up the screen based on canvas dimensions, and screen size
'
' @param {integer} canvas_width
' @param {integer} canvas_height
' @param {boolean} [canvas_as_screen_if_possible=false]
' @return {void}
private function setUpScreen(canvas_width as integer, canvas_height as integer, canvas_as_screen_if_possible = false as boolean) as void
UIResolution = m.device.getUIResolution()
SupportedResolutions = m.device.GetSupportedGraphicsResolutions()
FHD_Supported = false
for i = 0 to SupportedResolutions.Count() - 1
if SupportedResolutions[i].name = "FHD"
FHD_Supported = true
end if
end for
if UIResolution.name = "SD"
m.screen = CreateObject("roScreen", true, 854, 626)
else
if canvas_width <= 854
m.screen = CreateObject("roScreen", true, 854, 480)
else if canvas_width <= 1280 or not FHD_Supported
m.screen = CreateObject("roScreen", true, 1280, 720)
else
m.screen = CreateObject("roScreen", true, 1920, 1080)
end if
end if
m.compositor.SetDrawTo(m.screen, &h00000000)
m.screen.SetMessagePort(m.screen_port)
m.screen.SetAlphaEnable(true)
if canvas_as_screen_if_possible
if m.screen.GetWidth() = m.canvas.bitmap.GetWidth() and m.screen.GetHeight() = m.canvas.bitmap.GetHeight()
m.canvas.bitmap = m.screen
m.canvas_is_screen = true
end if
end if
m.setupUi()
end function
' Sets up the Ui layer
'
' @param {object} canvas
private function setupUi() as void
canvas = m.getCanvas()
m.gameUi = new BGE.UI.UiContainer(m)
m.gameUi.showBackground = false
m.gameUi.width = canvas.getWidth()
m.gameUi.height = canvas.getHeight()
m.gameUi.padding.set(32)
m.debugUi = new BGE.UI.UiContainer(m)
m.debugUi.showBackground = false
m.debugUi.width = canvas.getWidth()
m.debugUi.height = canvas.getHeight()
m.debugUi.padding.set(32)
end function
' Finds fonts and registers them
'
' @return {void}
private function setUpFonts() as void
ttfs_in_package = m.filesystem.FindRecurse("pkg:/fonts/", ".ttf")
otfs_in_package = m.filesystem.FindRecurse("pkg:/fonts/", ".otf")
for each font_path in ttfs_in_package
m.fontRegistry.Register("pkg:/fonts/" + font_path)
end for
for each font_path in otfs_in_package
m.fontRegistry.Register("pkg:/fonts/" + font_path)
end for
' Create the default font
m.fonts["default"] = m.fontRegistry.GetDefaultFont(28, false, false)
m.fonts["debugUi"] = m.fontRegistry.GetDefaultFont(20, false, false)
end function
' Gets the next valid id for a GameEntity
'
' @return {string}
function getNextGameEntityId() as string
id = m.nextGameEntityId.ToStr()
m.nextGameEntityId++
return id
end function
' Starts the game engine.
' Run this function after setting up the game.
'
' @return {void}
public function Play() as void
audio_guide_suppression_roURLTransfer = CreateObject("roURLTransfer")
audio_guide_suppression_roURLTransfer.SetUrl("http://localhost:8060/keydown/Backspace")
audio_guide_suppression_ticker = 0
m.running = true
while m.running
m.roomChangedThisFrame = false
if m.inputEntityId <> invalid and m.getEntityByID(m.inputEntityId) = invalid
m.inputEntityId = invalid
end if
m.currentInputEntityId = m.inputEntityId
m.compositor.draw() ' For some reason this has to be called or the colliders don't remove themselves from the compositor ¯\(°_°)/¯
m.dt = m.dtTimer.TotalMilliseconds() / 1000
m.totalRunTime += m.dt
if m.FakeDT <> invalid
m.dt = m.FakeDT
end if
m.dtTimer.Mark()
url_msg = m.url_port.GetMessage()
universalControlEvents = []
screen_msg = m.screen_port.GetMessage()
ecp_msg = m.ecp_input_port.GetMessage()
while screen_msg <> invalid
if type(screen_msg) = "roUniversalControlEvent" and screen_msg.GetInt() <> 11
universalControlEvents.Push(screen_msg)
if screen_msg.GetInt() < 100
m.buttonHeld = screen_msg.GetInt()
m.buttonHeldTimer.Mark()
else
m.buttonHeld = -1
if m.enableAudioGuideSuppression
if screen_msg.GetInt() = 110
audio_guide_suppression_ticker++
if audio_guide_suppression_ticker = 3
audio_guide_suppression_roURLTransfer.AsyncPostFromString("")
audio_guide_suppression_ticker = 0
end if
else
audio_guide_suppression_ticker = 0
end if
end if
end if
end if
screen_msg = m.screen_port.GetMessage()
end while
m.buttonHeldTime = m.buttonHeldTimer.TotalMilliseconds()
music_msg = m.music_port.GetMessage()
' ----------------------Handle entity interactions (collisions, etc)--------------------
m.processEntitiesPreDraw(universalControlEvents, music_msg, ecp_msg, url_msg)
' ----------------------Clear the screen before drawing entities-------------------------
if m.background_color <> invalid
m.canvas.bitmap.Clear(m.background_color)
end if
' ----------------------Then draw all of the entities and call onDrawBegin() and onDrawEnd()-------------------------
m.drawEntities()
' ---------------------- Draw debug items in game space -------------------------
m.drawDebugItems()
' ---------------------- Handle all UI updates and draws -------------------------
m.processAndDrawUI(universalControlEvents, music_msg, ecp_msg, url_msg)
' -------------------Draw everything to the screen----------------------------
if not m.canvas_is_screen
m.screen.DrawScaledObject(m.canvas.offset_x, m.canvas.offset_y, m.canvas.scale_x, m.canvas.scale_y, m.canvas.bitmap)
end if
' ---------------------- Handle all Debug UI updates and draws -------------------------
m.processAndDrawDebugUI(universalControlEvents, music_msg, ecp_msg, url_msg)
if m.debugging.draw_safe_zones
m.drawSafeZones()
end if
m.screen.SwapBuffers()
if m.debugging.limit_frame_rate > 0 and m.dtTimer.TotalMilliseconds() > 0
while 1000 / m.dtTimer.TotalMilliseconds() > m.debugging.limit_frame_rate
sleep(1)
end while
end if
' ------------------Destroy the UrlTransfer object if it has returned an event------------------
if type(url_msg) = "roUrlEvent"
url_transfer_id_string = url_msg.GetSourceIdentity().ToStr()
if invalid <> url_transfer_id_string and m.urlTransfers.DoesExist(url_transfer_id_string)
m.urlTransfers.Delete(url_transfer_id_string)
end if
end if
if m.garbageCollectionTimer.totalMilliseconds() > (m.secondsBetweenGarbageCollection * 1000)
m.lastGarbageCollection = runGarbageCollector()
m.garbageCollectionTimer.mark()
end if
if m.roomChangedThisFrame
m.handleRoomChange()
end if
end while
end function
' Handles all the processing for the entities before drawing
'
' @param {object} universalControlEvents - array of any control events that happened since last frame
' @param {object} music_msg - audio player event in last frame
' @param {object} ecp_msg - input event in last frame
' @param {object} url_msg - url event in last frame
' @return {void}
private function processEntitiesPreDraw(universalControlEvents as object, music_msg as object, ecp_msg as object, url_msg as object) as void
started_paused = m.paused
' Process all Entity Updates
for i = m.sortedEntities.Count() to 0 step -1
entity = invalid
if i = m.sortedEntities.Count()
' magic to process current room first
entity = m.currentRoom
else
entity = m.sortedEntities[i]
end if
if not m.isValidEntity(entity) or not entity.enabled or (started_paused and entity.pauseable)
' no need to process this entity
else
' --------------------First process the onInput() function--------------------
if m.isValidEntity(entity)
m.processEntityOnInput(entity, universalControlEvents)
end if
' -------------------Then send the audioPlayer event msg if applicable-------------------
if m.isValidEntity(entity) and invalid <> entity.onAudioEvent and "roAudioPlayerEvent" = type(music_msg)
entity.onAudioEvent(music_msg)
end if
' -------------------Then send the ecp input events if applicable-------------------
if m.isValidEntity(entity) and invalid <> entity.onECPInput and "roInputEvent" = type(ecp_msg) and ecp_msg.isInput()
entity.onECPInput(ecp_msg.GetInfo())
end if
' -------------------Then send the urltransfer event msg if applicable-------------------
if m.isValidEntity(entity) and invalid <> entity.onUrlEvent and "roUrlEvent" = type(url_msg)
entity.onUrlEvent(url_msg)
end if
' -------------------Then process the onUpdate() function----------------------
if m.isValidEntity(entity) and invalid <> entity.onUpdate
entity.onUpdate(m.dt)
end if
' -------------------- Then handle the object movement--------------------
if m.isValidEntity(entity)
m.processEntityMovement(entity)
end if
end if
m.deleteIfInvalidEntityByIndex(i)
end for
' Process all Entity Collisions
for i = m.sortedEntities.Count() - 1 to 0 step -1
entity = m.sortedEntities[i]
if not m.isValidEntity(entity) or not entity.enabled or (started_paused and entity.pauseable)
' no need to process this entity
else
' ---------------- Give a space for any processing to happen just before collision checking occurs ------------
if m.isValidEntity(entity) and invalid <> entity.onPreCollision
entity.onPreCollision()
end if
' -------------------Then handle collisions and call onCollision() for each collision---------------------------
if m.isValidEntity(entity) and invalid <> entity.onCollision
m.processEntityOnCollision(entity)
end if
' ---------------- Give a space for any processing to happen just after collision checking occurs ------------
if m.isValidEntity(entity) and invalid <> entity.onPostCollision
entity.onPostCollision()
end if
' --------------Adjust compositor collider at end of loop so collider is accurate for collision checking from other objects-------------
if m.isValidEntity(entity)
m.adjustEntityCompositorObjectPostCollision(entity)
end if
end if
m.deleteIfInvalidEntityByIndex(i)
end for
end function
' Checks if an entity is still valid
'
' @param {GameEntity} entity
' @return {boolean}
private function isValidEntity(entity as GameEntity) as boolean
return BGE.isValidEntity(entity)
end function
' Deletes an entity from the sortedEntities list by the index if that entity is not valid.
' Does not call the onDestroy() method of the entity.
'
' @param {integer} sortedEntitiesIndex
private function deleteIfInvalidEntityByIndex(sortedEntitiesIndex as integer) as void
entity = m.sortedEntities[sortedEntitiesIndex]
if not m.isValidEntity(entity)
m.destroyEntity(entity, false)
m.sortedEntities.Delete(sortedEntitiesIndex)
end if
end function
' Processes input events and calls the entity's onInput function
'
' @param {GameEntity} entity - the entity to relay input to
' @param {object} universalControlEvents - array of control events since last frame
' @return {boolean} true if this entity is still valid
private function processEntityOnInput(entity as GameEntity, universalControlEvents as object) as boolean
if not m.isValidEntity(entity)
return false
end if
for each msg in universalControlEvents
if entity.onInput <> invalid and (m.currentInputEntityId = invalid or m.currentInputEntityId = entity.id)
entity.onInput(new GameInput(msg.GetInt(), 0))
if not m.isValidEntity(entity)
return false
end if
end if
if entity.onECPKeyboard <> invalid and msg.GetChar() <> 0 and msg.GetChar() = msg.GetInt()
entity.onECPKeyboard(Chr(msg.GetChar()).toStr())
if not m.isValidEntity(entity)
return false
end if
end if
end for
if m.buttonHeld <> -1
' Button release codes are 100 plus the button press code
' This shows a button held code as 1000 plus the button press code
if entity.onInput <> invalid and (m.currentInputEntityId = invalid or m.currentInputEntityId = entity.id)
entity.onInput(new GameInput(1000 + m.buttonHeld, m.buttonHeldTime))
if not m.isValidEntity(entity)
return false
end if
end if
end if
return true
end function
' Moves an entity based on its velocity
'
' @param {GameEntity} entity
' @return {boolean} true if this entity is still valid
private function processEntityMovement(entity as GameEntity) as boolean
if not m.isValidEntity(entity)
return false
end if
if m.shouldUseIntegerMovement
entity.x = entity.x + cint(entity.xspeed * 60 * m.dt)
entity.y = entity.y + cint(entity.yspeed * 60 * m.dt)
else
entity.x = entity.x + entity.xspeed * 60 * m.dt
entity.y = entity.y + entity.yspeed * 60 * m.dt
end if
return true
end function
' Checks all colliders in an entity with all other colliders and if
' there is a collision, will call the entity's onCollsion() function
'
' @param {GameEntity} entity
' @return {boolean} true if this entity is still valid
private function processEntityOnCollision(entity as GameEntity) as boolean
if not m.isValidEntity(entity)
return false
end if
for each colliderKey in entity.colliders
collider = entity.colliders[colliderKey]
if collider <> invalid
if collider.enabled
collider.compositorObject.SetMemberFlags(collider.memberFlags)
collider.compositorObject.SetCollidableFlags(collider.collidableFlags)
collider.refreshColliderRegion()
collider.compositorObject.MoveTo(entity.x, entity.y)
multipleCollisions = collider.compositorObject.CheckMultipleCollisions()
if multipleCollisions <> invalid
for each otherCollider in multipleCollisions
otherColliderData = otherCollider.GetData()
if invalid <> otherColliderData and invalid <> otherColliderData.entityId
if otherColliderData.entityId <> entity.id
otherEntity = invalid
if m.Entities[otherColliderData.objectName].DoesExist(otherColliderData.entityId)
otherEntity = m.Entities[otherColliderData.objectName][otherColliderData.entityId]
else if invalid <> m.currentRoom and otherColliderData.objectName = m.currentRoom.name
' Or is it the current room?
otherEntity = m.currentRoom
end if
if invalid <> otherEntity and otherEntity.id <> entity.id
entity.onCollision(colliderKey, otherColliderData.colliderName, otherEntity)
if not m.isValidEntity(entity)
return false
end if
end if
end if
end if
end for
if not m.isValidEntity(entity)
return false
end if
end if
else
collider.compositorObject.SetMemberFlags(0)
collider.compositorObject.SetCollidableFlags(0)
end if
else
if entity.colliders.DoesExist(colliderKey)
entity.colliders.Delete(colliderKey)
end if
end if
end for
return true
end function
' Helper to adjust en entities colliders after movement
'
' @param {GameEntity} entity
' @return {boolean} true if this entity is still valid
private function adjustEntityCompositorObjectPostCollision(entity as GameEntity) as boolean
if not m.isValidEntity(entity)
return false
end if
for each colliderKey in entity.colliders
collider = entity.colliders[colliderKey]
if collider <> invalid
collider.adjustCompositorObject(entity.x, entity.y)
else
if entity.colliders.DoesExist(colliderKey)
entity.colliders.Delete(colliderKey)
end if
end if
end for
return true
end function
' Draws all the entities, by zIndex (low zIndex is drawn before high zIndex)
'
' @return {void}
private function drawEntities() as void
if invalid <> m.currentRoom
m.processEntityDraw(m.currentRoom)
end if
m.sortedEntities.sortBy("zIndex")
for each entity in m.sortedEntities
m.processEntityDraw(entity)
end for
end function
' Draws an entity, bookended by calling onDrawBegin/End functions
'
' @param {GameEntity} entity
' @return {boolean} true if this entity is still valid
private function processEntityDraw(entity as GameEntity) as boolean
if not m.isValidEntity(entity)
return false
end if
if m.isValidEntity(entity) and invalid <> entity.onDrawBegin
entity.onDrawBegin(m.canvas.bitmap)
end if
if m.isValidEntity(entity)
for each image in entity.images
image.Draw(entity.rotation)
end for
if m.isValidEntity(entity) and invalid <> entity.onDrawEnd
entity.onDrawEnd(m.canvas.bitmap)
end if
end if
return m.isValidEntity(entity)
end function
' Ends the Game
'
' @return {void}
function End() as void
m.running = false
end function
' Pauses the game
' Only entities marked as pausable = false will be processed in game loop
' For each entity, the onPause() function will be called
'
' @return {void}
function Pause() as void
if not m.paused
m.paused = true
for i = 0 to m.sortedEntities.Count() - 1
entity = m.sortedEntities[i]
if entity <> invalid and entity.id <> invalid and entity.onPause <> invalid
entity.onPause()
end if
end for
m.pauseTimer.Mark()
end if
end function
' Resumes / unpauses the game
' For each entity, the onResume() function will be called, and any image in the entity will have its onResume() called
'
' @return {void}
function Resume() as dynamic
if m.paused
m.paused = false
paused_time = m.pauseTimer.TotalMilliseconds()
for i = 0 to m.sortedEntities.Count() - 1
entity = m.sortedEntities[i]
if entity <> invalid and entity.id <> invalid
if invalid <> entity.images
for each image in entity.images
if image.DoesExist("onResume") and image.onResume <> invalid
image.onResume(paused_time)
end if
end for
end if
if entity.onResume <> invalid
entity.onResume(paused_time)
end if
end if
end for
return paused_time
end if
return invalid
end function
' Is the game paused?
'
' @return {boolean}
function isPaused() as boolean
return m.paused
end function
' Sets the default background color for the game
' Before any entities are drawn, the screen is cleared to this color
'
' @param {integer} color
' @return {void}
function setBackgroundColor(color as integer) as void
m.background_color = color
end function
' What's the time in seconds since last frame?
'
' @return {float}
function getDeltaTime() as float
return m.dt
end function
' What's the total time in seconds since ths start
'
' @return {float}
function getTotalTime() as float
return m.totalRunTime
end function
' Gets the current Room/Level the game is using
'
' @return {Room}
function getRoom() as Room
return m.currentRoom
end function
' Gets the bitmap the game is currently drawing to
'
' @return {object}
function getCanvas() as object
return m.canvas.bitmap
end function
' Gets the screen object
'
' @return {object}
function getScreen() as object
return m.screen
end function
' Resets the screen
' Note: _Important_ This function is here because of a bug with the Roku.
' If you ever try to use a component that displays something on the screen aside from roScreen,
' such as roKeyboardScreen, roMessageDialog, etc. the screen will flicker after you return to your game
' You should always call this method after using a screen that's outside of roScreen in order to prevent this bug.
'
' @return {void}
function resetScreen() as void
m.setUpScreen(m.screen.GetWidth(), m.screen.GetHeight(), m.canvas_is_screen)
if m.canvas_is_screen
m.canvas.bitmap = m.screen
' This is so all entities that have images that draw to the screen get updated with the new screen.
for each objectKey in m.Entities
for each entityKey in m.Entities[objectKey]
entity = m.Entities[objectKey][entityKey]
if entity <> invalid and entity.id <> invalid and entity.DoesExist("images")
for each image in entity.images
if type(image.drawTo) = "roScreen"
image.drawTo = m.screen
end if
end for
end if
end for
end for
end if
end function
' Gets a 1x1 bitmap image (used for collider compositing)
'
' @return {object} - a 1x1 empty roBitmap
function getEmptyBitmap() as object
return m.emptyBitmap
end function
' --------------------------------Begin Ui Functions----------------------------------------
' Gets the UI Container to add new UI elements (which get drawn on top off Game Entities)
'
' @return {BGE.Ui.UiContainer} - the container all other UI elements can be added to
function getUI() as BGE.Ui.UiContainer
return m.gameUi
end function
' Function to handle all input and updates and draws for UI layer
'
' @param {object} universalControlEvents - array of control events since last frame
' @param {object} music_msg - audio player event in last frame
' @param {object} ecp_msg - input event in last frame
' @param {object} url_msg - url event in last frame
private function processAndDrawUI(universalControlEvents as object, music_msg as object, ecp_msg as object, url_msg as object) as void
m.processUiUpdate(m.gameUi, universalControlEvents, music_msg, ecp_msg, url_msg)
if m.isValidEntity(m.gameUi) and invalid <> m.gameUi.draw
m.gameUi.draw()
end if
end function
' Function to handle all input and updates and draws for UI layer
'
' @param {BGE.Ui.UiWidget} - widget to process
' @param {object} universalControlEvents - array of control events since last frame
' @param {object} music_msg - audio player event in last frame
' @param {object} ecp_msg - input event in last frame
' @param {object} url_msg - url event in last frame
private function processUiUpdate(uiEntity as BGE.Ui.UiWidget, universalControlEvents as object, music_msg as object, ecp_msg as object, url_msg as object) as void
' --------------------First process the onInput() function--------------------
if m.isValidEntity(uiEntity)
m.processEntityOnInput(uiEntity, universalControlEvents)
end if
' -------------------Then send the audioPlayer event msg if applicable-------------------
if m.isValidEntity(uiEntity) and invalid <> uiEntity.onAudioEvent and "roAudioPlayerEvent" = type(music_msg)
uiEntity.onAudioEvent(music_msg)
end if
' -------------------Then send the ecp input events if applicable-------------------
if m.isValidEntity(uiEntity) and invalid <> uiEntity.onECPInput and "roInputEvent" = type(ecp_msg) and ecp_msg.isInput()
uiEntity.onECPInput(ecp_msg.GetInfo())
end if
' -------------------Then send the urltransfer event msg if applicable-------------------
if m.isValidEntity(uiEntity) and invalid <> uiEntity.onUrlEvent and "roUrlEvent" = type(url_msg)
uiEntity.onUrlEvent(url_msg)
end if
' -------------------Then process the onUpdate() function----------------------
if m.isValidEntity(uiEntity) and invalid <> uiEntity.onUpdate
uiEntity.onUpdate(m.dt)
end if
end function
' --------------------------------Begin Debug Functions----------------------------------------
' Gets the main debug window to add other debug widgets to
'
' @return {BGE.Debug.DebugWindow} - the container all other Debug UI elements can be added to
function getDebugUI() as BGE.Debug.DebugWindow
return m.debugUi
end function
' Function to handle all input and updates and draws for Debug UI layer
'
' @param {object} universalControlEvents - array of control events since last frame
' @param {object} music_msg - audio player event in last frame
' @param {object} ecp_msg - input event in last frame
' @param {object} url_msg - url event in last frame
private function processAndDrawDebugUI(universalControlEvents as object, music_msg as object, ecp_msg as object, url_msg as object) as void
m.processUiUpdate(m.debugUi, universalControlEvents, music_msg, ecp_msg, url_msg)
if m.debugging.show_debug_ui and m.isValidEntity(m.debugUi) and invalid <> m.debugUi.draw
if m.debugging.draw_debugUi_to_screen and m.debugUi.drawTo <> m.screen
m.debugUi.setCanvas(m.screen)
else if not m.debugging.draw_debugUi_to_screen and m.debugUi.drawTo <> m.getCanvas()
m.debugUi.setCanvas(m.getCanvas())
end if
m.debugUi.draw()
end if
end function
' Draw Debug Related Items on the game Layer
'
' @return {void}
private function drawDebugItems() as void
if m.debugging.draw_colliders
for i = m.sortedEntities.Count() - 1 to 0 step -1
entity = m.sortedEntities[i]
if m.isValidEntity(entity)
m.drawColliders(entity)
end if
end for
end if
end function
' Set if colliders should be drawn
'
' @param {boolean} enabled
function debugDrawColliders(enabled as boolean) as void
m.debugging.draw_colliders = enabled
end function
' Set if Safe Zone should be drawn
'
' @param {boolean} enabled
function debugDrawSafeZones(enabled as boolean) as void
m.debugging.draw_safe_zones = enabled
end function
' Set if Debug UI/Windows should be drawn
'
' @param {boolean} enabled
' @param {boolean} drawToScreen Draw the debug UI to the screen instead of the canvas
function debugShowUi(enabled as boolean, drawToScreen = false as boolean) as void
m.debugging.show_debug_ui = enabled
m.debugging.draw_debugUi_to_screen = drawToScreen
debugCanvas = m.canvas
if drawToScreen
debugCanvas = m.screen
end if
m.debugUi.width = debugCanvas.getWidth()
m.debugUi.height = debugCanvas.getHeight()
end function
' Sets the colors for the debug items to be drawn
' colors = {colliders: integer, safe_action_zone: integer, safe_title_zone: integer}
'
' @param {object} colors
function debugSetColors(colors as object) as void
if invalid = colors
return
end if
allowedColorNames = ["colliders", "safe_action_zone", "safe_title_zone"]
colorsToAppend = {}
for each colorName in allowedColorNames
if invalid <> colors[colorName]
colorsToAppend[colorName + "_color"] = colors[colorName]
end if
end for
m.debugging.append(colorsToAppend)
end function
' Limit the frame rate to the given number of frames per second
'
' @param {integer} limit_frame_rate
' @return {void}
function debugLimitFrameRate(limit_frame_rate as integer) as void
m.debugging.limit_frame_rate = limit_frame_rate
end function
' Gets the latest stats from automatic garbage collection
' https://developer.roku.com/en-ca/docs/references/brightscript/language/global-utility-functions.md#rungarbagecollector-as-object
'
' @return {object} Stats of garbage collection. Properties: count, orphaned, root
function getGarbageCollectionStats() as object
return m.lastGarbageCollection
end function
' Draw the colliders for the given entity
'
' @param {GameEntity} entity
' @param {integer} [color=0]
' @return {void}
private function drawColliders(entity as GameEntity, color = 0 as integer) as void
if not m.isValidEntity(entity) or invalid = entity.colliders
return
end if
if 0 = color
color = m.debugging.colliders_color
end if
for each colliderKey in entity.colliders
collider = entity.colliders[colliderKey]
if collider.enabled
collider.debugDraw(m.canvas.bitmap, entity.x, entity.y, color)
end if
end for
end function
' Draws the safe zones
'
' @return {void}
private function drawSafeZones() as void
screen_width = m.screen.GetWidth()
screen_height = m.screen.GetHeight()
if m.device.GetDisplayAspectRatio() = "4x3"
action_offset = {w: 0.033 * screen_width, h: 0.035 * screen_height}
title_offset = {w: 0.067 * screen_width, h: 0.05 * screen_height}
else
action_offset = {w: 0.035 * screen_width, h: 0.035 * screen_height}
title_offset = {w: 0.1 * screen_width, h: 0.05 * screen_height}
end if
action_safe_zone = {x1: action_offset.w, y1: action_offset.h, x2: screen_width - action_offset.w, y2: screen_height - action_offset.h}
title_safe_zone = {x1: title_offset.w, y1: title_offset.h, x2: screen_width - title_offset.w, y2: screen_height - title_offset.h}
m.screen.DrawRect(action_safe_zone.x1, action_safe_zone.y1, action_safe_zone.x2 - action_safe_zone.x1, action_safe_zone.y2 - action_safe_zone.y1, m.debugging.safe_action_zone_color)
m.screen.DrawRect(title_safe_zone.x1, title_safe_zone.y1, title_safe_zone.x2 - title_safe_zone.x1, title_safe_zone.y2 - title_safe_zone.y1, m.debugging.safe_title_zone_color)
m.screen.DrawText("Action Safe Zone", m.screen.GetWidth() / 2 - m.getFont("debugUI").GetOneLineWidth("Action Safe Zone", 1000) / 2, action_safe_zone.y1 + 10, &hFF0000FF, m.getFont("debugUI"))
m.screen.DrawText("Title Safe Zone", m.screen.GetWidth() / 2 - m.getFont("debugUI").GetOneLineWidth("Title Safe Zone", 1000) / 2, action_safe_zone.y1 + 50, &hFF00FFFF, m.getFont("debugUI"))
end function
' --------------------------------Begin Raycast Functions----------------------------------------
' Performs a raycast from a certain location along a vector to find any colliders on that line
'
' @param {float} sourceX x position of ray start
' @param {float} sourceY y position of ray start
' @param {float} vectorX x value of vector
' @param {float} vectorY y value of vector
' @return {object} {collider: collider, entity: entity} of first collider along the vector, or invalid if no collisions
function raycastVector(sourceX as float, sourceY as float, vectorX as float, vectorY as float) as object
' TODO Do Raycasts!
return invalid
end function
' Performs a raycast from a certain location along a n angle to find any colliders on that line
'
' @param {float} sourceX x position of ray start
' @param {float} sourceY y position of ray start
' @param {float} angle angle of ray
' @return {object} {collider: collider, entity: entity} of first collider along the angle, or invalid if no collisions
function raycastAngle(sourceX as float, sourceY as float, angle as float) as object
' TODO Do Raycasts!
return invalid
end function
' --------------------------------Begin Object Functions----------------------------------------
' Sets up the lookup table of entities for entities with this name
'
' @param {string} entityName
' @return {void}
private function defineEntity(entityName as string) as void
if invalid = m.Entities[entityName]
m.Entities[entityName] = {}
m.Statics[entityName] = {}
end if
end function
' TODO: work on interfaces
'
' @param {string} interfaceName
' @param {callable} interfaceCreationFunction
' @return {void}
function defineInterface(interfaceName as string, interfaceCreationFunction as function) as void
m.Interfaces[interfaceName] = interfaceCreationFunction
end function
' Adds a game entity to be processed by the game engine
' Only entities that have been added will be part of the game
' Calls the entity's onCreate() function with the args provided
'
' @param {GameEntity} entity - the entity to be added
' @param {object} [args={}] - arguments to the entity's onCreate() method
' @return {GameEntity} the entity that was added
function addEntity(entity as GameEntity, args = {} as object) as GameEntity
entity.onCreate(args)
m.defineEntity(entity.name)
m.Entities[entity.name][entity.id] = entity
m.sortedEntities.Push(entity)
return entity
end function
' Gets an entity by its unique id
'
' @param {string} entityId
' @return {GameEntity} the entity with the given id, if found, otherwise invalid
function getEntityByID(entityId as string) as GameEntity
for each entity in m.sortedEntities
if m.isValidEntity(entity) and entityId = entity.id
return entity
end if
end for
return invalid
end function
' Gets the first entity with the given name
'
' @param {string} objectName
' @return {GameEntity} the entity with the given name, if found, otherwise invalid
function getEntityByName(objectName as string) as GameEntity
if invalid <> objectName and m.Entities.DoesExist(objectName)
for each entityKey in m.Entities[objectName]
return m.Entities[objectName][entityKey] ' Obviously only retrieves the first value
end for
end if
return invalid
end function
' Gets all the entities that match the given name
'
' @param {string} objectName
' @return {object} an array with entities with the given name
function getAllEntities(objectName as string) as object
array = []
if invalid <> objectName and m.Entities.DoesExist(objectName)
for each entityKey in m.Entities[objectName]
array.Push(m.Entities[objectName][entityKey])
end for
end if
return []
end function
' TODO: work on interfaces
'
' @param {string} interfaceName
' @return {object}
function getAllEntitiesWithInterface(interfaceName as string) as dynamic
array = []
if invalid <> interfaceName and m.Interfaces.DoesExist(interfaceName)
for each entity in m.sortedEntities
if entity <> invalid and entity.id <> invalid and entity.hasInterface(interfaceName)
array.Push(entity)
end if
end for
end if
return array
end function
' Destroys an entity and all its colliders
' Clears its properties, so images, etc. won't get drawn anymore
'
' @param {GameEntity} entity - the entity to destroy
' @param {boolean} [callOnDestroy=true]
' @return {void}
function destroyEntity(entity as GameEntity, callOnDestroy = true as boolean) as void
if invalid <> entity
entityName = entity.name
entityId = entity.id
if invalid <> entity.clearAllColliders
entity.clearAllColliders()
end if
if invalid <> entity.onDestroy and callOnDestroy
entity.onDestroy()
end if
if invalid <> entity.name and invalid <> entityId and m.Entities[entityName].DoesExist(entityId) ' This redundancy is here because if somebody would try to change rooms within the onDestroy() method the game would break.
m.Entities[entityName].Delete(entityId)
end if
if invalid <> entity.Clear
entity.Clear()
end if
entity.id = invalid
end if
end function
' Destroys all entities with a given name
'
' @param {string} objectName
' @param {boolean} [callOnDestroy=true]
' @return {void}
function destroyAllEntities(objectName as string, callOnDestroy = true as boolean) as void
if invalid <> m.Entities[objectName]
for each entityKey in m.Entities[objectName]
m.destroyEntity(m.Entities[objectName][entityKey], callOnDestroy)
end for
end if
end function
' Gets the number of entities of a given name
'
' @param {string} objectName
' @return {integer}
function entityCount(objectName as string) as integer
count = 0
if invalid <> m.Entities[objectName]
count = m.Entities[objectName].Count()
end if
return count
end function
' --------------------------------Begin Room Functions----------------------------------------
' Defines a room (like a level) in the game
' TODO: work on rooms
'
' @param {Room} room
' @return {void}
function defineRoom(room as Room) as void
roomName = room.name
m.Rooms[roomName] = room
m.Entities[roomName] = {}
m.Statics[roomName] = {}
end function
function isRoomChanging() as boolean
return m.roomChangedThisFrame
end function
' Changes the active room to the one with the given name
' Calls the room's onCreate() method with the args given
' TODO: work on rooms
'
' @param {string} roomName
' @param {object} [args={}]
' @return {boolean} true if room change was successful
function changeRoom(roomName as string, args = {} as object) as boolean
if m.Rooms[roomName] <> invalid
m.roomChangedThisFrame = true
m.roomChangeDetails = {room: m.Rooms[roomName], args: args}
if invalid = m.currentRoom or not m.running
m.handleRoomChange()
end if
return true
else
print "changeRoom() - A room named " + roomName + " hasn't been defined"
return false
end if
end function
private sub handleRoomChange()
if m.roomChangedThisFrame and m.roomChangeDetails <> invalid and m.roomChangeDetails.room <> invalid
room = m.roomChangeDetails.room
args = {}
if m.roomChangeDetails.args <> invalid
args = m.roomChangeDetails.args
end if
for i = 0 to m.sortedEntities.Count() - 1
entity = m.sortedEntities[i]
if m.isValidEntity(entity) and entity.onChangeRoom <> invalid
entity.onChangeRoom(room)
end if
end for
for i = 0 to m.sortedEntities.Count() - 1
entity = m.sortedEntities[i]
if m.isValidEntity(entity) and not entity.persistent and entity.name <> m.currentRoom.name
m.destroyEntity(entity, false)
end if
end for
if m.isValidEntity(m.currentRoom) and m.currentRoom.onChangeRoom <> invalid
m.currentRoom.onChangeRoom(room)
end if
m.currentRoom = room
m.currentRoomArgs = args
m.currentRoom.onCreate(args)
m.roomChangedThisFrame = false
end if
end sub
' Resets the current room to its state at when it was first created
'
' @return {void}
function resetRoom() as void
m.changeRoom(m.currentRoom.name, m.currentRoomArgs)
end function
' --------------------------------Begin Bitmap Functions----------------------------------------
' Loads an image file (png/jpg) to be used as an image in the game
'
' @param {string} bitmapName - the name this bitmap will be referenced by later
' @param {dynamic} path - The path to the bitmap, or an associative array {width: integer, height: integer, alphaEnable:boolean}
' @return {boolean} true if image was loaded
function loadBitmap(bitmapName as string, path as dynamic) as boolean
if type(path) = "roAssociativeArray"
if path.width <> invalid and path.height <> invalid and path.AlphaEnable <> invalid
m.Bitmaps[bitmapName] = CreateObject("roBitmap", path)
return true
else
print "loadBitmap() - Width as Integer, Height as Integer, and AlphaEnabled as Boolean must be provided in order to create an empty bitmap"
return false
end if
else if m.filesystem.Exists(path)
path_object = CreateObject("roPath", path)
parts = path_object.Split()
if parts.extension = ".png" or parts.extension = ".jpg"
m.Bitmaps[bitmapName] = CreateObject("roBitmap", path)
return true
else
print "loadBitmap() - Bitmap " + path + " not loaded, file must be of type .png or .jpg"
return false
end if
else
print "loadBitmap() - Bitmap not created, invalid path or object properties provided"
return false
end if
end function
' Gets a bitmap image (roBitmap) by the name given to it when loadBitmap() was called
'
' @param {string} bitmapName
' @return {object}
function getBitmap(bitmapName as string) as object
return m.Bitmaps[bitmapName]
end function
' Invalidates a bitmap name, so it can't be loaded again
'
' @param {string} bitmapName
' @return {void}
function unloadBitmap(bitmapName as string) as void
m.Bitmaps[bitmapName] = invalid
end function
' --------------------------------Begin Font Functions----------------------------------------
' Registers a font by its path
'
' @param {string} path
' @return {boolean} true if font was registered
function registerFont(path as string) as boolean
if m.filesystem.Exists(path)
path_object = CreateObject("roPath", path)
parts = path_object.Split()
if parts.extension = ".ttf" or parts.extension = ".otf"
m.fontRegistry.register(path)
return true
else
print "Font must be of type .ttf or .otf"
return false
end if
else
print "File at path " ; path ; " doesn't exist"
return false
end if
end function
' Loads a font from the registry, and assigns it the given name
'
' @param {string} fontName - the lookup name to assign to this font
' @param {string} font - the font to load
' @param {integer} size
' @param {boolean} italic
' @param {boolean} bold
' @return {void}
function loadFont(fontName as string, font as string, size as integer, italic as boolean, bold as boolean) as void
m.fonts[fontName] = m.fontRegistry.GetFont(font, size, italic, bold)
end function
' Unloads a font so it can't be used again
'
' @param {string} fontName
' @return {void}
function unloadFont(fontName as string) as void
m.fonts[fontName] = invalid
end function
' Gets a font object, to be used for writing text to the screen
' For example, in BGE.DrawText()
'
' @param {string} fontName
' @return {object}
function getFont(fontName as string) as object
return m.fonts[fontName]
end function
' --------------------------------Begin Canvas Functions----------------------------------------
' Scales and positions the current canvas to fit the screen
'
' @return {void}
function fitCanvasToScreen() as void
canvas_width = m.canvas.bitmap.GetWidth()
canvas_height = m.canvas.bitmap.GetHeight()
screen_width = m.screen.GetWidth()
screen_height = m.screen.GetHeight()
if screen_width / screen_height < canvas_width / canvas_height
m.canvas.scale_x = screen_width / canvas_width
m.canvas.scale_y = m.canvas.scale_x
m.canvas.offset_x = 0
m.canvas.offset_y = (screen_height - (screen_width / (canvas_width / canvas_height))) / 2
else if screen_width / screen_height > canvas_width / canvas_height
m.canvas.scale_x = screen_height / canvas_height
m.canvas.scale_y = m.canvas.scale_x
m.canvas.offset_x = (screen_width - (screen_height * (canvas_width / canvas_height))) / 2
m.canvas.offset_y = 0
else
m.canvas.offset_x = 0
m.canvas.offset_y = 0
scale_difference = screen_width / canvas_width
m.canvas.scale_x = 1 * scale_difference
m.canvas.scale_y = 1 * scale_difference
end if
end function
' Centers the canvas on the screen
'
' @return {void}
function centerCanvasToScreen() as void
m.canvas.offset_x = m.screen.GetWidth() / 2 - (m.canvas.scale_x * m.canvas.bitmap.GetWidth()) / 2
m.canvas.offset_y = m.screen.GetHeight() / 2 - (m.canvas.scale_y * m.canvas.bitmap.GetHeight()) / 2
end function
' --------------------------------Begin Audio Functions----------------------------------------
' Plays an audio file at the given path
' This is designed for music, where only one file can play at a time.
'
' @param {string} path - the path of the music file
' @param {boolean} [loop=false]
' @return {boolean} - true if started
function musicPlay(path as string, loop = false as boolean) as boolean
if m.filesystem.Exists(path)
m.audioPlayer.stop()
m.audioPlayer.ClearContent()
song = {}
song.url = path
m.audioPlayer.AddContent(song)
m.audioPlayer.SetLoop(loop)
m.audioPlayer.play()
return true
else
print "musicPlay() - No file exists at path: " ; path
return false
end if
end function
' Stops the currently playing music file
'
' @return {void}
function musicStop() as void
m.audioPlayer.stop()
end function
' Pauses the currently playing music
'
' @return {void}
function musicPause() as void
m.audioPlayer.pause()
end function
' Resumes / unpauses the current music
'
' @return {void}
function musicResume() as void
m.audioPlayer.resume()
end function
' Loads a sound file from the given path to be played later
'
' @param {string} soundName - the name to assign this sound to, to be referenced later
' @param {string} path - the path to load
' @return {void}
function loadSound(soundName as string, path as string) as void
m.Sounds[soundName] = CreateObject("roAudioResource", path)
end function
' Plays the given sound
'
' @param {string} soundName - the name of the sound to play
' @param {integer} [volume=100] - volume (0-100) to play the sound at
' @return {boolean}
function playSound(soundName as string, volume = 100 as integer) as boolean
if invalid <> soundName and m.Sounds.DoesExist(soundName)
volume = cint(BGE.Math.Clamp(volume, 0, 100))
m.Sounds[soundName].trigger(volume)
return true
else
print "playSound() - No sound has been loaded under the name: " ; soundName
return false
end if
end function
' --------------------------------Begin Url Functions----------------------------------------
' Creates a new URL Async Transfer object, which is handled by the game loop
' Events from this URL transfer will be set to entities via the onUrlEvent() method
'
' @return {object}
function newAsyncUrlTransfer() as object
UrlTransfer = CreateObject("roUrlTransfer")
UrlTransfer.SetMessagePort(m.url_port)
m.urlTransfers[UrlTransfer.GetIdentity().ToStr()] = UrlTransfer
return UrlTransfer
end function
' --------------------------------Begin Input Entity Functions----------------------------------------
' Set only one entity to receive onInput() calls
' Useful for when a menu/pause screen should handle all input
'
' @param {GameEntity} entity
' @return {void}
function setInputEntity(entity as GameEntity) as void
m.inputEntityId = entity.id
end function
' Unset that only one entity will receive onInputCalls()
'
' @return {void}
function unsetInputEntity() as void
m.inputEntityId = invalid
end function
' --------------------------------Begin Game Event Functions----------------------------------------
' General purpose event dispatch method
' Game entities can listen for events via the onGameEvent() method
'
' @param {string} eventName - identifier for the event, eg. "hit", "win", etc.
' @param {object} [data={}] - any data that needs to be be passed with the event
' @return {void}
function postGameEvent(eventName as string, data = {} as object) as void
for i = 0 to m.sortedEntities.Count() - 1
entity = m.sortedEntities[i]
if m.isValidEntity(entity) and entity.onGameEvent <> invalid
entity.onGameEvent(eventName, data)
end if
end for
if invalid <> m.currentRoom
m.currentRoom.onGameEvent(eventName, data)
end if
if invalid <> m.ui
end if
end function
end class
end namespace