Expanding the World Map Editor Mode

Hello, Spellweavers!

I’m back with another update on the development of Spellweaver Saga: Retrieving The Legendary Relic. Over the past week, I’ve been focusing on enhancing the editor, particularly in creating and managing world maps. Let’s dive into new features that have been added!

Creating and Loading World Maps

One of the advancements is the ability to create and load world maps directly within the editor. Now, you can easily start a new map or load an existing one, making the process of building your game world more efficient. Once you’ve selected the map you’ll be working on, you can choose tilesets loaded with the asset file, giving you full control over the terrain and environment.

Enhanced Tile Selection with Scrollable Window

To make tile placement more intuitive, I’ve introduced a scrollable tile selection window that appears at the top of the screen. You can now easily browse through your tilesets by scrolling the mouse wheel, making it quicker to find the right tile for the job.

Introducing Tile Layers for Better Sorting

To improve the visual organization and layering of tiles, I’ve implemented tile layers with different Z coordinates. Think of it as a z-buffer for tiles—this addition enhances the sorting of tiles, ensuring that everything displays correctly in the game environment.

Auto-Saving and Map Bounds Checking

Building a robust and user-friendly editor means thinking ahead about potential issues. To prevent any mishaps, I’ve added an auto-save feature and additional checks for map bounds. This ensures that your progress is saved regularly, and any work outside the map’s limits is caught early.

Tile Flood Fill Functionality

One of the most exciting features is the tile flood fill function, which allows you to fill large areas with a specific tile easily. Here’s a peek into the code that makes it all happen:

internal void
TileFloodFill(action_stack *UndoStack, world *World, world_position StartP, ssa_tile NewTile, u32 ZLayer)
{
    sswm_ground_tile *TargetGroundTiles = GetWorldMapGroundTile(World, StartP);
    if(TargetGroundTiles)
    {
        u32 TargetChecksum = TargetGroundTiles->CheckSum[ZLayer];
        if(TargetChecksum != NewTile.CheckSum)
        {
            editor_action FloodFillAction = {};
            FloodFillAction.Type = Action_FloodFill;
            FloodFillAction.FloodFill.TileCount = 0;
            FloodFillAction.FloodFill.PrevTiles = (sswm_ground_tile *)Platform.AllocateMemory(MAX_FILL_TILES*sizeof(sswm_ground_tile));

            tile_queue Queue = {};
            Queue.Front = 0;
            Queue.Rear = -1;
            EnQueue(&Queue, StartP);

            while(!IsEmpty(&Queue))
            {
                world_position P = DeQueue(&Queue);

                sswm_ground_tile *GroundTile = GetWorldMapGroundTile(World, P);
                if(GroundTile)
                {
                    if(GroundTile->CheckSum[ZLayer] == TargetChecksum)
                    {
                        Copy(sizeof(sswm_ground_tile), GroundTile, FloodFillAction.FloodFill.PrevTiles + FloodFillAction.FloodFill.TileCount++);

                        AddGroundTile(UndoStack, World, P, NewTile, ZLayer, true);
                        
                        if(TileIsValid(World, P.TileX - 1, P.TileY))
                        {
                            EnQueue(&Queue, CenteredTilePoint(World, P.TileX - 1, P.TileY));
                        }

                        if(TileIsValid(World, P.TileX + 1, P.TileY))
                        {
                            EnQueue(&Queue, CenteredTilePoint(World, P.TileX + 1, P.TileY));
                        }

                        if(TileIsValid(World, P.TileX, P.TileY - 1))
                        {
                            EnQueue(&Queue, CenteredTilePoint(World, P.TileX, P.TileY - 1));
                        }

                        if(TileIsValid(World, P.TileX, P.TileY + 1))
                        {
                            EnQueue(&Queue, CenteredTilePoint(World, P.TileX, P.TileY + 1));
                        }
                    }
                }
            }

            PushOnActionStack(UndoStack, FloodFillAction);
        }
    }
}

This function not only simplifies the tile placement process but also supports undo actions, allowing for greater flexibility and creativity while building your world.

Implementing Undo and Redo Functionality

After spending some time working with these new tools, I realized how crucial it was to have the ability to undo and redo actions. To address this, I introduced an action_stack system that can store and manage actions, allowing you to revert or reapply them as needed. Here’s a look at how this system works:

#define MAX_UNDO_ACTIONS 128
struct action_stack
{
    editor_action *Actions;
    s32 Start;
    s32 End;
    u32 Size;
};

This action_stack structure holds an array of editor_action objects, as well as pointers to the start and end of the stack, and a size indicator. It can store up to 128 actions for undoing and redoing.

struct editor_action
{
    action_type Type;
    union
    {
        sswm_ground_tile PrevTile;
        flood_fill_action FloodFill;
    };
};

The editor_action structure includes the type of action and a union to hold the relevant data for undoing or redoing the action. This allows for flexibility in handling different types of actions, whether it’s a simple tile change or a complex flood fill.

Here are a few utility functions that manage the stack:

inline void
InitActionStack(action_stack *Stack, memory_arena *Arena)
{
    Stack->Actions = PushArray(Arena, MAX_UNDO_ACTIONS, editor_action);
    Stack->Start = 0;
    Stack->End = 0;
    Stack->Size = 0;
}

InitActionStack initializes the stack, setting up the memory for storing actions and resetting the start, end, and size indicators.

inline b32
IsActionStackEmpty(action_stack *Stack)
{
    b32 Result = (Stack->Size == 0);
    return(Result);
}

IsActionStackEmpty checks if the stack is empty by evaluating the size.

internal void
PushOnActionStack(action_stack *Stack, editor_action Action)
{
    Stack->Actions[Stack->End] = Action;
    Stack->End = (Stack->End + 1) % MAX_UNDO_ACTIONS;

    if(Stack->Size < MAX_UNDO_ACTIONS)
    {
        ++Stack->Size;
    }
    else
    {
        Stack->Start = (Stack->Start + 1) % MAX_UNDO_ACTIONS;
    }
}

PushOnActionStack adds a new action to the stack. If the stack is full, it overwrites the oldest action.

internal editor_action *
PopFromActionStack(action_stack *Stack)
{
    editor_action *Result = 0;
    if(!IsActionStackEmpty(Stack))
    {
        Stack->End = (Stack->End - 1 + MAX_UNDO_ACTIONS) % MAX_UNDO_ACTIONS;
        --Stack->Size;

        Result = Stack->Actions + Stack->End;
    }

    return(Result);
}

PopFromActionStack removes the most recent action from the stack, allowing it to be undone.

inline void
ClearStack(action_stack *Stack)
{
    ZeroArray(MAX_UNDO_ACTIONS, Stack->Actions);
    Stack->Start = 0;
    Stack->End = 0;
    Stack->Size = 0;
}

Finally, ClearStack resets the entire stack, clearing all stored actions.

What’s Next?

The journey doesn’t end here! I’m already working on implementing navigation meshes and will continue refining the editor to enhance usability and expand its capabilities. My goal is to make building the world of Spellweaver Saga as intuitive and enjoyable as possible. There is a little gallery below, you can check it out.

Happy spellweaving, BabyKaban




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • The Road to Rewriting Clipper2
  • Advancing Navigation Meshes and Triangulation
  • Triangle Boolean Subtraction
  • Introducing Spellweaver Saga World Map (SSWM)
  • Welcome to the Spellweaver Saga Blog!