Snake Game

This is a complete, playable version of the classic Snake game implemented in Godot 4. The core innovation of this project is using GDScript to manage all the game logic (movement, collision, growth, and food placement) while offloading the entire rendering of the grid, snake, and food to a single, optimized CanvasItem Shader.

The GDScript efficiently calculates the game state (a list of grid coordinates) and passes this data as uniform arrays to the shader every frame. The shader then determines the color of each pixel based on whether it falls on a food position, a snake segment, or the background.

 

Adjustable Uniforms (Shader Parameters):

 

Parameter Type Controlled By Description
COLOR_SERPIENTE vec4 @export var in GDScript The color used to render the snake’s body segments.
COLOR_COMIDA vec4 @export var in GDScript The color used to render the food pellet.
COLOR_FONDO vec4 @export var in GDScript The color of the game board background.
board_size vec2 const TAMANO_TABLERO The size of the game grid (e.g., 30×25 cells).
snake_body vec2[] GDScript Array The array of grid coordinates that define the snake’s current position and length.
food_pos vec2 GDScript Variable The current grid position of the food pellet.
game_over bool GDScript State A flag that triggers the semi-transparent overlay to indicate the game has ended.

 

 Script move and logic snake:

extends ColorRect

@export var color_serpiente: Color = Color("lime")
@export var color_comida: Color = Color.RED
@export var color_fondo: Color = Color.BLACK

const TAMANO_TABLERO = Vector2i(30, 25)

var serpiente: Array[Vector2i]
var direccion: Vector2i
var comida_pos: Vector2i
var game_over: bool = false
var moviendo: bool = false
var timer: Timer

func _ready():
	get_window().size = TAMANO_TABLERO * 20
	timer = Timer.new()
	add_child(timer)
	timer.timeout.connect(_mover_serpiente)
	iniciar_juego()

func _input(event):
	if game_over and event.is_action_pressed("ui_accept"):
		iniciar_juego()
		return
		
	if not moviendo:
		var nueva_direccion: Vector2i
		if event.is_action_pressed("ui_up"): nueva_direccion = Vector2i.UP
		elif event.is_action_pressed("ui_down"): nueva_direccion = Vector2i.DOWN
		elif event.is_action_pressed("ui_left"): nueva_direccion = Vector2i.LEFT
		elif event.is_action_pressed("ui_right"): nueva_direccion = Vector2i.RIGHT
		
		if nueva_direccion != Vector2i.ZERO and nueva_direccion + direccion != Vector2i.ZERO:
			direccion = nueva_direccion
			moviendo = true

func iniciar_juego():
	game_over = false
	serpiente.clear()
	
	var cabeza = TAMANO_TABLERO / 2
	serpiente.push_back(cabeza)
	serpiente.push_back(cabeza - Vector2i.RIGHT)
	
	direccion = Vector2i.RIGHT
	
	_colocar_comida()
	
	timer.wait_time = 0.15
	timer.start()
	
	_actualizar_shader()

func _mover_serpiente():
	if game_over:
		return

	var cabeza_actual = serpiente.front()
	var nueva_cabeza = cabeza_actual + direccion
	
	if (nueva_cabeza.x < 0 or nueva_cabeza.x >= TAMANO_TABLERO.x or
		nueva_cabeza.y < 0 or nueva_cabeza.y >= TAMANO_TABLERO.y or
		serpiente.has(nueva_cabeza)):
		_terminar_juego()
		return
	
	serpiente.insert(0, nueva_cabeza)
	
	if nueva_cabeza == comida_pos:
		_colocar_comida()
	else:
		serpiente.pop_back()
	
	moviendo = false
	_actualizar_shader()

func _colocar_comida():
	var nueva_pos: Vector2i
	while true:
		nueva_pos = Vector2i(randi_range(0, TAMANO_TABLERO.x - 1), randi_range(0, TAMANO_TABLERO.y - 1))
		if not serpiente.has(nueva_pos):
			comida_pos = nueva_pos
			return

func _terminar_juego():
	game_over = true
	timer.stop()
	_actualizar_shader()

func _actualizar_shader():
	material.set_shader_parameter("snake_body", serpiente)
	material.set_shader_parameter("snake_length", serpiente.size())
	material.set_shader_parameter("food_pos", comida_pos)
	material.set_shader_parameter("game_over", game_over)
	material.set_shader_parameter("COLOR_SERPIENTE", color_serpiente)
	material.set_shader_parameter("COLOR_COMIDA", color_comida)
	material.set_shader_parameter("COLOR_FONDO", color_fondo)
Shader code
shader_type canvas_item;

uniform vec2 board_size = vec2(30.0, 25.0);
uniform vec2 snake_body[512];
uniform int snake_length;
uniform vec2 food_pos;
uniform bool game_over;

uniform vec4 COLOR_FONDO;
uniform vec4 COLOR_SERPIENTE;
uniform vec4 COLOR_COMIDA;

const vec4 COLOR_GAME_OVER = vec4(1.0, 1.0, 1.0, 0.5);

void fragment() {
    ivec2 grid_uv = ivec2(UV * board_size);
    vec4 final_color = COLOR_FONDO;

    for (int i = 0; i < snake_length; i++) {
        if (grid_uv == ivec2(snake_body[i])) {
            final_color = COLOR_SERPIENTE;
            break;
        }
    }

    if (grid_uv == ivec2(food_pos)) {
        final_color = COLOR_COMIDA;
    }

    if (game_over) {
        final_color = mix(final_color, COLOR_GAME_OVER, COLOR_GAME_OVER.a);
    }

    COLOR = final_color;
}
Live Preview
Tags
canvasitem, Classic, game, GameLogic, GDScript, godotshader, grid, HybridRendering, snake, Uniforms
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Gerardo LCDF

Related shaders

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments