Begin refactor of scenes

Refactored the Menu Scene "subclass" as well as the main and settings menus
This commit is contained in:
Gordon Pedersen 2023-03-27 13:09:28 +11:00
parent 045a6f4e20
commit 9a82b55505
12 changed files with 320 additions and 236 deletions

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
# class to represent scenes
class SceneInstance
def initialize(_args, opts = {})
@tick_in_background = opts.tick_in_background._? false
end
attr_reader :tick_in_background
# called every tick of the game loop
def tick(args) end
# custom logic to reset this scene
def reset(args) end
end

View file

@ -6,6 +6,7 @@ require 'lib/coalesce.rb'
# then, some basic classes required for lists of assets # then, some basic classes required for lists of assets
require 'app/classes/sprite_instance.rb' require 'app/classes/sprite_instance.rb'
require 'app/classes/sound_instance.rb' require 'app/classes/sound_instance.rb'
require 'app/classes/scene_instance.rb'
# then, asset lists # then, asset lists
require 'sprites/_list.rb' require 'sprites/_list.rb'
@ -23,18 +24,20 @@ require 'app/util/util.rb'
require 'app/util/input.rb' require 'app/util/input.rb'
require 'app/constants.rb' require 'app/constants.rb'
require 'app/menu.rb'
require 'app/scene.rb'
require 'app/game_setting.rb' require 'app/game_setting.rb'
require 'app/text.rb' require 'app/text.rb'
# then, the scenes # then, the scenes
require 'app/scenes/menu.rb'
require 'app/scenes/gameplay.rb' require 'app/scenes/gameplay.rb'
require 'app/scenes/main_menu.rb' require 'app/scenes/main_menu.rb'
require 'app/scenes/paused.rb' require 'app/scenes/paused.rb'
require 'app/scenes/settings.rb' require 'app/scenes/settings.rb'
require 'app/scenes/cube_tube.rb' require 'app/scenes/cube_tube.rb'
require 'app/scenes/_list.rb'
require 'app/util/scene.rb'
# finally, the main tick # finally, the main tick
# NOTE: add all other requires above this # NOTE: add all other requires above this
require 'app/tick.rb' require 'app/tick.rb'

View file

@ -1,113 +0,0 @@
module Menu
class << self
# Updates and renders a list of options that get passed through.
#
# +options+ data structure:
# [
# {
# text: "some string",
# on_select: -> (args) { "do some stuff in this lambda" }
# }
# ]
def tick(args, state_key, options, menu_y: 420)
args.state.send(state_key).current_option_i ||= 0
args.state.send(state_key).hold_delay ||= 0
menu_state = args.state.send(state_key)
labels = []
spacer = mobile? ? 100 : 60
options.each.with_index do |option, i|
text = case option.kind
when :toggle
"#{text(option[:key])}: #{text_for_setting_val(option[:setting_val])}"
else
text(option[:key])
end
label = label(
text,
x: args.grid.w / 2,
y: menu_y + (options.length - i * spacer),
align: ALIGN_CENTER,
size: SIZE_MD
)
label.key = option[:key]
label_size = args.gtk.calcstringbox(label.text, label.size_enum)
labels << label
if menu_state.current_option_i == i
if !mobile? || (mobile? && args.inputs.controller_one.connected)
args.outputs.solids << {
x: label.x - (label_size[0] / 1.4) - 24 + (Math.sin(args.state.tick_count / 8) * 4),
y: label.y - 22,
w: 16,
h: 16,
}.merge(WHITE)
end
end
end
labels.each do |l|
button_border = { w: 340, h: 80, x: l.x - 170, y: l.y - 55 }.merge(WHITE)
if mobile?
args.outputs.borders << button_border
end
if args.inputs.mouse.up && args.inputs.mouse.inside_rect?(button_border)
o = options.find { |o| o[:key] == l[:key] }
Sound.play(args, :menu)
o[:on_select].call(args) if o
end
end
args.outputs.labels << labels
move = nil
if args.inputs.down
move = :down
elsif args.inputs.up
move = :up
else
menu_state.hold_delay = 0
end
if move
menu_state.hold_delay -= 1
if menu_state.hold_delay <= 0
Sound.play(args, :menu)
index = menu_state.current_option_i
if move == :up
index -= 1
else
index += 1
end
if index < 0
index = options.length - 1
elsif index > options.length - 1
index = 0
end
menu_state.current_option_i = index
menu_state.hold_delay = 10
end
end
if Input.pressed?(args, :primary)
Sound.play(args, :select)
options[menu_state.current_option_i][:on_select].call(args)
end
end
def text_for_setting_val(val)
case val
when true
text(:on)
when false
text(:off)
else
val
end
end
end
end

