test/snake/snake.c

Mon, 21 Jul 2025 21:28:34 +0200

author
Mike Becker <universe@uap-core.de>
date
Mon, 21 Jul 2025 21:28:34 +0200
changeset 217
4b3c974eab44
parent 216
943980fa37b5
permissions
-rw-r--r--

improve snap-to-grid-movement

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * Copyright 2023 Mike Becker. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <ascension/core.h>
#include <ascension/ui.h>
#include <ascension/sprite.h>
#include <ascension/2d.h>

#include <cx/printf.h>

enum Textures2d {
    TEX_SHIP = 0,
    TEX_BACKDROP,
    TEX2D_COUNT
};
static AscTexture tex2d[TEX2D_COUNT];
#define TEXTURE_SHIP &tex2d[TEX_SHIP]
#define TEXTURE_BACKDROP &tex2d[TEX_BACKDROP]

#define BACKDROP_SCENE asc_window_scene(0)
#define MAIN_SCENE asc_window_scene(1)

enum MoveDirection {
    MOVE_UP,
    MOVE_LEFT,
    MOVE_DOWN,
    MOVE_RIGHT
};

static asc_transform rotations[4];
static asc_vec2i directions[4];

typedef struct {
    enum MoveDirection direction;
    enum MoveDirection target_direction;
    /**
     * The speed in tiles per second.
     */
    float speed;
} Spaceship;

static const unsigned game_field_size = 16;
static const unsigned game_field_tile_size = 32;

static void init_globals(void) {
    asc_transform_identity(rotations[MOVE_UP]);
    asc_transform_roll(rotations[MOVE_LEFT], asc_rad(-90));
    asc_transform_roll(rotations[MOVE_RIGHT], asc_rad(90));
    asc_transform_roll(rotations[MOVE_DOWN], asc_rad(180));
    directions[MOVE_UP] = ASC_VEC2I(0, -1);
    directions[MOVE_LEFT] = ASC_VEC2I(-1, 0);
    directions[MOVE_DOWN] = ASC_VEC2I(0, 1);
    directions[MOVE_RIGHT] = ASC_VEC2I(1, 0);
}

static void destroy_textures(void) {
    asc_texture_destroy(tex2d, TEX2D_COUNT);
}

static void init_textures(void) {
    asc_texture_init_2d(tex2d, TEX2D_COUNT);
    asc_texture_from_file(TEXTURE_SHIP, "ship.png");
    asc_texture_from_file(TEXTURE_BACKDROP, "backdrop.png");
    asc_gl_context_add_cleanup_func(asc_active_glctx, destroy_textures);
}

static void scale_backdrop(AscBehavior *behavior) {
    // scale the backdrop to the size of the window
    if (asc_active_window->resized) {
        asc_ptr_cast(AscSprite, sprite, behavior->node);
        asc_vec2u window_size = asc_active_window->dimensions;
        asc_sprite_set_size(sprite, window_size);
    }
}

static void create_backdrop(void) {
    AscSceneNode *node = asc_sprite(
        .texture = TEXTURE_BACKDROP,
        .texture_scale_mode = ASC_TEXTURE_SCALE_REPEAT
    );
    asc_behavior_add(node, .func = scale_backdrop);
    asc_scene_add_node(BACKDROP_SCENE, node);
}

static void update_fps_counter(AscBehavior *behavior) {
    asc_ptr_cast(AscText, node, behavior->node);
    static float last_fps = 0.f;
    if (fabsf(asc_context.frame_rate - last_fps) > 1) {
        last_fps = asc_context.frame_rate;
        asc_text_printf(node, "%.2f FPS", asc_context.frame_rate);
    }
}

static void tie_fps_counter_to_corner(AscBehavior *behavior) {
    // TODO: this should be replaced with some sort of UI layout manager
    AscSceneNode *node = behavior->node;
    if (asc_test_flag(node->flags, ASC_SCENE_NODE_GRAPHICS_UPDATED) || asc_active_window->resized) {
        asc_vec2u bottom_right = asc_active_window->dimensions;
        asc_vec2u text_size = ((AscText*)node)->dimension;
        asc_scene_node_set_position2f(node, ASC_VEC2F(
                (int) bottom_right.x - (int) text_size.width - 10,
                (int) bottom_right.y - (int) text_size.height - 10
        ));
    }
}

