Skip to content

Instantly share code, notes, and snippets.

@N-Carter
Last active January 26, 2022 15:16
Show Gist options
  • Save N-Carter/fcabdaa51c5b62ece8658e369ccbb2e8 to your computer and use it in GitHub Desktop.
Save N-Carter/fcabdaa51c5b62ece8658e369ccbb2e8 to your computer and use it in GitHub Desktop.
extends KinematicBody2D
export(float) var speed := 8.0
export(float) var jump_impulse := 200.0
export(float) var max_speed := 200.0
export(float) var gravity_multiplier := 1.0
export(float) var snap_length := 5.0
export(float) var floor_damping := 200.0
export var _max_stamina := 100.0
onready var _stamina := _max_stamina
export var _stamina_regeneration_rate := 60.0
export var _stamina_flap_consumption := 15.0
onready var _gravity : float = ProjectSettings.get_setting("physics/2d/default_gravity")
onready var _gravity_vector : Vector2 = ProjectSettings.get_setting("physics/2d/default_gravity_vector")
var _velocity : Vector2
var _is_jumping : bool
var _time_in_air : float
var _is_powered_flight : bool
var _is_busy : bool
export var sprite : NodePath
onready var _sprite_node := get_node(sprite) as AnimatedSprite
export var sensor : NodePath
onready var _sensor_node := get_node(sensor) as CollisionShape2D
export var audio : NodePath
onready var _audio_node := get_node(audio) as AudioStreamPlayer2D
export(Array, AudioStream) var flap_sounds
export var pickup_sound : AudioStream
export var drop_sound : AudioStream
onready var _physics_query := Physics2DShapeQueryParameters.new()
var _escape_route_waypoints : Array
var _current_waypoint := 0
var _is_fleeing := false
# The contents of these arrays must match the Globals.EffectType enum:
export(Array, NodePath) var effect_statuses : Array
var _effect_status_nodes := []
var _use_state_machine := true
func _ready():
assert(not Game.player)
Game.player = self
_sprite_node.play()
if _use_state_machine:
_sprite_node.connect("frame_changed", self, "_frame_changed_state_machine")
_sprite_node.connect("animation_finished", self, "_animation_finished_state_machine")
_set_state("_idle_state")
Game.dialogue_box.connect("completed", self, "_dialogue_completed")
else:
_sprite_node.connect("frame_changed", self, "_frame_changed")
_sprite_node.connect("animation_finished", self, "_animation_finished")
_physics_query.collide_with_areas = true
_physics_query.collide_with_bodies = true
_physics_query.collision_layer = 0b1010
_physics_query.set_shape(_sensor_node.shape)
Game.overlay.set_max_stamina(_max_stamina)
Game.overlay.set_stamina(_stamina)
for effect_status in effect_statuses:
_effect_status_nodes.append(get_node(effect_status))
_effect_status_nodes[Globals.EffectType.THREAT].connect("on_threshold_reached", self, "_on_threat_triggered")
_effect_status_nodes[Globals.EffectType.THREAT].connect("on_effect_ended", self, "_on_threat_ended")
var frames = _sprite_node.frames
frames.set_animation_loop("land", false)
frames.set_animation_loop("pickup", false)
frames.set_animation_loop("jump", false)
frames.set_animation_loop("operate", false)
frames.set_animation_speed("listen", 4.0)
func _process(_delta):
Game.overlay.set_stamina(_stamina)
var effect_status := _effect_status_nodes[Globals.EffectType.THREAT] as EffectStatus
Game.overlay.set_max_threat(effect_status.get_capacity())
Game.overlay.set_threat(effect_status.get_current())
if Game.overlay.is_debug_visible:
Game.overlay.set_stats_text("V: %+6.2f, %+6.2f" % [_velocity.x, _velocity.y])
func _physics_process(delta):
if _use_state_machine:
_physics_process_state_machine(delta)
else:
if _is_fleeing:
_flee()
elif not _is_busy:
_player_control(delta)
# Original non-state machine behaviour:
func _player_control(delta : float) -> void:
# var joystick := Vector2(Input.get_joy_axis(0, JOY_AXIS_0), Input.get_joy_axis(0, JOY_AXIS_1))
var joystick := Vector2() # Now uses the input settings instead, for better or worse.
var force := Vector2.ZERO
var floored := is_on_floor()
if floored and _time_in_air > 0.2:
print("Landing")
_sprite_node.animation = "land"
_is_busy = true
_time_in_air = 0.0
return
if _velocity.y > 0.0:
_is_jumping = false
if Input.is_action_pressed("move_left") or joystick.x < -0.5:
force.x -= 1.0
_sprite_node.flip_h = true
if Input.is_action_pressed("move_right") or joystick.x > 0.5:
force.x += 1.0
_sprite_node.flip_h = false
if Input.is_action_pressed("move_up") or joystick.y < -0.5:
_is_powered_flight = true
if floored:
_jump()
else:
if _stamina > 0.0:
force.y -= 2.0
else:
_is_powered_flight = false
_velocity += force * speed
if floored:
_sprite_node.animation = "walk" if abs(_velocity.x) > 0.1 else "idle"
_stamina += 30.0 * delta
_time_in_air = 0.0
else:
_sprite_node.animation = "fly" if _velocity.y < 0.0 else "dive"
_stamina += 10.0 * delta
_time_in_air += delta
_stamina = min(_stamina, _max_stamina)
_velocity += _gravity_vector * (_gravity * gravity_multiplier)
var speed_limit = max_speed * (1.0 if floored else 2.0)
_velocity.x = clamp(_velocity.x, -speed_limit, speed_limit)
_velocity.y = clamp(_velocity.y, -speed_limit, speed_limit)
# move_and_slide multiplies by delta internally, so don't do it here!
var snap = 0.0 if _is_jumping else snap_length
var old_x = _velocity.x
# _velocity = move_and_slide(_velocity, Vector2.UP, true, 4, 1.0)
_velocity = move_and_slide_with_snap(_velocity, Vector2.DOWN * snap, Vector2.UP, true, 4, 1.0)
_velocity.x = lerp(old_x, _velocity.x, 0.5)
# var num_slides := get_slide_count()
# if(num_slides > 0):
# for i in num_slides:
# var slide := get_slide_collision(i)
## _velocity.x *= slide.normal.x # This is not correct, but something in here might help!
# _velocity.x -= slide.travel.x * 20.0
# if floored:
_velocity.x = move_toward(_velocity.x, 0.0, floor_damping * delta)
# print("%6.2f, %6.2f" % [_velocity.x, _velocity.y])
if Input.is_action_just_pressed("interact"):
_action()
if Input.is_action_just_pressed("held_item_use"):
_throw(1.0)
if Input.is_action_just_pressed("held_item_drop"):
_drop()
# FIXME: Unfinished!
# if Input.is_key_pressed(KEY_F):
# _escape_route_waypoints = $EscapeRoute.get_escape_route()
func _flee() -> void:
if _current_waypoint >= _escape_route_waypoints.size():
print("Fled to end of path")
return
var direction := _escape_route_waypoints[_current_waypoint] as Vector2 - global_position
var distance := direction.length()
if distance > 16.0:
_velocity = direction.normalized() * max_speed * 4.0 # Hardcoded multiplier!
move_and_slide(_velocity, Vector2.UP, true, 4, 1.0)
# print("Fleeing direction %s, to point %s, distance %f" % [direction, _escape_route_waypoints[_current_waypoint], distance])
else:
_current_waypoint += 1
print("Next waypoint ", _current_waypoint)
func _jump() -> void:
# print("Jump frame: %d" % get_tree().get_frame())
_velocity.y -= jump_impulse
_is_jumping = true
# sprite_node.animation = "fly"
# The action function is still used by the state machine:
func _action() -> void:
var state := get_world_2d().direct_space_state
_physics_query.transform = transform * _sensor_node.transform
print("Detecting:")
var results := state.intersect_shape(_physics_query)
for result in results:
var item = result["collider"]
print("Found %s" % item.name)
if item is Pickup:
if _use_state_machine:
_set_state("_pickup_state")
else:
_sprite_node.animation = "pickup"
_is_busy = true
_audio_node.stream = pickup_sound
_audio_node.play()
_face_point(item.global_position)
item.pick_up()
Game.overlay.add_to_inventory(item)
return
if item.has_method("action"):
print("Calling action() on " + item.name + " (owner: " + item.owner.name + ")")
_face_point(item.global_position)
if _use_state_machine:
_set_state("_operate_state")
item.action()
return
elif item.has_method("talk"):
print("Calling talk() on " + item.name + " (owner: " + item.owner.name + ")")
_face_point(item.global_position)
if _use_state_machine:
_set_state("_listen_state")
item.talk()
return
break
func _throw(force : float) -> void:
var pickup := Game.overlay.get_held_items().release_selected() as Pickup
if not pickup:
return
print("Throwing " + pickup.name)
pickup.drop(self)
pickup.apply_central_impulse(Vector2(-force if _sprite_node.flip_h else force, -0.2) * 200.0)
_audio_node.stream = drop_sound
_audio_node.play()
func _drop() -> void:
var pickup := Game.overlay.get_held_items().release_selected() as Pickup
if not pickup:
return
print("Dropping " + pickup.name)
pickup.drop(self)
_audio_node.stream = drop_sound
_audio_node.play()
func _frame_changed():
if(_sprite_node.animation == "fly" and _sprite_node.frame == 1):
_audio_node.stream = flap_sounds[randi() % flap_sounds.size()]
_audio_node.play()
if _is_powered_flight and _stamina > -15.0:
_stamina -= 15.0
func _animation_finished():
match _sprite_node.animation:
"pickup":
_is_busy = false
_velocity.x = 0.0
"land":
_is_busy = false
_sprite_node.animation = "default"
_velocity.x = 0.0
_:
return
print("Animation finished: ", _sprite_node.animation)
func add_effect(type : int, amount : float) -> void:
var effect_status := _effect_status_nodes[type] as EffectStatus
effect_status.add(amount)
func _on_threat_triggered() -> void:
Game.overlay.set_threat_draining(true)
_escape_route_waypoints = $EscapeRoute.get_escape_route()
_current_waypoint = 0
_is_fleeing = true
func _on_threat_ended() -> void:
Game.overlay.set_threat_draining(false)
_is_fleeing = false
# State machine stuff in progress:
var _current_state := ""
var _is_new_state := 0 # If greater than 0, this is still a new state
var _next_state_on_animation_end := ""
var _has_animation_frame_changed : bool
var _has_animation_ended : bool
var _delta : float
var _state_time : float
var _joystick : Vector2
var _is_floored : bool
var _last_state_velocity_y : float
func _physics_process_state_machine(delta):
_delta = delta
_joystick = Vector2.ZERO
_is_floored = is_on_floor()
if Input.is_action_pressed("move_left"):
_joystick.x -= 1.0
if Input.is_action_pressed("move_right"):
_joystick.x += 1.0
if Input.is_action_pressed("move_up"):
_joystick.y -= 1.0
if Input.is_action_pressed("move_down"):
_joystick.y += 1.0
_call_state()
_state_time += _delta
_has_animation_ended = false
_has_animation_frame_changed = false
func _set_state(state_name : String) -> void:
if _current_state != state_name:
_current_state = state_name
_is_new_state = 2
_state_time = 0.0
print("New state: ", state_name)
else:
print("Tried to switch to this state while already in it: ", state_name)
func _call_state() -> void:
call(_current_state)
if _is_new_state > 0:
_is_new_state -= 1
# State machine states:
func _idle_state() -> void:
if _is_new_state:
_sprite_node.animation = "idle"
_regenerate_stamina(1.0)
_stand_motion()
_ground_interactions()
if not _is_floored:
_set_state("_dive_state")
return
if abs(_joystick.x) > 0.01:
_set_state("_walk_state")
return
if _joystick.y < -0.01:
_set_state("_jump_state")
return
func _walk_state() -> void:
if _is_new_state:
_sprite_node.animation = "walk"
_regenerate_stamina(1.0)
_correct_flip()
_walk_motion()
_ground_interactions()
if not _is_floored:
_set_state("_dive_state")
return
if abs(_joystick.x) <= 0.01:
_set_state("_idle_state")
return
if _joystick.y < -0.01:
_set_state("_jump_state")
return
func _jump_state() -> void:
if _is_new_state:
_sprite_node.animation = "jump"
_next_state_on_animation_end = "_fly_state"
_velocity.y -= jump_impulse
_consume_stamina(_stamina_flap_consumption)
print("Jump impulse applied")
# _set_state("_fly_state")
func _fly_state() -> void:
if _is_new_state:
_sprite_node.animation = "fly"
# Scale joystick for up direction only:
if _joystick.y < 0.0:
_sprite_node.speed_scale = (1.0 - _joystick.y * 0.5)
else:
_sprite_node.speed_scale = 1.0
# Downwards acceleration when out of stamina, crude and doesn't feel right:
if _stamina <= 0.0:
_velocity.y += 400.0 * _delta
_regenerate_stamina(0.33)
_correct_flip()
_fly_motion()
if(_has_animation_frame_changed and _sprite_node.frame == 1):
_audio_node.stream = flap_sounds[randi() % flap_sounds.size()]
_audio_node.play()
# _velocity.y -= jump_impulse * 0.5 # Add some vertical speed on each flap
if _joystick.y < 0.0 and _stamina > -_stamina_flap_consumption:
_consume_stamina(_stamina_flap_consumption)
# if _stamina <= 0.0:
# _set_state("_dive_state")
# return
if _joystick.y > 0.0 and _velocity.y > 0.0:
_set_state("_dive_state")
return
if _is_floored:
# _set_state("_touchdown_state")
_set_state("_walk_state")
return
func _dive_state() -> void:
if _is_new_state:
_sprite_node.animation = "dive"
_regenerate_stamina(0.5)
_correct_flip()
_fly_motion()
if _joystick.y <= 0.0:
_set_state("_fly_state")
return
if _is_floored:
_set_state("_land_state")
return
func _land_state() -> void:
# Note that this is for heavy landings, and shouldn't really be used normally as it makes the character less nimble.
if _is_new_state:
_sprite_node.animation = "land"
_next_state_on_animation_end = "_idle_state"
_regenerate_stamina(1.0)
_stand_motion()
#func _touchdown_state() -> void:
# if _is_new_state:
# _sprite_node.animation = "jump"
# _next_state_on_animation_end = "_idle_state"
#
# _stand_motion()
func _pickup_state() -> void:
if _is_new_state:
_sprite_node.animation = "pickup"
_next_state_on_animation_end = "_idle_state"
_regenerate_stamina(1.0)
_stand_motion()
func _operate_state() -> void:
if _is_new_state:
_sprite_node.animation = "operate"
_next_state_on_animation_end = "_idle_state"
_regenerate_stamina(1.0)
_stand_motion()
func _listen_state() -> void:
if _is_new_state:
_sprite_node.animation = "listen"
_regenerate_stamina(1.0)
_stand_motion()
func _panic_state() -> void:
pass
# State machine utility functions:
func _correct_flip() -> void:
if _joystick.x < -0.01:
_sprite_node.flip_h = true
elif _joystick.x > 0.01:
_sprite_node.flip_h = false
func _face_point(global_point : Vector2) -> void:
_sprite_node.flip_h = global_position.x > global_point.x
func _stand_motion() -> void:
_velocity += _gravity_vector * (_gravity * gravity_multiplier)
_velocity.x = 0.0
# Alternative with heavy damping. If you make it too light, it slides constantly.
# _velocity.x = move_toward(_velocity.x, 0.0, max_speed * 20.0 * _delta)
_velocity = move_and_slide_with_snap(_velocity, Vector2.DOWN * snap_length, Vector2.UP, true, 4, 1.0)
func _walk_motion() -> void:
_velocity += _joystick * speed
_velocity += _gravity_vector * (_gravity * gravity_multiplier)
_velocity.x = clamp(_velocity.x, -max_speed, max_speed)
# _velocity.y = clamp(_velocity.y, -max_speed, max_speed)
# move_and_slide multiplies by delta internally, so don't do it here!
_velocity = move_and_slide_with_snap(_velocity, Vector2.DOWN * snap_length, Vector2.UP, true, 4, 1.0)
func _fly_motion() -> void:
var speed_scaled := speed * 0.5
_velocity.x += _joystick.x * speed_scaled
_velocity.y += _joystick.y * (1.0 if _joystick.y > 0.0 else 2.0) * speed_scaled
_velocity += _gravity_vector * (_gravity * gravity_multiplier) # Gravity
_velocity.x += (-1.0 if _sprite_node.flip_h else 1.0) * speed_scaled * 0.2 # Drift in direction of travel
_velocity = move_and_slide_with_snap(_velocity, Vector2.ZERO, Vector2.UP, true, 4, 1.0)
_velocity *= pow(0.9, _delta * Engine.iterations_per_second)
func _ground_interactions() -> void:
if Input.is_action_just_pressed("interact"):
_action()
if Input.is_action_just_pressed("held_item_use"):
_throw(1.0)
if Input.is_action_just_pressed("held_item_drop"):
_drop()
func _consume_stamina(amount : float) -> void:
_stamina -= amount
func _regenerate_stamina(factor : float) -> void:
_stamina += _stamina_regeneration_rate * factor * _delta
_stamina = min(_stamina, _max_stamina)
func _frame_changed_state_machine():
_has_animation_frame_changed = true
func _animation_finished_state_machine():
# print("Animation finished (state machine): ", _current_state)
_has_animation_ended = true
if _next_state_on_animation_end != "":
_set_state(_next_state_on_animation_end)
_next_state_on_animation_end = ""
func _dialogue_completed():
_set_state("_idle_state")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment