extends Area2D class_name SnakePart const TILE_SIZE = 40 enum PartTypes {HEAD, BODY, TAIL} enum States {ALIVE, DEAD, OUROBOROS, OLD_OUROBOROS} var part_type : PartTypes = PartTypes.HEAD var state : States = States.ALIVE 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 var queued_growth : int = 0 signal on_movement(new_dir) signal on_death signal on_ouroboros func _ready() -> void: if part_type == PartTypes.HEAD: # Add to group for easy access from other nodes add_to_group("Head") # Attach clock var timer : Node = get_tree().get_first_node_in_group("GameClock") if timer is Timer: timer.timeout.connect(process_movement.bind(Vector2.ZERO)) # 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) on_movement.connect(body.process_movement) on_death.connect(body.lose_game) on_ouroboros.connect(body.ouroboros) # 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.on_movement.connect(tail.process_movement) on_death.connect(tail.lose_game) on_ouroboros.connect(tail.ouroboros) 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() # TODO: Optimise this! func generate_spawn_grid() -> Array[Vector2]: var spawn_grid : Array[Vector2] = [] # Determine the level limits var level : Area2D = get_tree().get_first_node_in_group("Level") var x_bounds : Vector2 = level.position.x * Vector2.ONE + Vector2(-TILE_SIZE, TILE_SIZE) * level.scale.x / 2 var y_bounds : Vector2 = level.position.y * Vector2.ONE + Vector2(-TILE_SIZE, TILE_SIZE) * level.scale.y / 2 var possible_x : Array = range(x_bounds.x, x_bounds.y, TILE_SIZE) var possible_y : Array = range(y_bounds.x, y_bounds.y, TILE_SIZE) # Flood fill all possibilities until we get an inside to use for x in possible_x: for y in possible_y: var start_position : Vector2 = Vector2(x, y).snapped(Vector2.ONE * TILE_SIZE) spawn_grid.clear() if !check_position_for_snake(start_position) and flood_fill(spawn_grid, start_position, x_bounds, y_bounds): GridManager.current_allowed_spawns = spawn_grid return spawn_grid GridManager.current_allowed_spawns = spawn_grid return spawn_grid func flood_fill(positions : Array[Vector2], new_position : Vector2, x_bounds : Vector2, y_bounds : Vector2) -> bool: var directions : Array[Vector2] = [Vector2.DOWN, Vector2.LEFT, Vector2.UP, Vector2.RIGHT] var possible_x : Array = range(x_bounds.x, x_bounds.y, TILE_SIZE) var possible_y : Array = range(y_bounds.x, y_bounds.y, TILE_SIZE) var inside : bool = true for direction in directions: var test_position : Vector2 = (new_position + direction * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) if not int(test_position.x) in possible_x or not int(test_position.y) in possible_y: inside = false if not test_position in positions and int(test_position.x) in possible_x and int(test_position.y) in possible_y and !check_position_for_snake(test_position): positions.append(test_position) inside = inside and flood_fill(positions, test_position, x_bounds, y_bounds) return inside func check_position_for_snake(query_position : Vector2) -> bool: var obstacles : Array[Node] = get_tree().get_nodes_in_group("BlocksSpawn") for obstacle in obstacles: if obstacle is SnakePart: if obstacle.state == States.OUROBOROS and obstacle.position == query_position: return true return false func process_movement(new_direction : Vector2) -> void: # Only alive snakes can move if state != States.ALIVE: return # The head needs to check if we are about to collide with something if part_type == PartTypes.HEAD: check_movement() if queued_growth: extend() # Only alive snakes can move if state != States.ALIVE: return # 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: on_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: if object_in_path.state == States.ALIVE: match object_in_path.part_type: PartTypes.BODY: lose_game() PartTypes.TAIL: ouroboros() else: lose_game() if object_in_path is Fruit: queued_growth += object_in_path.growth_amount object_in_path.respawn() func extend() -> void: # Remove old connection to previous next part on_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) on_movement.connect(new_body.process_movement) new_body.on_movement.connect(next_part.process_movement) on_death.connect(new_body.lose_game) on_ouroboros.connect(new_body.ouroboros) 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 # Mark the growth step as done queued_growth -= 1 func lose_game() -> void: # You lose! print("Game lost") state = States.DEAD on_death.emit() func ouroboros() -> void: print("You have achieved the ouroboros!") if state == States.OUROBOROS: state = States.OLD_OUROBOROS elif state == States.ALIVE: state = States.OUROBOROS on_ouroboros.emit() # The head can spawn a new snake if part_type == PartTypes.HEAD: GameManager.score += 1 get_tree().get_first_node_in_group("GameClockPause").start(1) get_tree().get_first_node_in_group("GameClock").stop() spawn_new_snake() func spawn_new_snake() -> void: # Find out where we can spawn a snake #var start = Time.get_ticks_usec() var all_spawn_positions : Array[Vector2] = generate_spawn_grid().duplicate() #var end = Time.get_ticks_usec() #var worker_time = (end-start)/1000000.0 #print(worker_time) # We cannot spawn too close to a snake on the left, so add a condition var spawn_positions : Array[Vector2] = [] for pos in all_spawn_positions: if (pos + Vector2.LEFT * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions and \ (pos + Vector2.LEFT * TILE_SIZE * 2).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions: spawn_positions.append(pos) var slightly_preferred_spawns : Array[Vector2] = [] for pos in spawn_positions: if (pos + Vector2.RIGHT * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions: slightly_preferred_spawns.append(pos) var most_preferred_spawns : Array[Vector2] = [] for pos in slightly_preferred_spawns: if (pos + Vector2.UP * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions and \ (pos + Vector2.DOWN * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions: most_preferred_spawns.append(pos) if !most_preferred_spawns.is_empty(): spawn_positions = most_preferred_spawns elif !slightly_preferred_spawns.is_empty(): spawn_positions = slightly_preferred_spawns # If there is no room to spawn a snake, we lose if spawn_positions.is_empty(): lose_game() return # Pick a position var chosen_position : Vector2 = spawn_positions.pick_random() # Spawn in the new snake var new_head = snake_part_obj.instantiate() new_head.position = chosen_position.snapped(Vector2.ONE * TILE_SIZE) get_parent().add_child(new_head) new_head.on_ouroboros.connect(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()