static void create_fps_counter(void) {
    AscSceneNode *node = asc_text(
        .name = "FPS Counter",
        .color = ASC_RGB(255, 255, 255),
        .font = asc_font(ASC_FONT_REGULAR, 12),
    );
    asc_behavior_add(node, .func = update_fps_counter, .interval = asc_seconds(1));
    asc_behavior_add(node, tie_fps_counter_to_corner);
    asc_ui_add_node(node);
}

static void create_score_counter(void) {
    AscSceneNode *node = asc_text(
        .name = "Score Counter",
        .x = 10, .y = 10,
        .text = "Score: 0",
        .color = ASC_RGB(0, 255, 0),
        .font = asc_font(ASC_FONT_BOLD, 16),
    );
    asc_ui_add_node(node);
}

static void move_spaceship(AscBehavior *behavior) {
    AscSceneNode *node = behavior->node;
    Spaceship *spaceship = node->user_data;
    const int ts = (int) game_field_tile_size;
    const float fts = (float) ts;
    const float speed = fts * spaceship->speed * asc_context.frame_factor;
    const asc_vec2i dir = directions[spaceship->direction];
    const asc_vec2f movement = asc_vec2f_scale(asc_vec2_itof(dir), speed);
    // check if we are supposed to change the direction
    if (spaceship->direction == spaceship->target_direction) {
        // move without changing the direction
        asc_scene_node_move2f(node, movement);
    } else {
        // determine axis
        // and check if we are about to cross the center
        // this relies on positive positions!
        bool rotate = false;
        if (movement.x == 0) {
            // vertical movement
            const int y0 = (int)node->position.y / ts;
            const int y1 = (int)(node->position.y+movement.y) / ts;
            rotate = y0 != y1 && asc_sgn(y1-y0) == dir.y;
        } else {
            // horizontal movement
            const int x0 = (int)node->position.x / ts;
            const int x1 = (int)(node->position.x+movement.x) / ts;
            rotate = x0 != x1 && asc_sgn(x1-x0) == dir.x;
        }
        if (rotate) {
            // snap position to the center of the tile
            node->position.x = roundf(node->position.x / fts) * fts;
            node->position.y = roundf(node->position.y / fts) * fts;
            spaceship->direction = spaceship->target_direction;
            asc_scene_node_set_rotation(node, rotations[spaceship->direction]);
        } else {
            // changing the direction not permitted, yet, continue movement
            asc_scene_node_move2f(node, movement);
        }
    }
}

static Spaceship *create_spaceship(void) {
    AscSceneNode *sprite = asc_sprite(
        .name = "Player",
        .texture = TEXTURE_SHIP,
        // TODO: introduce a function to set the position of a space ship
        .x = game_field_tile_size * 8,
        .y = game_field_tile_size * 12,
        .width = game_field_tile_size,
        .height = game_field_tile_size,
        .origin_x = game_field_tile_size / 2,
        .origin_y = game_field_tile_size / 2,
    );
    asc_scene_add_node(MAIN_SCENE, sprite);
    Spaceship *ship = asc_scene_node_allocate_data(sprite, sizeof(Spaceship));
    ship->speed = 2.f; // start with 2 tiles/sec
    asc_behavior_add(sprite, move_spaceship);
    return sprite->user_data;
}

static void create_gamefield() {
    // TODO: create a proper data structure and a more interesting map than just a basic grid
    AscSceneNode *gamefield = asc_scene_node_empty();
    for (unsigned x = 1; x <= game_field_size; x++) {
        for (unsigned y = 1; y <= game_field_size; y++) {
            AscSceneNode *tile = asc_rectangle(
                .x = x*game_field_tile_size, .y = y*game_field_tile_size, .filled = true, .thickness = 1,
                .origin_x = game_field_tile_size / 2, .origin_y = game_field_tile_size / 2,
                .width = game_field_tile_size, .height = game_field_tile_size,
                .color = ASC_RGB(0, 128, 255),
                .border_color = ASC_RGB(64, 196, 255),
            );
            asc_scene_node_link(gamefield, tile);
        }
    }
    asc_scene_node_set_zindex(gamefield, -2);
    asc_scene_add_node(MAIN_SCENE, gamefield);
}

