PVG
8. Enemy Behaviors — Program Video Games

8. Enemy Behaviors

[[programvideogames]]If you have experience with using game engines, you may think of Entity Behaviors as programmable scripts that you can attach to entities.

We could implement something like that, but it doesn't really make sense for our game to be so generalised.

Instead, let's consider what kind of behaviors we want in a Metrdoivania style game.

![[Pasted image 20240904110550.png]]

Take this enemy from Hollow Knight. It walks in one direction, hits a wall or an edge, then turns around and walks in the other direction.

We could use scripting to achieve this result. What happens if we want one enemy to walk off the ledge rather than turn around? As is the case with Goombas in Mario.

We'd probably create extra functions like flip_at_edge and turn_around_at_wall.

Now, I'm not against this solution - it will work just fine.

However, I think the using composable behavior flags is more reliable and once we have a few set up, it makes creating new enemy types trivial.

Let's create this enemy that flips at the edge and the wall.

First, create a new struct, I put it in main.odin for now.

Then add a new bit_set to Entity.

Entity_Behaviors :: enum {
    Walk,
    Flip_At_Wall,
    Flip_At_Edge,
}

Entity :: struct {
  // ...
    behaviors:                  bit_set[Entity_Behaviors],
}

This allows us to turn on or off behaviors using entity.behaviors += {.Walk}.

Now, let's add the entity to our level:

I've included only the bottom of the level here.

#                     #########        #
##                  e #vvv#            #
#>                                     #
#>                               ###   #
#>                               ###   #
###########^^^^^^#######################
########################################

For now, we'll use an e to denote this enemy.

Let's parse it:

// In our level loading switch
            case 'e':
                entity_create(
                    Entity {
                        collider = Rect{x, y, TILE_SIZE, TILE_SIZE},
                        move_speed = 50,
                        flags = {.Debug_Draw},
                        behaviors = {.Walk, .Flip_At_Wall, .Flip_At_Edge},
                        debug_color = rl.RED,
                    },
                )

If you run the game now, you'll see the enemy spawn and sit there.

Even though we gave it move_speed, it doesn't yet walk.

Let's fix that.

Create a new file src/behavior.odin:

package main

behavior_update :: proc(entities: []Entity, static_colliders: []Rect, dt: f32) {
    for &e in entities {
        if .Walk in e.behaviors {
            if .Left in e.flags {
                e.vel.x = -e.move_speed
            } else {
                e.vel.x = e.move_speed
            }
        }

        if .Flip_At_Wall in e.behaviors {
            if .Left in e.flags {
                if hits, ok := raycast(
                    {e.x + e.width / 2, e.y + e.height / 2},
                    {-e.width / 2 - COLLISION_EPSILON, 0},
                    static_colliders,
                ); ok {
                    e.flags -= {.Left}
                    e.vel.x = 0
                }
            } else {
                if hits, ok := raycast(
                    {e.x + e.width / 2, e.y + e.height / 2},
                    {e.width / 2 + COLLISION_EPSILON, 0},
                    static_colliders,
                ); ok {
                    e.flags += {.Left}
                    e.vel.x = 0
                }
            }
        }

        // Grounded check because otherwise the direction will flip every
        // tick while falling
        if .Flip_At_Edge in e.behaviors && .Grounded in e.flags {
            if .Left in e.flags {
                start := Vec2{e.x, e.y + e.height / 2}
                magnitude := Vec2{0, e.height / 2 + COLLISION_EPSILON}
                if hits, ok := raycast(start, magnitude, static_colliders); !ok {
                    e.flags -= {.Left}
                    e.vel.x = 0
                }
            } else {
                start := Vec2{e.x + e.width, e.y + e.height / 2}
                magnitude := Vec2{0, e.height / 2 + COLLISION_EPSILON}
                if hits, ok := raycast(start, magnitude, static_colliders); !ok {
                    e.flags += {.Left}
                    e.vel.x = 0
                }

            }
        }
    }
}

We've used a couple of things that don't yet exist, let's create them in physics.odin:

COLLISION_EPSILON :: 0.01

raycast :: proc(
    start, magnitude: Vec2,
    targets: []Rect,
    allocator := context.temp_allocator,
) -> (
    hits: []Vec2,
    ok: bool,
) {
    hit_store := make([dynamic]Vec2, allocator)

    for t in targets {
        p, q, r, s: Vec2 =
            {t.x, t.y},
            {t.x, t.y + t.height},
            {t.x + t.width, t.y + t.height},
            {t.x + t.width, t.y}
        lines := [4][2]Vec2{{p, q}, {q, r}, {r, s}, {s, p}}
        for line in lines {
            point: Vec2
            if rl.CheckCollisionLines(start, start + magnitude, line[0], line[1], &point) {
                append(&hit_store, point)
            }
        }

        color := len(hit_store) > 0 ? rl.GREEN : rl.YELLOW
        debug_draw_line(start, start + magnitude, 1, color)
    }

    return hit_store[:], len(hit_store) > 0
}

Finally, we need to call behavior_update. We'll do that in main.odin just after physics_update.

        physics_update(gs.entities[:], gs.solid_tiles[:], dt)
        behavior_update(gs.entities[:], gs.solid_tiles[:], dt)

Run the game now and you should see the enemy walking back and forth with the rays shooting out. One ahead and one down!