move the player's life "controller" to a behavior

Fri, 22 Aug 2025 23:27:36 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 22 Aug 2025 23:27:36 +0200
changeset 274
ba7f043f9fdf
parent 273
966bfca56b9d
child 275
2256af1440db

move the player's life "controller" to a behavior

demo/snake/snake.c file | annotate | diff | comparison | revisions
src/ascension/behavior.h file | annotate | diff | comparison | revisions
src/behavior.c file | annotate | diff | comparison | revisions
--- a/demo/snake/snake.c	Thu Aug 21 22:13:51 2025 +0200
+++ b/demo/snake/snake.c	Fri Aug 22 23:27:36 2025 +0200
@@ -72,8 +72,9 @@
     /**
      * The new position of the player when @c reset_position is @c true.
      */
-    asc_vec2i new_position;
+    asc_vec2u new_position;
     unsigned health;
+    bool alive;
     bool reset_position;
     uint8_t number;
 } Player;
@@ -272,25 +273,46 @@
     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 void player_position(Player *pl, unsigned x, unsigned y) {
+    pl->new_position.x = x;
+    pl->new_position.y = y;
+    pl->reset_position = true;
+}
+
+static void player_position_random(Player *pl) {
+    // TODO: check if the spawn location is viable when there is more action on the board
+    player_position(
+        pl,
+        4u + asc_util_rand(GAME_FIELD_SIZE - 8u),
+        4u + asc_util_rand(GAME_FIELD_SIZE - 8u)
+    );
 }
 
-static unsigned player_get_health(Player *player) {
-    return player->health;
+static void player_main_behavior(AscBehavior *behavior) {
+    AscSceneNode *node = behavior->node;
+    Player *player = node->user_data;
+
+    if (player->alive && player->health == 0) {
+        player->alive = false;
+        asc_scene_node_hide(node);
+        // TODO: probably we don't want the entire trace to disappear instantly
+        cxListClear(player->trace);
+        // TODO: this should be controlled by another behavior that watches all players
+        game.state = GAME_STATE_GAME_OVER;
+    }
+
+    // TODO: replace with respawn event
+    if (!player->alive && player->health > 0) {
+        player_position_random(player);
+        player->alive = true;
+        asc_scene_node_show(node);
+    }
 }
 
 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
@@ -354,12 +376,8 @@
     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;
+        // TODO: replace setting health to zero with a "kill" event
+        player->health = 0;
         return;
     }
 
@@ -383,9 +401,6 @@
 }
 
 static void player_controls(AscBehavior *behavior) {
-    // TODO: instead of checking the game state, disable the behavior when the player is dead
-    if (game.state != GAME_STATE_PLAYING) return;
-
     Player *player = behavior->node->user_data;
     // TODO: different key bindings for different player number in hot seat mode
     if (asc_key_pressed(ASC_KEY(LEFT))) {
@@ -410,21 +425,6 @@
     }
 }
 
-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_position_random(Player *pl) {
-    // TODO: check if the spawn location is viable when there is more action on the board
-    player_position(
-        pl,
-        4 + asc_util_rand(GAME_FIELD_SIZE - 8),
-        4 + asc_util_rand(GAME_FIELD_SIZE - 8)
-    );
-}
-
 static void player_destroy(CxAllocator *allocator, Player *player) {
     cxListFree(player->trace);
     cxFree(allocator, player);
@@ -444,7 +444,6 @@
     );
     asc_scene_add_node(MAIN_SCENE, node);
     Player *player = asc_scene_node_allocate_data(node, sizeof(Player));
-    player_position_random(player);
     player->speed = 3.f; // start with 3 tiles/sec
     player->number = 1;
     player->health = 100;
@@ -453,9 +452,13 @@
     cxDefineDestructor(player->trace, player_trace_release_tile);
     node->draw_func = player_draw;
     node->user_data_free_func = (cx_destructor_func2)player_destroy;
-    // add behaviors (the order is important!)
+
+    // add behaviors
+    asc_behavior_add(node, player_main_behavior, .always_enabled = true);
     asc_behavior_add(node, player_controls);
     asc_behavior_add(node, player_move);
+    asc_behavior_pause_all_while_hidden(node);
+
     return player;
 }
 