static asc_rect update_viewport_for_window_resize(asc_vec2u window_size) {
    // Compute scaling and offsets
    unsigned viewport_size, offset_x = 0, offset_y = 0;
    if (window_size.width > window_size.height) {
        // Wider window: letterbox (black bars on top/bottom)
        offset_x = (window_size.width - window_size.height) / 2;
        viewport_size = window_size.height;
    } else {
        // Taller window: pillarbox (black bars on sides)
        offset_y = (window_size.height - window_size.width) / 2;
        viewport_size = window_size.width;
    }

    // Set the viewport to the scaled and centered region
    return ASC_RECT(offset_x, offset_y, viewport_size, viewport_size);
}

int main(void) {
    asc_context_initialize();
    if (asc_has_error()) {
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
                "Fatal Error",asc_get_error(), NULL);
        return 1;
    }
#ifdef TEST_BUILD
    asc_set_font_path("../../fonts");
    asc_set_shader_path("../../shader");
    asc_set_texture_path("../../test/snake/textures");
#endif

    // initialize globals
    init_globals();

    // create window
    AscWindowSettings settings;
    asc_window_settings_init_defaults(&settings);
    settings.title = "Snake";
    settings.dimensions = ASC_VEC2U(800, 800);
    asc_window_initialize(0, &settings);
    asc_ui_scale_auto();

    // load textures
    init_textures();

    // initialize the scenes
    asc_scene_init(BACKDROP_SCENE,
        .type = ASC_CAMERA_ORTHO,
        .projection_update_func = asc_camera_ortho_update_size
    );
    asc_scene_init(MAIN_SCENE,
        .type = ASC_CAMERA_ORTHO,
        .ortho.rect = ASC_RECT(0, 0,
            (game_field_size+1)*game_field_tile_size,
            (game_field_size+1)*game_field_tile_size
        ),
        .viewport_clear = true,
        .clear_color = ASC_RGB(0, 128, 90),
        .viewport_update_func = update_viewport_for_window_resize
    );

    // backdrop for letterbox/pillarbox
    create_backdrop();

    // the game field
    create_gamefield();

    // create UI elements
    create_fps_counter();
    create_score_counter();

    // create spaceship
    Spaceship *spaceship = create_spaceship();

    // Main Loop
    do {
        // quit application on any error
        if (asc_has_error()) {
            SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
                    "Fatal Error", asc_get_error(),
                    asc_active_window->window);
            asc_clear_error();
            asc_context_quit();
        }

        // player rotation
        // TODO: queue up to two movement commands (for sharp 90° turns)
        if (asc_key_pressed(ASC_KEY(LEFT))) {
            if (spaceship->direction != MOVE_RIGHT) {
                spaceship->target_direction = MOVE_LEFT;
            }
        }
        if (asc_key_pressed(ASC_KEY(RIGHT))) {
            if (spaceship->direction != MOVE_LEFT) {
                spaceship->target_direction = MOVE_RIGHT;
            }
        }
        if (asc_key_pressed(ASC_KEY(UP))) {
            if (spaceship->direction != MOVE_DOWN) {
                spaceship->target_direction = MOVE_UP;
            }
        }
        if (asc_key_pressed(ASC_KEY(DOWN))) {
            if (spaceship->direction != MOVE_UP) {
                spaceship->target_direction = MOVE_DOWN;
            }
        }

        // debug-key for clearing the shader registry
        if (asc_key_pressed(ASC_KEY(S))) {
            asc_shader_clear_registry();
            asc_dprintf("Shader cache cleared.");
        }

        // quit application on ESC key press
        if (asc_key_pressed(ASC_KEY(ESCAPE))) {
            asc_context_quit();
        }
    } while (asc_loop_next());

    // cleanup
    asc_context_destroy();
    return 0;
}

mercurial