Last active
July 2, 2024 21:46
-
-
Save amirrajan/c56ae84ae8a5bbcbef45ced4ee430dbb to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - Player State Machine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Player | |
attr :x, :y, :w, :h, :action, :action_at, | |
:dx, :dy, :breathe_at, :facing, :run_speed, :on_ground_y, :on_ground, | |
:prev_on_ground | |
def initialize | |
@breathe_at = -100 | |
@action = :idle | |
@action_at = 0 | |
@x = 100 | |
@y = 720 | |
@w = 128 | |
@h = 128 | |
@is_falling = true | |
@fall_at = 0 | |
@requested_dx = 0 | |
@dx = 0 | |
@dy = 0 | |
@facing = 1 | |
@run_speed = 3 | |
@next_action = nil | |
@action_history = [] | |
@future_callback = [] | |
end | |
def action_lookup | |
lookup = { | |
idle: { | |
frame_count: 6, | |
sprite_sheet: "sprites/player-idle.png" | |
}, | |
run_start: { | |
frame_count: 3, | |
sprite_sheet: "sprites/player-run-start.png" | |
}, | |
run: { | |
frame_count: 8, | |
repeat: true, | |
sprite_sheet: "sprites/player-run.png" | |
}, | |
run_stop: { | |
frame_count: 7, | |
sprite_sheet: "sprites/player-run-stop.png" | |
}, | |
light_attack: { | |
frame_count: 12, | |
hold_for: 6, | |
sprite_sheet: "sprites/player-light-attack.png" | |
}, | |
light_attack_2: { | |
frame_count: 7, | |
sprite_sheet: "sprites/player-light-attack-2.png" | |
}, | |
land: { | |
frame_count: 5, | |
sprite_sheet: "sprites/player-land.png" | |
}, | |
fall: { | |
frame_count: 4, | |
repeat: true, | |
sprite_sheet: "sprites/player-fall.png" | |
}, | |
jump: { | |
frame_count: 6, | |
sprite_sheet: "sprites/player-jump.png", | |
default_index: 5 | |
}, | |
block_start: { | |
frame_count: 2, | |
sprite_sheet: "sprites/player-block-start.png" | |
}, | |
block: { | |
frame_count: 1, | |
repeat: true, | |
sprite_sheet: "sprites/player-block.png" | |
}, | |
block_stop: { | |
frame_count: 2, | |
sprite_sheet: "sprites/player-block-stop.png" | |
} | |
} | |
lookup.each do |k, v| | |
v.hold_for ||= 6 | |
v.default_index ||= 0 | |
v.duration = v[:frame_count] * v[:hold_for] | |
end | |
lookup | |
end | |
def tick left_right:, attack_held:, attack_pressed:, jump:, terrain:, camera:, block:; | |
# GTK.slowmo! 4 | |
@on_ground_y ||= (terrain.find_all { |t| (t.y + t.h) < @y }.sort_by { |t| -t.y }.first || { y: @y }).y | |
@input_requests = { left_right: left_right, | |
attack_held: attack_held, | |
jump: jump, | |
block: block, | |
attack_pressed: attack_pressed } | |
@future_callback.each do |callback| | |
if callback[:at] <= Kernel.tick_count | |
callback[:callback].call | |
end | |
end | |
@future_callback.reject! { |callback| callback[:at] <= Kernel.tick_count } | |
@terrain = terrain | |
@camera = camera | |
$args.outputs.debug.watch @input_requests, | |
label_style: { size_px: 30 } | |
$args.outputs.debug.watch @action, | |
label_style: { size_px: 30 } | |
# $args.outputs.debug.watch @facing | |
# $args.outputs.debug.watch @dx | |
# $args.outputs.debug.watch @prev_on_ground | |
# $args.outputs.debug.watch @on_ground | |
# $args.outputs.debug.watch @dy | |
# @action_history.each do |a| | |
# $args.outputs.debug.watch a | |
# end | |
__calc_actions__ | |
__process_next_action__ | |
__calc_collisions__ | |
end | |
def feet_box | |
{ x: @x - 1, y: @y + 4, w: 2, h: 8 } | |
end | |
def feet_box_just_below | |
feet_box.yield_self { |t| t.merge({ y: t.y - 4 }) } | |
end | |
def ramp_y collision | |
rel_x = (self.x - collision.x).fdiv collision.w | |
((collision.type.right_perc - collision.type.left_perc) * rel_x + collision.type.left_perc) * collision.h | |
end | |
def __clipping_ramp__? ramp | |
clip_height = 16 | |
@y < ramp.rect.y + ramp.ramp_y && @y + clip_height > ramp.rect.y + ramp.ramp_y | |
end | |
def __aabb_terrain__ | |
@terrain.find_all { |t| t.type.id == :ramp_aabb } | |
end | |
def __ramp_terrain__ | |
@terrain.find_all { |t| t.type.id != :ramp_aabb } | |
end | |
def body_box | |
{ x: @x - 12, y: @y, w: 24, h: 64 } | |
end | |
def __calc_collisions__ | |
# determine lowest ramp for camera | |
@lowest_ramp ||= @terrain.min_by { |t| t.y } | |
@lowest_ramp ||= { y: 0 } | |
# $args.outputs[:scene].borders << (Camera.to_screen_space @camera, body_box.merge({ r: 255, g: 0, b: 0 })) | |
# move the player horizontally | |
@x += @dx | |
collision = Geometry.find_intersect_rect(body_box, __aabb_terrain__) | |
if collision | |
if @dx > 0 | |
@x = collision.x - body_box.w / 2 | |
@dx = 0 | |
elsif @dx < 0 | |
@x = collision.x + collision.w + body_box.w / 2 | |
@dx = 0 | |
end | |
end | |
# if the player is on the ground, reduce dx by friction amount | |
if @on_ground | |
@dx *= 0.9 | |
else | |
@dx *= 0.99 | |
end | |
# store the last on_ground value and prev_y value | |
@prev_on_ground = @on_ground | |
@prev_y = @y | |
# apply gravity and set terminal velocity | |
@dy -= 0.2 | |
@dy = @dy.clamp(-8, 8) | |
# move player by y | |
@y += @dy | |
# $args.outputs[:scene].primitives << (Camera.to_screen_space @camera, feet_box.merge({ r: 255, g: 0, b: 0, path: :solid })) | |
# $args.outputs[:scene].primitives << (Camera.to_screen_space @camera, feet_box.merge({ y: feet_box.y + 64, r: 255, g: 0, b: 0, path: :solid })) | |
collision = Geometry.find_intersect_rect(body_box, __aabb_terrain__) | |
if collision | |
if @dy > 0 | |
@y = collision.y - body_box.h | |
@dy = 0 | |
@on_ground = false | |
elsif @dy <= 0 | |
@y = collision.y + collision.h | |
@on_ground = true | |
end | |
else | |
# get all bounding boxes that intersect with feet | |
collisions = Geometry.find_all_intersect_rect(feet_box, __ramp_terrain__) | |
# visualization of bounding boxes that are being evaluated | |
# $args.outputs[:scene].primitives << collisions.map do |collision| | |
# Camera.to_screen_space @camera, collision.merge({ r: 0, g: 0, b: 255, path: :solid, a: 128 }) | |
# end | |
collision = collisions.map do |collision| | |
r = { rect: collision, ramp_y: ramp_y(collision) } # get the ramp_y given the player's collision and ramp angle | |
r.delta_y = (@y - (collision.y + r.ramp_y)) # calc the difference between the ramp and the player position | |
r | |
end.sort_by { |c| c.delta_y.abs }.first # sort by the smallest ramp delta | |
if collision | |
# visualization of feet | |
# $args.outputs[:scene].primitives << Camera.to_screen_space(@camera, collision.rect.merge({ r: 255, g: 0, b: 128 })) | |
# set player to ramp location if the player is past the ramp | |
if __clipping_ramp__?(collision) && @dy < 0 | |
@y = collision.rect.y + collision.ramp_y | |
@on_ground = true | |
elsif @on_ground | |
@on_ground = false | |
@dy = 0 | |
end | |
elsif @on_ground | |
collision = Geometry.find_intersect_rect(feet_box_just_below, @terrain) | |
if !collision | |
@on_ground = false | |
@dy = 0 | |
end | |
end | |
end | |
if @y + @h < @lowest_ramp.y | |
@y = 1000 | |
@on_ground = false | |
end | |
end | |
def __light_attack__ | |
@facing = @input_requests.left_right.sign if @input_requests.left_right != 0 | |
queue_action action: :light_attack | |
end | |
def __calc_actions_jump__ | |
if @on_ground && @prev_on_ground | |
queue_action action: :land | |
elsif @prev_y > @y | |
queue_action action: :fall | |
end | |
if @input_requests.left_right != 0 && @facing == @input_requests.left_right.sign | |
@dx = @facing * @run_speed | |
elsif @input_requests.left_right != 0 | |
@dx = @input_requests.left_right * @run_speed.fdiv(2) | |
end | |
end | |
def __calc_actions_fall__ | |
if @input_requests.left_right != 0 && @facing == @input_requests.left_right.sign | |
@dx = @facing * @run_speed | |
elsif @input_requests.left_right != 0 | |
@dx = @input_requests.left_right * @run_speed.fdiv(2) | |
end | |
if @prev_on_ground != @on_ground && @on_ground | |
queue_action action: :land | |
end | |
end | |
def __calc_actions_idle__ | |
if @input_requests.attack_held | |
__light_attack__ | |
elsif @input_requests.block | |
queue_action action: :block_start | |
elsif @input_requests.jump | |
__jump__ | |
elsif @input_requests.left_right != 0 | |
@facing = @input_requests.left_right.sign | |
queue_action action: :run_start, at: Kernel.tick_count - action_lookup.run_start.hold_for | |
end | |
end | |
def __calc_actions_run__ | |
if @input_requests.attack_held | |
queue_action action: :light_attack | |
elsif @input_requests.jump | |
__jump__ | |
elsif @input_requests.block | |
queue_action action: :block_start | |
elsif @facing == @input_requests.left_right.sign | |
@dx = @input_requests.left_right * @run_speed | |
elsif @facing != @input_requests.left_right.sign && @input_requests.left_right != 0 | |
queue_action action: :run_stop | |
elsif @input_requests.left_right == 0 | |
queue_action action: :run_stop | |
end | |
end | |
def __calc_actions_run_stop__ | |
if @input_requests.attack_held | |
__light_attack__ | |
elsif @input_requests.jump && action_complete?(less_frames: 4) | |
__jump__ | |
elsif @input_requests.left_right != 0 && action_complete?(less_frames: 2) | |
@facing = @input_requests.left_right.sign | |
queue_action action: :run_start, at: Kernel.tick_count - action_lookup.run_start.hold_for * 2 | |
elsif @input_requests.left_right == 0 && action_complete? | |
queue_action action: :idle | |
end | |
end | |
def __calc_actions_light_attack_2__ | |
if action_complete? | |
queue_action action: :idle | |
end | |
end | |
def __calc_actions_light_attack__ | |
if @input_requests.attack_held && action_complete?(less_frames: 2) | |
queue_action action: :light_attack_2 | |
elsif @input_requests.attack_pressed | |
future_time = Kernel.tick_count + action_lookup[@action][:duration] - @action_at.elapsed_time | |
puts @action | |
puts Kernel.tick_count | |
puts future_time | |
queue_action action: :light_attack_2, at: future_time | |
elsif @input_requests.left_right != 0 && action_complete?(less_frames: 2) | |
@facing = @input_requests.left_right.sign | |
queue_action action: :run_start, at: Kernel.tick_count - action_lookup.run_start.hold_for | |
elsif @input_requests.jump && action_complete?(less_frames: 2) | |
__jump__ | |
elsif @input_requests.left_right == 0 && action_complete? | |
queue_action action: :idle | |
end | |
end | |
def __calc_actions_run_start__ | |
if action_complete? && @input_requests.left_right == 0 | |
queue_action action: :run_stop, at: Kernel.tick_count - action_lookup.run_stop.hold_for * 3 | |
elsif action_complete? && @input_requests.left_right != 0 | |
queue_action action: :run, at: Kernel.tick_count | |
else | |
@dx = @facing * @run_speed * 0.1 | |
end | |
end | |
def __calc_actions_land__ | |
if action_complete?(less_frames: 4) && @input_requests.left_right != 0 | |
@facing = @input_requests.left_right.sign | |
queue_action action: :run_start, at: Kernel.tick_count - action_lookup.run_start.hold_for | |
elsif @input_requests.attack_held | |
__light_attack__ | |
elsif @input_requests.jump && action_complete?(less_frames: 4) | |
__jump__ | |
elsif action_complete? && @input_requests.left_right == 0 | |
queue_action action: :idle | |
end | |
end | |
def queue_callback at:, &block | |
@future_callback << { at: at, callback: block } | |
end | |
def __calc_actions_block_start__ | |
if @input_requests.block && action_complete? | |
queue_action action: :block | |
end | |
end | |
def __calc_actions_block__ | |
if !@input_requests.block | |
queue_action action: :block_stop | |
elsif @input_requests.attack_held | |
queue_action action: :block_stop | |
queue_callback at: Kernel.tick_count + action_lookup.block_stop.duration do | |
__light_attack__ | |
end | |
elsif @input_requests.jump | |
queue_action action: :block_stop | |
queue_callback at: Kernel.tick_count + action_lookup.block_stop.duration do | |
__jump__ | |
end | |
elsif @input_requests.left_right != 0 && @facing.sign != @input_requests.left_right.sign | |
queue_action action: :block_stop | |
lr = @input_requests.left_right.sign | |
queue_callback at: Kernel.tick_count + action_lookup.block_stop.duration do | |
queue_action action: :block_start | |
@facing = lr | |
end | |
end | |
end | |
def __calc_actions_block_stop__ | |
if @input_requests.block && action_complete? | |
if @input_requests.left_right != 0 && @facing.sign != @input_requests.left_right.sign | |
@facing = @input_requests.left_right.sign | |
queue_action action: :block_start | |
end | |
elsif @input_requests.attack_held && action_complete? | |
__light_attack__ | |
elsif @input_requests.jump && action_complete? | |
__jump__ | |
elsif @input_requests.left_right != 0 && action_complete? | |
@facing = @input_requests.left_right.sign | |
queue_action action: :run | |
elsif @input_requests.left_right == 0 && action_complete? | |
queue_action action: :idle | |
end | |
end | |
def __calc_actions__ | |
if Kernel.tick_count.zmod? 240 | |
if rand > 0.5 | |
@breathe_at = Kernel.tick_count | |
end | |
end | |
if @action == :jump | |
__calc_actions_jump__ | |
elsif !@on_ground && !@prev_on_ground && @action != :fall | |
queue_action action: :fall | |
elsif @action == :fall | |
__calc_actions_fall__ | |
elsif @action == :idle | |
__calc_actions_idle__ | |
elsif @action == :run | |
__calc_actions_run__ | |
elsif @action == :run_stop | |
__calc_actions_run_stop__ | |
elsif @action == :light_attack | |
__calc_actions_light_attack__ | |
elsif @action == :light_attack_2 | |
__calc_actions_light_attack_2__ | |
elsif @action == :run_start | |
__calc_actions_run_start__ | |
elsif @action == :land | |
__calc_actions_land__ | |
elsif @action == :block_start | |
__calc_actions_block_start__ | |
elsif @action == :block | |
__calc_actions_block__ | |
elsif @action == :block_stop | |
__calc_actions_block_stop__ | |
end | |
end | |
def __jump__ | |
@dy = 6.5 | |
@on_ground = false | |
@facing = @input_requests.left_right.sign if @input_requests.left_right != 0 | |
queue_action action: :jump | |
end | |
def action_complete? less_frames: 0 | |
@action_at.elapsed_time >= action_lookup[@action][:duration] - action_lookup[@action][:hold_for] * less_frames | |
end | |
def prefab | |
lookup = action_lookup[@action] | |
frame_and_sprite = if @action == :idle | |
{ frame_index: Numeric.frame_index(start_at: @breathe_at, **lookup) || lookup.default_index, | |
sprite_sheet: action_lookup[@action][:sprite_sheet] } | |
else | |
{ frame_index: (Numeric.frame_index start_at: @action_at, **lookup) || lookup.default_index, | |
sprite_sheet: action_lookup[@action][:sprite_sheet] } | |
end | |
sprite = { x: @x, | |
y: @y, | |
w: @w, | |
h: @h, | |
flip_horizontally: @facing == -1, | |
anchor_x: 0.5, | |
source_x: 160 * frame_and_sprite[:frame_index], | |
source_y: 0, | |
source_w: 160, | |
source_h: 160, | |
path: frame_and_sprite[:sprite_sheet] } | |
end | |
def __process_next_action__ | |
return if !@next_action | |
return if !@next_action.action | |
return if @next_action.at < Kernel.tick_count | |
if @action != @next_action.action | |
@action = @next_action.action | |
@action_at = @next_action.at | |
@action_history.unshift({ action: @next_action.action, at: @next_action.at }) | |
end | |
@action_history = @action_history.take 10 | |
end | |
def queue_action action: nil, at: Kernel.tick_count | |
return if !action | |
@next_action = { at: at, action: action } | |
end | |
end | |
# GTK.reset_and_replay "replay.txt" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment