Category Archives: Game Development

Building a Scalable 2D Game Scene Architecture: From Back to Front

Creating a clean, scalable scene architecture for a 2D game is more than just organizing visualsβ€”it’s about building a system that supports gameplay, UI, effects, and camera logic in a way that’s intuitive and future-proof. In this post, we’ll walk through a layered architecture that separates concerns, supports depth-based gameplay, and keeps your UI crisp and your effects polished.

Whether you’re building a vertical shooter, a platformer, or a retro arcade game, this structure gives you the flexibility to scale without chaos.

🧱 Scene Graph Overview

At the core is root_scene, which contains all visual and logical layers. These layers are organized from background to foreground, with clear roles and transformation rules.

root_scene
β”œβ”€β”€ game_group                            # Camera-controlled gameplay container
β”‚   β”œβ”€β”€ hidden_group                     # Off-screen/inactive entities (object pooling)
β”‚   β”œβ”€β”€ background_group                 # Default background layer + depth container
β”‚   β”‚   β”œβ”€β”€ background_bottom_group      # Farthest background visuals (sky, base)
β”‚   β”‚   β”œβ”€β”€ background_mid_group         # Parallax mid-layers, distant FX
β”‚   β”‚   β”œβ”€β”€ background_top_group         # Closest background visuals
β”‚   β”œβ”€β”€ objects_group                    # Default gameplay layer + depth container
β”‚   β”‚   β”œβ”€β”€ objects_depth_bottom_group   # Farthest gameplay entities
β”‚   β”‚   β”œβ”€β”€ objects_depth_mid_group      # Primary gameplay layer (player, pickups)
β”‚   β”‚   β”œβ”€β”€ objects_depth_top_group      # Foreground gameplay entities
β”‚   β”œβ”€β”€ foreground_group                 # Foreground visuals + depth container
β”‚   β”‚   β”œβ”€β”€ foreground_bottom_group      # Farthest foreground elements
β”‚   β”‚   β”œβ”€β”€ foreground_mid_group         # Mid-range foreground visuals
β”‚   β”‚   β”œβ”€β”€ foreground_top_group         # Closest foreground overlays
β”‚   β”œβ”€β”€ visual_fx_group                  # Explosions, particles, transient visuals
β”‚
β”œβ”€β”€ hud_group                            # Score, gauges, indicators (screen-anchored)
β”œβ”€β”€ menu_group                           # Title screen, credits (non-blocking UI)
β”œβ”€β”€ modal_group                          # Pause, game over, dialogs (blocking overlays)
β”œβ”€β”€ debug_group                          # Dev-only overlays, performance HUD
β”œβ”€β”€ screen_fx_group                      # CRT shader, bloom, vignette (post-processing)
  

🧠 Layer Roles & Camera Behavior

Each layer has a defined purpose and relationship with the camera. Gameplay and visual layers move with the camera, while UI and post-processing layers remain fixed or apply globally. Note that background and foreground groups can also be used in menu layers along with menu group.

LayerPurposeTransforms with Camera
game_groupMaster container for gameplay layersβœ… Yes
hidden_groupObject pooling, inactive/off-screen entitiesβœ… Yes
background_groupDefault background layerβœ… Yes
background_bottom_groupFarthest background visuals (sky, base)βœ… Yes
background_mid_groupParallax mid-layers, distant FXβœ… Yes
background_top_groupClosest background visualsβœ… Yes
objects_groupDefault gameplay layerβœ… Yes
objects_depth_bottom_groupFarthest gameplay entitiesβœ… Yes
objects_depth_mid_groupCore gameplay layer (player, enemies, pickups)βœ… Yes
objects_depth_top_groupForeground gameplay entitiesβœ… Yes
foreground_groupDefault foreground layerβœ… Yes
foreground_bottom_groupFarthest foreground visualsβœ… Yes
foreground_mid_groupMid-range foreground visualsβœ… Yes
foreground_top_groupClosest foreground overlaysβœ… Yes
visual_fx_groupExplosions, particles, screen shakeβœ… Yes
hud_groupScore, gauges, indicators❌ No
menu_groupTitle screen, credits❌ No
modal_groupPause, game over, dialogs❌ No
debug_groupDev overlays, performance HUD❌ No
screen_fx_groupPost-processing shaders (CRT, bloom, vignette)❌ No (global)

🧰 API Naming Conventions

To keep things clean and predictable, each layer has dedicated adders and getters. This ensures encapsulation and avoids direct manipulation of scene graph internals.

πŸ”§ Adders

python

add_to_hidden_group(obj)
add_to_background_group(obj)
add_to_background_bottom_group(obj)
add_to_background_mid_group(obj)
add_to_background_top_group(obj)

add_to_objects_group(obj)
add_to_objects_depth_bottom_group(obj)
add_to_objects_depth_mid_group(obj)
add_to_objects_depth_top_group(obj)

add_to_foreground_group(obj)
add_to_foreground_bottom_group(obj)
add_to_foreground_mid_group(obj)
add_to_foreground_top_group(obj)

add_to_visual_fx_group(obj)
add_to_hud_group(obj)
add_to_menu_group(obj)
add_to_modal_group(obj)
add_to_debug_group(obj)
add_to_screen_fx_group(obj)
  

πŸ” Getters

python

get_hidden_group()
get_background_group()
get_background_bottom_group()
get_background_mid_group()
get_background_top_group()

