--- a/test/snake/snake.c Mon Aug 18 23:11:50 2025 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,598 +0,0 @@ -/* - * 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 <ascension/shader.h> - -#include <cx/printf.h> -#include <cx/linked_list.h> - -#define TEXTURE_2D_COUNT 3 -static AscTexture tex2d[TEXTURE_2D_COUNT]; -#define TEXTURE_PLAYER &tex2d[0] -#define TEXTURE_PLAYER_COLOR_MAP &tex2d[1] -#define TEXTURE_BACKDROP &tex2d[2] - -#define SHADER_ID_PLAYER 1 - -#define BACKDROP_SCENE asc_window_scene(0) -#define MAIN_SCENE asc_window_scene(1) -#define HUD_WIDTH 400 - -enum MoveDirection { - MOVE_UP, - MOVE_LEFT, - MOVE_DOWN, - MOVE_RIGHT -}; - -static asc_transform rotations[4]; -static asc_vec2i directions[4]; - -typedef struct { - asc_color color; - enum MoveDirection direction; - enum MoveDirection target_direction; - /** - * The speed in tiles per second. - */ - float speed; - /** - * A linked list of vec2u elements describing the current trace. - */ - CxList *trace; - /** - * The new position of the player when @c reset_position is @c true. - */ - asc_vec2i new_position; - unsigned health; - bool reset_position; - uint8_t number; -} Player; - -#define GAME_FIELD_SIZE 32 -#define GAME_FIELD_TILE_SIZE 32 - -/** The bit in the tile data indicating if the tile exists. */ -#define GAME_FIELD_TILE_EXISTS_FLAG 0x80 -/** The bits in the tile data that identify the owner. */ -#define GAME_FIELD_TILE_OWNER_MASK 0xF - -typedef struct { - AscSceneNode *nodes[GAME_FIELD_SIZE][GAME_FIELD_SIZE]; - int8_t tile_data[GAME_FIELD_SIZE][GAME_FIELD_SIZE]; -} GameField; - - -#define GAME_STATE_MENU 0 -#define GAME_STATE_PLAYING 1 -#define GAME_STATE_GAME_OVER 2 - -typedef struct { - int state; - GameField *field; - Player *players[4]; -} GameState; - -GameState game = {0}; - -static void globals_init(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 textures_destroy(void) { - asc_texture_destroy(tex2d, TEXTURE_2D_COUNT); -} - -static void textures_init(void) { - asc_texture_init_2d(tex2d, TEXTURE_2D_COUNT); - asc_texture_from_file(TEXTURE_PLAYER, "player.png"); - asc_texture_from_file(TEXTURE_PLAYER_COLOR_MAP, "player-color-map.png"); - asc_texture_from_file(TEXTURE_BACKDROP, "backdrop.png"); - asc_gl_context_add_cleanup_func(asc_active_glctx, textures_destroy); -} - -static void backdrop_scale(AscBehavior *behavior) { - // scale the backdrop to the size of the window - if (!asc_active_window->resized) return; - asc_ptr_cast(AscSprite, sprite, behavior->node); - asc_vec2u window_size = asc_active_window->dimensions; - asc_sprite_set_size(sprite, window_size); -} - -static void main_scene_frame_scale(AscBehavior *behavior) { - if (!asc_active_window->resized) return; - asc_ptr_cast(AscRectangle, frame, behavior->node); - asc_rectangle_set_bounds(frame, MAIN_SCENE->camera.viewport); -} - -static void backdrop_create(void) { - const float scale = 1.f / asc_active_window->ui_scale; - AscSceneNode *node = asc_sprite( - .texture = TEXTURE_BACKDROP, - .texture_scale_mode = ASC_TEXTURE_SCALE_REPEAT, - .texture_scale_x = scale, .texture_scale_y = scale, - ); - asc_behavior_add(node, .func = backdrop_scale); - asc_scene_add_node(BACKDROP_SCENE, node); - - // also add a frame for the main scene - // add this to the UI layer so that the border size does not scale - AscSceneNode *frame = asc_rectangle(.thickness = 2, .color = ASC_RGBi(66, 142, 161)); - asc_behavior_add(frame, .func = main_scene_frame_scale); - asc_ui_add_node(frame); -} - -static void fps_counter_update(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 fps_counter_tie_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_scene_node_set_position2f(node, ASC_VEC2F(10, - asc_active_window->dimensions.y - ((AscText*)node)->dimension.height - 10 - )); - } -} - -static AscSceneNode *fps_counter_create(void) { - AscSceneNode *node = asc_text( - .name = "FPS Counter", - .color = ASC_RGB(1, 1, 1), - .font = asc_font(ASC_FONT_REGULAR, 12), - ); - asc_behavior_add(node, .func = fps_counter_update, .interval = asc_seconds(1)); - asc_behavior_add(node, fps_counter_tie_to_corner); - asc_ui_add_node(node); - return node; -} - -static bool game_field_tile_chown(asc_vec2u coords, Player *player) { - unsigned x = coords.x, y = coords.y; - asc_ptr_cast(AscRectangle, tile, game.field->nodes[x][y]); - int old_owner = game.field->tile_data[x][y] & GAME_FIELD_TILE_OWNER_MASK; - if (player == NULL) { - asc_clear_flag(game.field->tile_data[x][y], GAME_FIELD_TILE_OWNER_MASK); - tile->color = ASC_RGBi(16, 50, 160); - } else { - asc_set_flag_masked(game.field->tile_data[x][y], GAME_FIELD_TILE_OWNER_MASK, player->number); - tile->color = player->color; - } - int new_owner = game.field->tile_data[x][y] & GAME_FIELD_TILE_OWNER_MASK; - return old_owner != new_owner; -} - -static void game_field_create() { - // TODO: create a more interesting map than just a basic grid - AscSceneNode *node = asc_scene_node_empty(); - game.field = asc_scene_node_allocate_data(node, sizeof(GameField)); - for (unsigned x = 0; x < GAME_FIELD_SIZE; x++) { - for (unsigned y = 0; y < GAME_FIELD_SIZE; y++) { - AscSceneNode *tile = asc_rectangle( - .x = x*GAME_FIELD_TILE_SIZE, .y = y*GAME_FIELD_TILE_SIZE, .filled = true, .thickness = 2, - .width = GAME_FIELD_TILE_SIZE, .height = GAME_FIELD_TILE_SIZE, - .color = ASC_RGBi(16, 50, 160), - .border_color = ASC_RGBi(20, 84, 128), - ); - - game.field->tile_data[x][y] = GAME_FIELD_TILE_EXISTS_FLAG; - game.field->nodes[x][y] = tile; - - asc_scene_node_link(node, tile); - } - } - asc_scene_node_set_zindex(node, -2); - asc_scene_add_node(MAIN_SCENE, node); -} - -static asc_vec2u game_field_tile_at_position(asc_vec3f position) { - return ASC_VEC2U((int)position.x / GAME_FIELD_TILE_SIZE, (int)position.y / GAME_FIELD_TILE_SIZE); -} - -typedef struct { - AscShaderProgram program; - asc_uniform_loc map_albedo; - asc_uniform_loc map_color; - asc_uniform_loc color; -} PlayerShader; - -static void player_shader_init(AscShaderProgram *p, cx_attr_unused int flags) { - asc_shader_init_uniform_loc_nice(p, PlayerShader, map_albedo); - asc_shader_init_uniform_loc_nice(p, PlayerShader, map_color); - asc_shader_init_uniform_loc_nice(p, PlayerShader, color); -} - -static AscShaderProgram *player_shader_create(cx_attr_unused int unused) { - return asc_shader_create((AscShaderCodes) { - .vtx = {.source_file = "sprite_vtx.glsl"}, - .frag = {.source_file = "player.glsl",}, - }, sizeof(PlayerShader), player_shader_init, 0); -} - -static void player_draw(const AscCamera *camera, const AscSceneNode *node) { - asc_cptr_cast(AscSprite, sprite, node); - const Player *player = node->user_data; - - // TODO: we shall finally add the shader information to the node - const AscShaderProgram *s = asc_shader_lookup( - SHADER_ID_PLAYER, player_shader_create, 0 - ); - if (asc_shader_use(s, camera)) return; - asc_cptr_cast(PlayerShader, shader, s); - - asc_shader_upload_model_matrix(s, node); - - // Bind texture - asc_texture_bind(TEXTURE_PLAYER, shader->map_albedo, 0); - asc_texture_bind(TEXTURE_PLAYER_COLOR_MAP, shader->map_color, 1); - asc_shader_upload_color(shader->color, player->color); - asc_mesh_draw_triangle_strip(&sprite->mesh); -} - -static void player_set_health(Player *player, unsigned health) { - player->health = health; - // TODO: probably we want to add more effects when the health changes -} - -static unsigned player_get_health(Player *player) { - return player->health; -} - -static void player_move(AscBehavior *behavior) { - AscSceneNode *node = behavior->node; - Player *player = node->user_data; - - // TODO: instead of skipping this behavior, it should be disabled when health is zero - if (player_get_health(player) == 0) return; - - // TODO: move this to a different behavior - asc_scene_node_show(node); - - const float ts = (float) GAME_FIELD_TILE_SIZE; - - // check if the position is set programmatically - if (player->reset_position) { - asc_scene_node_set_position2f(node, - ASC_VEC2F( - ts * (player->new_position.x + .5f), - ts * (player->new_position.y + .5f) - )); - player->reset_position = false; - return; - } - - // normal movement - const float speed = ts * player->speed * asc_context.frame_factor; - const asc_vec2i dir = directions[player->direction]; - const asc_vec2f movement = asc_vec2f_scale(asc_vec2_itof(dir), speed); - - // check if we are supposed to change the direction - if (player->direction == player->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 float y_0 = floorf(node->position.y / ts); - const float y_curr = node->position.y / ts - y_0; - const float y_next = (node->position.y+movement.y) / ts - y_0; - const bool side_curr = y_curr > 0.5f; - const bool side_next = y_next > 0.5f; - rotate = side_curr ^ side_next; - } else { - // horizontal movement - const float x0 = floorf(node->position.x / ts); - const float x_curr = node->position.x / ts - x0; - const float x_next = (node->position.x+movement.x) / ts - x0; - const bool side_curr = x_curr > 0.5f; - const bool side_next = x_next > 0.5f; - rotate = side_curr ^ side_next; - } - if (rotate) { - // snap position to the center of the tile - asc_scene_node_set_position2f(node, - ASC_VEC2F( - (.5f+floorf(node->position.x / ts)) * ts, - (.5f+floorf(node->position.y / ts)) * ts - )); - player->direction = player->target_direction; - asc_scene_node_set_rotation(node, rotations[player->direction]); - } else { - // changing the direction not permitted, yet, continue movement - asc_scene_node_move2f(node, movement); - } - } - - // die when leaving the game field - if (node->position.x < 0 || node->position.y < 0 || - node->position.x > GAME_FIELD_SIZE*GAME_FIELD_TILE_SIZE || - node->position.y > GAME_FIELD_SIZE*GAME_FIELD_TILE_SIZE) { - // TODO: add fancy death animation - asc_scene_node_hide(node); - player_set_health(player, 0); - // TODO: remove the trace gradually (dequeuing the trace should be a different behavior) - cxListClear(player->trace); - game.state = GAME_STATE_GAME_OVER; - return; - } - - // TODO: collision detection - - // update the trace, if necessary. - // remark: some calculations are repeated here, but they are cheap enough - { - const asc_vec2u tile_coords = game_field_tile_at_position(node->position); - // TODO: player should have been destroyed before leaving the field - if (tile_coords.x > GAME_FIELD_SIZE || tile_coords.y > GAME_FIELD_SIZE) return; - if (game_field_tile_chown(tile_coords, player)) { - // new owner of the tile! - asc_vec2u p = tile_coords; - cxListAdd(player->trace, &p); - if (cxListSize(player->trace) > 7) { - // TODO: implement power-up which makes the trace longer - cxListRemove(player->trace, 0); - } - } - } -} - -static void player_controls(Player *player) { - if (asc_key_pressed(ASC_KEY(LEFT))) { - if (player->direction != MOVE_RIGHT) { - player->target_direction = MOVE_LEFT; - } - } - if (asc_key_pressed(ASC_KEY(RIGHT))) { - if (player->direction != MOVE_LEFT) { - player->target_direction = MOVE_RIGHT; - } - } - if (asc_key_pressed(ASC_KEY(UP))) { - if (player->direction != MOVE_DOWN) { - player->target_direction = MOVE_UP; - } - } - if (asc_key_pressed(ASC_KEY(DOWN))) { - if (player->direction != MOVE_UP) { - player->target_direction = MOVE_DOWN; - } - } -} - -static void player_position(Player *pl, int x, int y) { - pl->new_position.x = x; - pl->new_position.y = y; - pl->reset_position = true; -} - -static void player_destroy(CxAllocator *allocator, Player *player) { - cxListFree(player->trace); - cxFree(allocator, player); -} - -static void player_trace_release_tile(asc_vec2u *coords) { - game_field_tile_chown(*coords, NULL); -} - -static Player *player_create(void) { - AscSceneNode *node = asc_sprite( - .name = "Player", - .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, node); - Player *player = asc_scene_node_allocate_data(node, sizeof(Player)); - player_position(player, 12, 8); - player->speed = 3.f; // start with 3 tiles/sec - player->number = 1; - player->health = 100; - player->color = ASC_RGB(1, 0, 0); - player->trace = cxLinkedListCreateSimple(sizeof(asc_vec2u)); - cxDefineDestructor(player->trace, player_trace_release_tile); - node->draw_func = player_draw; - node->user_data_free_func = (cx_destructor_func2)player_destroy; - asc_behavior_add(node, player_move); - return player; -} - -static asc_rect main_scene_viewport_update(asc_vec2u window_size) { - - // margins - const unsigned margin = 16; - - // space for score, power-ups, etc. - const unsigned left_area = (unsigned) (asc_active_window->ui_scale*HUD_WIDTH); - - // calculate how many pixels need to be removed from width and height - const unsigned rw = 2*margin + left_area; - const unsigned rh = 2*margin; - - // check if there is still a viewport left and chicken out when not - if (window_size.width < rw || window_size.height < rh) { - return ASC_RECT(0, 0, 0, 0); - } - window_size.width -= rw; - window_size.height -= rh; - - // 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; - } - offset_x += left_area + margin; - offset_y += margin; - - // 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; - } - - // initialize globals - globals_init(); - - // create the window - asc_window_initialize(0, asc_gl_context_settings_default(4, 0)); - asc_window_set_title(0, "Snake"); - asc_window_set_size(0, asc_vec2_ftou( - asc_vec2f_scale(ASC_VEC2F(1000+HUD_WIDTH, 1000), asc_ui_scale_auto()))); - asc_window_center(0); - - // load textures - textures_init(); - - // initialize backdrop scene - asc_scene_init(BACKDROP_SCENE, "backdrop", - .type = ASC_CAMERA_ORTHO, - .projection_update_func = asc_camera_ortho_update_size - ); - backdrop_create(); - - // Initialize main scene - asc_scene_init(MAIN_SCENE, "main", - .type = ASC_CAMERA_ORTHO, - .ortho.rect = ASC_RECT( - -GAME_FIELD_TILE_SIZE, - -GAME_FIELD_TILE_SIZE, - (GAME_FIELD_SIZE+2)*GAME_FIELD_TILE_SIZE, - (GAME_FIELD_SIZE+2)*GAME_FIELD_TILE_SIZE - ), - .viewport_clear = true, - .clear_color = ASC_RGBi(0, 32, 16), - .viewport_update_func = main_scene_viewport_update - ); - - // create the fps counter - AscSceneNode *fps_counter = fps_counter_create(); - asc_scene_node_hide(fps_counter); - - // create game over text - AscSceneNode *text_game_over = asc_text( - .name = "game_over_text", - .text = "Game Over\nPress R to Restart", - .color = ASC_RGB(1, 1, 1), - .font = asc_font(ASC_FONT_REGULAR, 36), - .alignment = ASC_TEXT_ALIGN_CENTER, - .centered = true, - .x = (GAME_FIELD_SIZE+2)*GAME_FIELD_TILE_SIZE/2, - .y = (GAME_FIELD_SIZE+2)*GAME_FIELD_TILE_SIZE/2 - 60, - ); - asc_scene_node_hide(text_game_over); - // TODO: add as a UI node and add a behavior which centers the node in the main scenes viewport - // otherwise we won't be able to implement a moving camera in the future - asc_scene_add_node(MAIN_SCENE, text_game_over); - - // initialize the game state - // TODO: add a main menu and start with the menu - game.state = GAME_STATE_PLAYING; - game.players[0] = player_create(); - game_field_create(); - - // 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(); - } - - // game states - // TODO: move all this into behaviors - if (game.state == GAME_STATE_PLAYING) { - // TODO: implement hot seat 1on1 multiplayer - player_controls(game.players[0]); - } else if (game.state == GAME_STATE_GAME_OVER) { - asc_scene_node_show(text_game_over); - if (asc_key_pressed(ASC_KEY(R))) { - // TODO: re-load the "level" - player_position(game.players[0], 12, 8); - player_set_health(game.players[0], 100); - game.state = GAME_STATE_PLAYING; - asc_scene_node_hide(text_game_over); - } - } - - // debug-key for clearing the shader registry - if (asc_key_pressed(ASC_KEY(S))) { - asc_shader_clear_registry(); - asc_dprintf("Shader cache cleared."); - } - - // show/hide the FPS counter - if (asc_key_pressed(ASC_KEY(F2))) { - asc_scene_node_toggle_visibility(fps_counter); - } - - // 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; -} -