Skip to content

Instantly share code, notes, and snippets.

@amirrajan
Last active July 2, 2024 21:46
Show Gist options
  • Save amirrajan/c56ae84ae8a5bbcbef45ced4ee430dbb to your computer and use it in GitHub Desktop.
Save amirrajan/c56ae84ae8a5bbcbef45ced4ee430dbb to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - Player State Machine
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