11
app/scenes/_list.rb Normal file
View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Scene
SCENES = {
DEFAULT: MainMenu,
main_menu: MainMenu,
settings: SettingsMenu,
#paused: PauseMenu,
#cube_tube: CubeTube
}
end

View file

@ -89,6 +89,7 @@ class CubeTubeGame
@current_music = :music1 @current_music = :music1
Music.play(@args, @current_music) Music.play(@args, @current_music)
Music.set_volume(args, args.state.setting.music ? 0.8 : 0.0)
end end
def render_grid_border x, y, w, h, color def render_grid_border x, y, w, h, color

View file

@ -1,50 +1,64 @@
module Scene # frozen_string_literal: true
class << self
# what's displayed when your game starts
def tick_main_menu(args)
draw_bg(args, DARK_PURPLE)
options = [
{
key: :start,
on_select: -> (args) { Scene.switch(args, :cube_tube, reset: true) }
},
{
key: :settings,
on_select: -> (args) { Scene.push(args, :settings, reset: true) }
},
]
if args.gtk.platform?(:desktop) # This is the first screen in this game, allowing access to everything else
options << { class MainMenu < MenuScene
key: :quit, def initialize(args, opts = {})
on_select: -> (args) { args.gtk.request_quit } # these are the menu options for the main menu
} menu_options = [
end {
key: :start,
on_select: ->(iargs) { Scene.switch(iargs, :cube_tube, reset: true) }
},
{
key: :settings,
on_select: ->(iargs) { Scene.push(iargs, :settings, reset: true) }
}
]
Menu.tick(args, :main_menu, options) if args.gtk.platform?(:desktop)
menu_options << {
labels = [] key: :quit,
labels << label( on_select: ->(iargs) { iargs.gtk.request_quit }
"v#{version}", }
x: 32.from_left, y: 32.from_top,
size: SIZE_XS, align: ALIGN_LEFT)
labels << label(
title.upcase, x: args.grid.w / 2, y: args.grid.top - 100,
size: SIZE_LG, align: ALIGN_CENTER, font: FONT_BOLD_ITALIC)
labels << label(
"#{text(:made_by)} #{dev_title}",
x: args.grid.left + 24, y: 48,
size: SIZE_XS, align: ALIGN_LEFT)
labels << label(
:controls_title,
x: args.grid.right - 24, y: 84,
size: SIZE_SM, align: ALIGN_RIGHT)
labels << label(
args.inputs.controller_one.connected ? :controls_gamepad : :controls_keyboard,
x: args.grid.right - 24, y: 48,
size: SIZE_XS, align: ALIGN_RIGHT)
args.outputs.labels << labels
end end
super args, opts, menu_options
end
# called every tick of the game loop
def tick(args)
draw_bg(args, DARK_PURPLE)
# actual menu logic is handled by the MenuScene super class
super
# additionally draw some labels with information about the game
labels = []
labels << label(
"v#{version}",
x: 32.from_left, y: 32.from_top,
size: SIZE_XS, align: ALIGN_LEFT
)
labels << label(
title.upcase, x: args.grid.w / 2, y: args.grid.top - 100,
size: SIZE_LG, align: ALIGN_CENTER, font: FONT_BOLD_ITALIC
)
labels << label(
"#{text(:made_by)} #{dev_title}",
x: args.grid.left + 24, y: 48,
size: SIZE_XS, align: ALIGN_LEFT
)
labels << label(
:controls_title,
x: args.grid.right - 24, y: 84,
size: SIZE_SM, align: ALIGN_RIGHT
)
labels << label(
args.inputs.controller_one.connected ? :controls_gamepad : :controls_keyboard,
x: args.grid.right - 24, y: 48,
size: SIZE_XS, align: ALIGN_RIGHT
)
args.outputs.labels << labels
end end
end end

129
app/scenes/menu.rb Normal file
View file