@@ -580,9 +583,9 @@
         if (game.state == GAME_STATE_GAME_OVER) {
             asc_scene_node_show(text_game_over);
             if (asc_key_pressed(ASC_KEY(R))) {
+                // TODO: instead of setting the health, send a "respawn" event to the behavior
+                game.players[0]->health = 100;
                 // TODO: re-load the "level"
-                player_position_random(game.players[0]);
-                player_set_health(game.players[0], 100);
                 game.state = GAME_STATE_PLAYING;
                 asc_scene_node_hide(text_game_over);
             }
--- a/src/ascension/behavior.h	Thu Aug 21 22:13:51 2025 +0200
+++ b/src/ascension/behavior.h	Fri Aug 22 23:27:36 2025 +0200
@@ -45,6 +45,7 @@
     uint64_t interval;
     cxmutstr name;
     bool pause_while_hidden;
+    bool always_enabled;
     bool enabled;
     bool killed;
 };
@@ -75,6 +76,17 @@
      * @see asc_scene_node_hide()
      */
     bool pause_while_hidden;
+    /**
+     * Set to true if the behavior shall start disabled.
+     */
+    bool start_disabled;
+    /**
+     * Behavior that cannot be disabled or paused (even by asc_behavior_disable_all()).
+     *
+     * Useful if you want to create a behavior that is always running (for example, a watchdog),
+     * even when the node is hidden and/or all (other) behaviors are disabled.
+     */
+    bool always_enabled;
 };
 
 AscBehavior *asc_behavior_add_(AscSceneNode *node, struct asc_behavior_create_args args);
@@ -122,7 +134,7 @@
  * @param behavior the behavior to disable
  */
 static inline void asc_behavior_disable(AscBehavior *behavior) {
-    behavior->enabled = false;
+    behavior->enabled = behavior->always_enabled;
 }
 
 /**
--- a/src/behavior.c	Thu Aug 21 22:13:51 2025 +0200
+++ b/src/behavior.c	Fri Aug 22 23:27:36 2025 +0200
@@ -58,9 +58,11 @@
     cxmutstr name = args.name == NULL
         ? asc_util_gen_name("behavior")
         : cx_mutstr(strdup(args.name));
+    // FIXME: we need the ordered map here, because the execution order of behaviors is important
     AscBehavior *behavior = cxMapEmplace(node->behaviors, name);
     assert(behavior != NULL);
-    behavior->enabled = true;
+    behavior->enabled = !args.start_disabled;
+    behavior->always_enabled = args.always_enabled;
     behavior->killed = false;
     behavior->node = node;
     behavior->func = args.func;
@@ -83,7 +85,7 @@
 void asc_behavior_trigger(AscBehavior *behavior) {
     if (!behavior->enabled) return;
     if (behavior->last_execution + behavior->interval > asc_context.total_nanos) return;
-    if (behavior->pause_while_hidden && asc_scene_node_is_hidden(behavior->node)) return;
+    if (!behavior->always_enabled && behavior->pause_while_hidden && asc_scene_node_is_hidden(behavior->node)) return;
 
     behavior->func(behavior);
     behavior->last_execution = asc_context.total_nanos;
@@ -99,27 +101,27 @@
 void asc_behavior_enable_all(AscSceneNode *node) {
     CxMapIterator iter = cxMapIteratorValues(node->behaviors);
     cx_foreach(AscBehavior*, behavior, iter) {
-        behavior->enabled = true;
+        asc_behavior_enable(behavior);
     }
 }
 
 void asc_behavior_disable_all(AscSceneNode *node) {
     CxMapIterator iter = cxMapIteratorValues(node->behaviors);
     cx_foreach(AscBehavior*, behavior, iter) {
-        behavior->enabled = false;
+        asc_behavior_disable(behavior);
     }
 }
 
 void asc_behavior_pause_all_while_hidden(AscSceneNode *node) {
     CxMapIterator iter = cxMapIteratorValues(node->behaviors);
     cx_foreach(AscBehavior*, behavior, iter) {
-        behavior->pause_while_hidden = true;
+        asc_behavior_pause_while_hidden(behavior);
     }
 }
 
 void asc_behavior_continue_all_while_hidden(AscSceneNode *node) {
     CxMapIterator iter = cxMapIteratorValues(node->behaviors);
     cx_foreach(AscBehavior*, behavior, iter) {
-        behavior->pause_while_hidden= false;
+        asc_behavior_continue_while_hidden(behavior);
     }
 }

mercurial