--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo/snake/snake.c Tue Aug 19 18:05:35 2025 +0200 @@ -0,0 +1,598 @@ +/* + * 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; +} +