@ -0,0 +1,129 @@
# frozen_string_literal: true
# A scene which updates and renders a list of options that get passed through.
#
# +menu_options+ data structure:
# [
# {
# text: 'some string',
# on_select: -> (args) { 'do some stuff in this lambda' }
# }
# ]
class MenuScene < SceneInstance
def initialize(args, opts = {}, menu_options = [])
super args, opts
@menu_state ||= {
current_option_i: 0,
hold_delay: 0
}
@spacer ||= mobile? ? 100 : 60
@menu_options ||= menu_options
@menu_y = opts.menu_y._?(420)
end
def render_options(args)
labels = []
@menu_options.each.with_index do |option, i|
text = case option.kind
when :toggle
"#{text(option[:key])}: #{text_for_setting_val(args, option[:key])}"
else
text(option[:key])
end
l = label(
text,
x: args.grid.w / 2,
y: @menu_y + (@menu_options.length - (i * @spacer)),
align: ALIGN_CENTER,
size: SIZE_MD
)
l.key = option[:key]
l.width, l.height = args.gtk.calcstringbox(l.text, l.size_enum)
labels << l
if @menu_state.current_option_i == i && (!mobile? || (mobile? && args.inputs.controller_one.connected))
args.outputs.solids << {
x: l.x - (l.width / 1.4) - 24 + (Math.sin(args.state.tick_count / 8) * 4),
y: l.y - 22,
w: 16,
h: 16
}.merge(WHITE)
end
button_border = { w: 340, h: 80, x: l.x - 170, y: l.y - 55 }.merge(WHITE)
(args.outputs.borders << button_border) if mobile?
if args.inputs.mouse.up && args.inputs.mouse.inside_rect?(button_border)
o = options.find { |o| o[:key] == l[:key] }
Sound.play(args, :menu)
o[:on_select].call(args) if o
end
end
args.outputs.labels << labels
end
# called every tick of the game loop
def tick(args)
super
render_options(args)
move = nil
if Input.down?(args)
move = :down
elsif Input.up?(args)
move = :up
else
@menu_state.hold_delay = 0
end
if move
@menu_state.hold_delay -= 1
if @menu_state.hold_delay <= 0
Sound.play(args, :menu)
index = @menu_state.current_option_i
if move == :up
index -= 1
else
index += 1
end
if index.negative?
index = @menu_options.length - 1
elsif index > @menu_options.length - 1
index = 0
end
@menu_state.current_option_i = index
@menu_state.hold_delay = 10
end
end
if Input.pressed?(args, :primary)
@menu_options[@menu_state.current_option_i][:on_select].call(args)
Sound.play(args, :select)
end
end
# custom logic to reset this scene
def reset(args)
super
@menu_state.current_option_i = 0
@menu_state.hold_delay = 0
end
def text_for_setting_val(args, key)
val = args.state.setting[key]
case val
when true
text(:on)
when false
text(:off)
else
val
end
end
end

View file

