Tue, 19 Aug 2025 18:05:35 +0200
rename test folder to demo
Makefile | file | annotate | diff | comparison | revisions | |
demo/snake/Makefile | file | annotate | diff | comparison | revisions | |
demo/snake/shader/player.glsl | file | annotate | diff | comparison | revisions | |
demo/snake/snake.c | file | annotate | diff | comparison | revisions | |
demo/snake/textures/backdrop.png | file | annotate | diff | comparison | revisions | |
demo/snake/textures/player-color-map.png | file | annotate | diff | comparison | revisions | |
demo/snake/textures/player.png | file | annotate | diff | comparison | revisions | |
test/snake/Makefile | file | annotate | diff | comparison | revisions | |
test/snake/shader/player.glsl | file | annotate | diff | comparison | revisions | |
test/snake/snake.c | file | annotate | diff | comparison | revisions | |
test/snake/textures/backdrop.png | file | annotate | diff | comparison | revisions | |
test/snake/textures/player-color-map.png | file | annotate | diff | comparison | revisions | |
test/snake/textures/player.png | file | annotate | diff | comparison | revisions |
--- a/Makefile Mon Aug 18 23:11:50 2025 +0200 +++ b/Makefile Tue Aug 19 18:05:35 2025 +0200 @@ -26,7 +26,7 @@ all: snake snake: build/lib/libascension.a FORCE - @cd test/snake && $(MAKE) + @cd demo/snake && $(MAKE) build/lib/libascension.a: config.mk FORCE @cd src && $(MAKE) @@ -43,7 +43,7 @@ update-rules: make/update-rules.sh src - CFLAGS=-I../../src make/update-rules.sh test/snake + CFLAGS=-I../../src make/update-rules.sh demo/snake FORCE:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo/snake/Makefile Tue Aug 19 18:05:35 2025 +0200 @@ -0,0 +1,65 @@ +# 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 ../../config.mk + +BUILD_DIR=$(srcdir)/build/snake +LIB_ASCENSION=$(srcdir)/build/lib/libascension.a +CFLAGS += -I$(srcdir)/src $(CPPFLAGS) + +all: $(BUILD_DIR) $(BUILD_DIR)/snake FORCE + @echo "Demo game 'snake' successfully built." + @cp -Ruf $(srcdir)/shader $(BUILD_DIR)/ + @cp -Ruf $(srcdir)/fonts $(BUILD_DIR)/ + @cp -Ruf textures $(BUILD_DIR)/ + @cp -Ruf shader $(BUILD_DIR)/ + @echo "Assets for demo game 'snake' successfully copied." + +$(BUILD_DIR): + mkdir -p $@ + +$(BUILD_DIR)/snake: $(BUILD_DIR)/snake.o $(LIB_ASCENSION) + @echo "Linking snake..." + $(CC) $(LDFLAGS) -o $@ $^ + +$(LIB_ASCENSION): + test -f "$@" + +FORCE: + +$(BUILD_DIR)/snake.o: snake.c ../../src/ascension/core.h \ + ../../src/ascension/error.h ../../src/ascension/context.h \ + ../../src/ascension/datatypes.h ../../src/ascension/window.h \ + ../../src/ascension/glcontext.h ../../src/ascension/scene.h \ + ../../src/ascension/scene_node.h ../../src/ascension/transform.h \ + ../../src/ascension/camera.h ../../src/ascension/input.h \ + ../../src/ascension/behavior.h ../../src/ascension/ui.h \ + ../../src/ascension/text.h ../../src/ascension/font.h \ + ../../src/ascension/mesh.h ../../src/ascension/texture.h \ + ../../src/ascension/sprite.h ../../src/ascension/2d.h \ + ../../src/ascension/shader.h + @echo "Compiling $<" + $(CC) -o $@ $(CFLAGS) -c $< +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo/snake/shader/player.glsl Tue Aug 19 18:05:35 2025 +0200 @@ -0,0 +1,16 @@ +layout(location = 0) out vec4 diffuse; +in vec2 uvcoord; + +uniform sampler2D map_albedo; +uniform sampler2D map_color; +uniform vec4 color; + +void main(void) { + // TODO: use greyscale texture instead + vec4 color_map_pixel = texture(map_color, uvcoord); + if (color_map_pixel.a > 0) { + diffuse = color * color_map_pixel * texture(map_albedo, uvcoord); + } else { + diffuse = texture(map_albedo, uvcoord); + } +}
--- /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; +} +
--- a/test/snake/Makefile Mon Aug 18 23:11:50 2025 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ -# 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 ../../config.mk - -BUILD_DIR=$(srcdir)/build/snake -LIB_ASCENSION=$(srcdir)/build/lib/libascension.a -CFLAGS += -I$(srcdir)/src $(CPPFLAGS) - -all: $(BUILD_DIR) $(BUILD_DIR)/snake FORCE - @echo "Demo game 'snake' successfully built." - @cp -Ruf $(srcdir)/shader $(BUILD_DIR)/ - @cp -Ruf $(srcdir)/fonts $(BUILD_DIR)/ - @cp -Ruf textures $(BUILD_DIR)/ - @cp -Ruf shader $(BUILD_DIR)/ - @echo "Assets for demo game 'snake' successfully copied." - -$(BUILD_DIR): - mkdir -p $@ - -$(BUILD_DIR)/snake: $(BUILD_DIR)/snake.o $(LIB_ASCENSION) - @echo "Linking snake..." - $(CC) $(LDFLAGS) -o $@ $^ - -$(LIB_ASCENSION): - test -f "$@" - -FORCE: - -$(BUILD_DIR)/snake.o: snake.c ../../src/ascension/core.h \ - ../../src/ascension/error.h ../../src/ascension/context.h \ - ../../src/ascension/datatypes.h ../../src/ascension/window.h \ - ../../src/ascension/glcontext.h ../../src/ascension/scene.h \ - ../../src/ascension/scene_node.h ../../src/ascension/transform.h \ - ../../src/ascension/camera.h ../../src/ascension/input.h \ - ../../src/ascension/behavior.h ../../src/ascension/ui.h \ - ../../src/ascension/text.h ../../src/ascension/font.h \ - ../../src/ascension/mesh.h ../../src/ascension/texture.h \ - ../../src/ascension/sprite.h ../../src/ascension/2d.h \ - ../../src/ascension/shader.h - @echo "Compiling $<" - $(CC) -o $@ $(CFLAGS) -c $< -
--- a/test/snake/shader/player.glsl Mon Aug 18 23:11:50 2025 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -layout(location = 0) out vec4 diffuse; -in vec2 uvcoord; - -uniform sampler2D map_albedo; -uniform sampler2D map_color; -uniform vec4 color; - -void main(void) { - // TODO: use greyscale texture instead - vec4 color_map_pixel = texture(map_color, uvcoord); - if (color_map_pixel.a > 0) { - diffuse = color * color_map_pixel * texture(map_albedo, uvcoord); - } else { - diffuse = texture(map_albedo, uvcoord); - } -}
--- 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; -} -