get_objects_group()
get_objects_depth_bottom_group()
get_objects_depth_mid_group()
get_objects_depth_top_group()

get_foreground_group()
get_foreground_bottom_group()
get_foreground_mid_group()
get_foreground_top_group()

get_visual_fx_group()
get_hud_group()
get_menu_group()
get_modal_group()
get_debug_group()
get_screen_fx_group()
  

πŸ“ Ownership & Layering Rules

To maintain clarity and prevent misuse, each type of entity has a designated home:

  • Gameplay entities β†’ objects_group or one of its depth layers
  • Background visuals β†’ background_group or its depth layers
  • Foreground visuals β†’ foreground_group or its depth layers
  • HUD elements β†’ hud_group
  • Menus β†’ menu_group
  • Blocking overlays β†’ modal_group
  • Debug tools β†’ debug_group only
  • Visual effects β†’ visual_fx_group
  • Post-processing shaders β†’ screen_fx_group
  • Camera transformations β†’ applied only to game_group and its children

🚫 Layering Constraints

To avoid rendering chaos and maintain performance:

  • ❌ No toFront() calls in gameplay layers
  • βœ… UI systems may adjust local order within their own group
  • βœ… Depth layers maintain internal z-ordering

βœ… Benefits of This Architecture

  • Clear separation of concerns: Each layer has a distinct visual and logical role
  • Scalable and maintainable: Easy to audit, extend, and debug
  • Camera-friendly: game_group isolates gameplay transformations from UI
  • Depth flexibility: objects_group, background_group, and foreground_group support layered interactions
  • UI integrity: HUD and modals remain crisp and unaffected by zoom/shake
  • Post-processing polish: screen_fx_group applies final visual effects globally

πŸ§ͺ Final Thoughts

This architecture isn’t just a technical blueprintβ€”it’s a philosophy of clarity. By separating gameplay, background, foreground, UI, and effects into well-defined layers, you empower your team to build faster, debug smarter, and scale confidently.

If you’re working on a game and want help adapting this structure to your engine or genre, I’d love to collaborate. Let’s build something beautiful.

How to save game scores in Corona SDK by writing to a file.

The scope of this tutorial will be to record and store the highest score achieved by a player in a game. This is done by:

  • Creating a file with a score of zero where the file does not exists.
  • When a player dies:
    • The previous score, i.e. value within the file, is assigned to a variable.
    • The player’s new score is assigned to a variable.
  • The two variables are compared.
  • If the previous score is higher nothing is changed.
  • If the previous score is lower the new score will be written to the file in its place.

Note:

No UI is provided as part of the tutorial as corona sdk often changes the manner in which objects are displayed, meaning the UI could break with future corona sdk versions. We will be working from the simulator output window alone.

For security reasons, you are not allowed to write files in the system.ResourceDirectory (the directory where the application is stored). You must specify either system.DocumentsDirectory, system.TemporaryDirectory, or system.CachesDirectory in the system.pathForFile() function when opening the file for writing. Read move about this here.

Below is a table describing when and where each directory should be used.

systemDirectoriesTo use this tutorial create a folder containing a main.lua file.
Paste the code below into the file and save.
Open the file with Corona SDK and the score.txt file will be created and populated with a score of zero.

Play around with the Player Score variable:
newScore = 99

Enter a higher score and it will overwrite what currently exists in the file.

Enter a lower score and nothing will be changed.

-- main.lua

local path = system.pathForFile( "score.txt", system.DocumentsDirectory )

deleteFile = function()
 local result, reason = os.remove(path) 
	if result then
		print( "File removed" )
	else
		print( "File does not exist", reason )  --> score.txt: No such file or directory
	end
end

--[[ 
Uncomment below to remove file
--]]

--deleteFile()

-- Player Score

newScore = 99

-- io.open opens a file at path. returns nil if no file found
-- fh short for file handle
-- "r" is the read instruction
local fh, reason = io.open( path, "r" )

if fh then
    -- Read all contents of file into a variable oldScore
	-- This will be the previous score
	-- To read file content as number use "*number"
	-- To read file content as text use "*a"
	-- "\n" new line
    local oldScore = fh:read( "*number" )
    print( "Old contents of " .. path .. "\n" .. oldScore )
	
	if oldScore < newScore then
		-- re-opening the file in "w+" mode will erase all previous data stored in the file
		-- in the comments below is a table listing all the file mode types
		fh = io.open( path, "w+" )
		-- Set the score to the player's new score.
		fh:write( newScore )
		print( "New contents of " .. path .. "\n" .. newScore )
	end	
else
	-- Error logic
    print( "Reason open failed: " .. reason )  -- display failure message in terminal

    -- create file because it doesn't exist yet
    fh = io.open( path, "w" )

    if fh then
        print( "Created file" )
    else
        print( "Create file failed!" )
    end
	
	-- Set the score to zero.
    fh:write( 0 )

end

io.close( fh )

--[[
The various file modes are listed in the following table:

"r"	Read-only mode and is the default mode where an existing file is opened.
"w"	Write enabled mode that overwites existing file or creates a new file.
"a"	Append mode that opens an existing file or a creates a new file for appending.
"r+" Read and write mode for an existing file.
"w+" All existing data is removed if file exists or new file is created with read write permissions.
"a+" Append mode with read mode enabled that opens an existing file or creates a new file.

]]--

See the lua online book’s I/O library section for more information on working with files.