@ -1,60 +1,74 @@
module Scene # frozen_string_literal: true
class << self
# reachable via main menu or pause menu, allows for configuring the game
# for the player's preferences.
def tick_settings(args)
draw_bg(args, DARK_GREEN)
options = [ # This is the settings menu, allowing for various settings to be changed
{ class SettingsMenu < MenuScene
key: :sfx, def initialize(args, opts = {})
kind: :toggle, menu_options = [
setting_val: args.state.setting.sfx, {
on_select: -> (args) do key: :sfx,
GameSetting.save_after(args) do |args| kind: :toggle,
args.state.setting.sfx = !args.state.setting.sfx on_select: ->(args) do
end puts 'toggle sfx'
GameSetting.save_after(args) do |args|
args.state.setting.sfx = !args.state.setting.sfx
puts "sfx = #{args.state.setting.sfx}"
end end
}, end
{ },
key: :music, {
kind: :toggle, key: :music,
setting_val: args.state.setting.music, kind: :toggle,
on_select: -> (args) do on_select: ->(args) do
GameSetting.save_after(args) do |args| GameSetting.save_after(args) do |args|
args.state.setting.music = !args.state.setting.music args.state.setting.music = !args.state.setting.music
Music.set_volume(args, args.state.setting.music ? 0.8 : 0.0) Music.set_volume(args, args.state.setting.music ? 0.8 : 0.0)
end
end end
}, end
{ },
key: :back, {
on_select: -> (args) { Scene.pop(args) } key: :back,
}, on_select: ->(iargs) { Scene.pop(iargs) }
] }
]
if args.gtk.platform?(:desktop) if args.gtk.platform?(:desktop)
options.insert(options.length - 1, { menu_options.insert(
key: :fullscreen, menu_options.length - 1,
kind: :toggle, {
setting_val: args.state.setting.fullscreen, key: :fullscreen,
on_select: -> (args) do kind: :toggle,
on_select: ->(args) do
GameSetting.save_after(args) do |args| GameSetting.save_after(args) do |args|
args.state.setting.fullscreen = !args.state.setting.fullscreen args.state.setting.fullscreen = !args.state.setting.fullscreen
args.gtk.set_window_fullscreen(args.state.setting.fullscreen) args.gtk.set_window_fullscreen(args.state.setting.fullscreen)
end end
end end
}) }
end )
Menu.tick(args, :settings, options)
if Input.pressed?(args, :secondary)
Sound.play(args, :select)
options.find { |o| o[:key] == :back }[:on_select].call(args)
end
args.outputs.labels << label(:settings, x: args.grid.w / 2, y: args.grid.top - 200, align: ALIGN_CENTER, size: SIZE_LG, font: FONT_BOLD)
end end
super args, opts, menu_options
end
# called every tick of the game loop
def tick(args)
draw_bg(args, DARK_GREEN)
# actual menu logic is handled by the MenuScene super class
super
if Input.pressed?(args, :secondary)
Sound.play(args, :select)
@menu_options.find { |o| o[:key] == :back }[:on_select].call(args)
end
args.outputs.labels << label(
:settings,
x: args.grid.w / 2,
y: args.grid.top - 200,
align: ALIGN_CENTER,
size: SIZE_LG,
font: FONT_BOLD
)
end end
end end

View file

@ -1,21 +1,27 @@
# frozen_string_literal: true
# Code that only gets run once on game start # Code that only gets run once on game start
def init(args) def init(args)
Input.reset_swipe(args) Input.reset_swipe(args)
GameSetting.load_settings(args) GameSetting.load_settings(args)
end end
# Code that runs every game tick (mainly just calling other ticks)
def tick(args) def tick(args)
init(args) if args.state.tick_count == 0 init(args) if args.state.tick_count.zero?
# this looks good on non 16:9 resolutions; game background is different # this looks good on non 16:9 resolutions; game background is different
args.outputs.background_color = TRUE_BLACK.values args.outputs.background_color = TRUE_BLACK.values
args.state.has_focus ||= true args.state.has_focus ||= true
Scene.push(args, :main_menu, reset: true) if !args.state.scene args.state.scene_stack ||= []
Scene.push(args, Scene.default(args), reset: true) if args.state.scene_stack.empty?
Input.track_swipe(args) if mobile? Input.track_swipe(args) if mobile?
Scene.send("tick_#{args.state.scene}", args) # Scene.send("tick_#{args.state.scene}", args)
args.state.scene_stack.each do |scene|
scene.tick(args) if scene.tick_in_background || scene == args.state.scene_stack.last
end
Music.tick(args) Music.tick(args)
@ -45,7 +51,7 @@ def debug_tick(args)
if args.inputs.keyboard.key_down.i if args.inputs.keyboard.key_down.i
Sound.play(args, :select) Sound.play(args, :select)
Sprite.reset_all(args) Sprite.reset_all(args)
args.gtk.notify!("Sprites reloaded") args.gtk.notify!('Sprites reloaded')
end end
if args.inputs.keyboard.key_down.r if args.inputs.keyboard.key_down.r
@ -57,9 +63,9 @@ def debug_tick(args)
Sound.play(args, :select) Sound.play(args, :select)
args.state.simulate_mobile = !args.state.simulate_mobile args.state.simulate_mobile = !args.state.simulate_mobile
msg = if args.state.simulate_mobile msg = if args.state.simulate_mobile
"Mobile simulation on" 'Mobile simulation on'
else else
"Mobile simulation off" 'Mobile simulation off'
end end
args.gtk.notify!(msg) args.gtk.notify!(msg)
end end

View file

