PVG
18. Loading Enemy From Ldtk — Program Video Games

18. Loading Enemy From Ldtk

[[programvideogames]]First up, we're tweaking our behavior.odin file. We're making a small optimisation in our raycast checks. Instead of capturing the hits variable that we're not using, we're replacing it with an underscore. This is a common pattern in Odin (and other languages) to ignore a returned value we don't need.

// Before
if hits, ok := raycast(...)

// After
if _, ok := raycast(...)

Now, let's move on to the more substantial changes in our main.odin file. We're expanding our LDtk integration to handle more complex entity data.

First, we're importing the log package. This will give us some better debugging tools:

import "core:log"

We're also updating our LDtk_Entity struct to include more fields:

LDtk_Entity :: struct {
    __identifier:   string,
    __worldX:       f32,
    __worldY:       f32,
    __tags:         []string,
    width, height:  f32,
    fieldInstances: []LDtk_Field_Instance,
}

This change allows us to capture more data from our LDtk files, including tags and custom fields. To handle these custom fields, we're adding a few new structs:

LDtk_Field_Instance :: struct {
    __identifier: string,
    __type:       string,
    __value:      LDtk_Field_Instance_Value,
}

LDtk_Field_Instance_Value :: union {
    LDtk_Entity_Ref,
    bool,
    f32,
    int,
}

LDtk_Entity_Ref :: struct {
    entityIid, layerIid, levelIid, worldIid: string,
}

The union type for LDtk_Field_Instance_Value allows us to handle different types of values that might be stored in our custom fields.

In our main function, we're updating how we handle JSON parsing errors. Instead of just printing the error and returning, we're now using log.panicf to provide more detailed error information:

if err != nil {
    log.panicf("failed to parse json: %v", err)
}

Finally we're adding support for enemy entities defined in our LDtk file:

if slice.contains(entity.__tags, "Enemy") {
    enemy := Entity {
        x           = entity.__worldX,
        y           = entity.__worldY,
        width       = entity.width,
        height      = entity.height,
        flags       = {.Debug_Draw},
        debug_color = rl.RED,
    }

    for field_instance in entity.fieldInstances {
        switch field_instance.__identifier {
        case "Move_Speed":
            enemy.move_speed = field_instance.__value.(f32)
        case "Health":
            enemy.health = int(field_instance.__value.(f32))
        case "EB_Walk":
            if field_instance.__value.(bool) do enemy.behaviors += {.Walk}
        case "EB_Flip_At_Wall":
            if field_instance.__value.(bool) do enemy.behaviors += {.Flip_At_Wall}
        case "EB_Flip_At_Edge":
            if field_instance.__value.(bool) do enemy.behaviors += {.Flip_At_Edge}
        }
    }

    entity_create(enemy)
}

This code checks if an entity has the "Enemy" tag. If it does, we create a new Enemy entity and populate its fields based on the data from LDtk. We're using custom fields to set things like move speed, health, and behaviors. This gives us a lot of flexibility in designing our enemies directly in LDtk.

The EB_ prefix on some fields stands for "Enemy Behavior". These are boolean flags that determine which behaviors an enemy should have. We're using our bitset-based behavior system here, adding behaviors to the enemy based on these flags.