extends Area2D class_name SnakePart const TILE_SIZE = 20 enum PartTypes {HEAD, BODY, TAIL, DEAD} var part_type : PartTypes = PartTypes.HEAD var current_direction : Vector2 = Vector2.RIGHT var inputs : Dictionary[String, Vector2] = {"right": Vector2.RIGHT, "left": Vector2.LEFT, "up": Vector2.UP, "down": Vector2.DOWN} @onready var raycast_right : RayCast2D = $RightRayCast2D @onready var raycast_left : RayCast2D = $LeftRayCast2D @onready var raycast_up : RayCast2D = $UpRayCast2D @onready var raycast_down : RayCast2D = $DownRayCast2D var snake_part_obj : PackedScene = preload("res://Scenes/snake_part.tscn") @onready var timer_ref : Timer = $"../Timer" var next_part : SnakePart = null var skip_next_move_propagation : bool = false signal do_movement(new_dir) func _ready() -> void: if part_type == PartTypes.HEAD: # Add to group for easy access from other nodes add_to_group("Head") # Set up a body part var body = snake_part_obj.instantiate() body.part_type = PartTypes.BODY body.position = (position - current_direction * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) next_part = body get_parent().add_child.call_deferred(body) do_movement.connect(body.process_movement) # Set up a tail part var tail = snake_part_obj.instantiate() tail.part_type = PartTypes.TAIL tail.position = (position - current_direction * TILE_SIZE * 2).snapped(Vector2.ONE * TILE_SIZE) body.next_part = tail get_parent().add_child.call_deferred(tail) body.do_movement.connect(tail.process_movement) else: # Free raycasts if we don't need them (consider only loading them in for the head for performance if needed) raycast_down.queue_free() raycast_up.queue_free() raycast_left.queue_free() raycast_right.queue_free() func process_movement(new_direction : Vector2) -> void: # The head needs to check if we are about to collide with something if part_type == PartTypes.HEAD: check_movement() # Update the position of this part position = (position + current_direction * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) # Tell the next part to move and give it the direction we are moving if !skip_next_move_propagation: do_movement.emit(current_direction) skip_next_move_propagation = false # Unless we are the head, change our direction to the direction given if new_direction != Vector2.ZERO: current_direction = new_direction func check_movement() -> void: # Get the correct raycast for the direction we are moving var raycast_to_use : RayCast2D if current_direction == Vector2.RIGHT: raycast_to_use = raycast_right elif current_direction == Vector2.LEFT: raycast_to_use = raycast_left elif current_direction == Vector2.UP: raycast_to_use = raycast_up elif current_direction == Vector2.DOWN: raycast_to_use = raycast_down # Look using the raycast for anything to collide with var object_in_path : Object = raycast_to_use.get_collider() # If we found nothing, stop processing collision if object_in_path == null: return # If we found a snake part, then either end the game or perform an ouroboros maneouvre if object_in_path is SnakePart: match object_in_path.part_type: PartTypes.BODY: lose_game() PartTypes.TAIL: ouroboros() if object_in_path is Fruit: object_in_path.respawn() extend() func extend() -> void: # Remove old connection to previous next part do_movement.disconnect(next_part.process_movement) # Set up a body part to be in the chain var new_body = snake_part_obj.instantiate() new_body.part_type = PartTypes.BODY new_body.position = (position).snapped(Vector2.ONE * TILE_SIZE) get_parent().add_child(new_body) do_movement.connect(new_body.process_movement) new_body.do_movement.connect(next_part.process_movement) new_body.next_part = next_part # Set direction of new part new_body.current_direction = current_direction # Update reference to next part next_part = new_body # Mark the next movement to not propagate the movement down the chain skip_next_move_propagation = true func lose_game() -> void: # You lose! print("Game lost") timer_ref.stop() func ouroboros() -> void: print("You have achieved the ouroboros!") func _unhandled_input(event: InputEvent) -> void: # Change direction if we are the head if event is InputEventKey and part_type == PartTypes.HEAD: for dir in inputs.keys(): if event.is_action_pressed(dir): current_direction = inputs[dir] # If we leave the arena func _on_level_area_exited(area: Area2D) -> void: lose_game()