@ -26,7 +26,7 @@ module Music
end end
def paused(args, channel = 0) def paused(args, channel = 0)
args.audio["MUSIC_CHANNEL_#{channel}"].paused args.audio["MUSIC_CHANNEL_#{channel}"].paused unless stopped(args, channel)
end end
def stop(args, channel = 0) def stop(args, channel = 0)
@ -34,15 +34,15 @@ module Music
end end
def pause(args, channel = 0) def pause(args, channel = 0)
args.audio["MUSIC_CHANNEL_#{channel}"].paused = true args.audio["MUSIC_CHANNEL_#{channel}"].paused = true unless stopped(args, channel)
end end
def resume(args, channel = 0) def resume(args, channel = 0)
args.audio["MUSIC_CHANNEL_#{channel}"].paused = false args.audio["MUSIC_CHANNEL_#{channel}"].paused = false unless stopped(args, channel)
end end
def set_volume(args, volume, channel = 0) def set_volume(args, volume, channel = 0)
args.audio["MUSIC_CHANNEL_#{channel}"].gain = volume args.audio["MUSIC_CHANNEL_#{channel}"].gain = volume unless stopped(args, channel)
end end
def tick(args) def tick(args)

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true
# A scene represents a discreet state of gameplay. Things like the main menu, # A scene represents a discreet state of gameplay. Things like the main menu,
# game over screen, and gameplay. # game over screen, and gameplay.
# #
# Define a new scene by adding one to `app/scenes/` and defining a # Define a new scene by adding one to `app/scenes/` and inheriting from
# `Scene.tick_SCENE_NAME` class method. # SceneInstance
# #
# The main `#tick` of the game handles delegating to the current scene based on # The main `#tick` of the game handles delegating to the current scene based on
# the `args.state.scene` value, which is a symbol of the current scene, ex: # the `args.state.scene` value, which is a symbol of the current scene, ex:
@ -14,17 +16,12 @@ module Scene
# ex: # ex:
# Scene.switch(args, :gameplay) # Scene.switch(args, :gameplay)
def switch(args, scene, reset: false, push_or_pop: false) def switch(args, scene, reset: false, push_or_pop: false)
args.state.scene_stack ||= []
# if we're here /not/ from push or pop, clear the scene stack # if we're here /not/ from push or pop, clear the scene stack
args.state.scene_stack.clear unless push_or_pop args.state.scene_stack.clear unless push_or_pop
args.state.scene_stack.push(scene) if args.state.scene_stack.empty?
if reset scene.reset(args) if reset
args.state.send(scene)&.current_option_i = nil
args.state.send(scene)&.hold_delay = nil
# you can also add custom reset logic as-needed for specific scenes
# here
args.state.game.reset_game if args.state.game
end
args.state.scene = scene args.state.scene = scene
raise FinishTick, 'finish tick early' raise FinishTick, 'finish tick early'
@ -32,17 +29,23 @@ module Scene
# Change the current scene and push the previous scene onto the stack # Change the current scene and push the previous scene onto the stack
def push(args, scene, reset: false) def push(args, scene, reset: false)
puts "Pushing #{scene}"
args.state.scene_stack ||= [] args.state.scene_stack ||= []
args.state.scene_stack.push(args.state.scene) the_scene = scene.is_a?(SceneInstance) ? scene : SCENES[scene].new(args)
args.state.scene_stack.push(the_scene)
switch(args, scene, reset: reset, push_or_pop: true) switch(args, the_scene, reset: reset, push_or_pop: true)
end end
# Return to the previous scene on the stack # Return to the previous scene on the stack
def pop(args, reset: false) def pop(args, reset: false)
scene = !args.state.scene_stack || args.state.scene_stack.empty? ? :back : args.state.scene_stack.pop scene = args.state.scene_stack&.pop
switch(args, scene, reset: reset, push_or_pop: true)
switch(args, scene._?(default(args)), reset: reset, push_or_pop: true)
end
def default(args)
args.state.scene_stack ||= []
SCENES[:DEFAULT].new(args)
end end
end end
end end

View file

@ -8,7 +8,7 @@ module Sound
end end
def play(args, key, opts = {}) def play(args, key, opts = {})
SOUNDS.fetch(key).play(args, opts) SOUNDS.fetch(key).play(args, opts) if args.state.setting.sfx
end end
def stop(args, key) def stop(args, key)