XGrid Framework

Primary Technologies

Unreal Engine

Unreal Engine

C++

C++

Visual Studio

Visual Studio

Overview

XGrid is designed to accelerate turn-based strategy development by focusing on three core goals: rapid content creation, fast level prototyping, and flexible, player-focused UI design. Each of these systems supports modular development and scalable gameplay design. XGrid is availble on the FAB marketplace. View XGrid FAB Page

Feature Demo

Primary Framework Classes

Grid

The Grid class is the nexus for all other classes in the framework. It allows for a dynamically sized 2D grid to render on top of 3D spaces. It provides visual feedback for facilitating player decision making. Each cell in the grid is represented by cell objects, which are created in the constructor of the Grid. This allows the Grid to render inside the editor window, allowing for powerful visual feedback on grid and zone placement.

Class Summary

  • Total Events: 12
  • Total Functions: 39
  • Total Variables: 24

Event Graph

Cell

The Cell class represents one slot of the Grid. It tracks various properties like occupying units, terrain data, traversibility, and more. It coordinates character unit movement between itself and other cells. It also contains functionality to display cell status visually to the player.

Class Summary

  • Total Events: 35
  • Total Functions: 22
  • Total Variables: 28

Event Graph

Tactical Player Controller

The Tactical Player Controller is the most sophistacted blueprint in the XGrid System. It is primarily responsible for controlling player units on the grid. It coordinates with the Grid, Character Unit, Game Manager, and Cinematic Combat Manager classes to facilitate player movement, combat, and game state management. It also facilitates the use of skills and items. It also facilitates smooth camera movement in line with player input and decision making. This player controller supports both PC and Gamepad controls.

Class Summary

  • Total Events: 60
  • Total Functions: 104
  • Total Variables: 108

Event Graph

Character Unit

The Character Unit class is a parent class to player and enemy unit classes. It processes information on unit stats, skills, items, and more. It contains animation events that allow the unit to move, cast, attack, and to use items and skills. It interacts with XGrid pathfinding to support complex movement on the grid.

Class Summary

  • Total Events: 57
  • Total Functions: 96
  • Total Variables: 110

Event Graph

Enemy Unit

The Enemy Unit class inherits from the Character Unit class and facilitates the creation and execution of action plans in order to produce intelligent enemy beavhior. The enemy AI combines targeting priorities with general behavior types in order to provide game designers with a large playbook of enemy behaviors to work with when designing encounters for the player to overcome. Targeting priorities and behavior types are enumerated, streamlining the creation of new AI strategies by developers.

Class Summary

  • Total Events: 15
  • Total Functions: 53
  • Total Variables: 30

Event Graph

Cinematic Manager

The Cinematic Manager facilitates the seamless transition between the grid view and the cinematic battlefield view when a player intiates a cinematic attack sequence. It also processes the flow between game states during a cinematic attack sequence so that the sequence displays cleanly and promptly returns to the tactical gird view when it is complete.

Class Summary

  • Total Events: 8
  • Total Functions: 2
  • Total Variables: 12

Event Graph

Battlefield

The Battlefield class allows developers to create individualized locations for cinematic combat sequences to play on. It controls the flow of combat between two character units. Cinematic attack sequences are unique to each character unit, but by careful use of overriding in the character unit class, the battlefield is able to facilitate dynamic combat sequences between any two character units on the battlefield.

Class Summary

  • Total Events: 16
  • Total Functions: 1
  • Total Variables: 19

Event Graph

XGrid Pathfinder

The C++ XGrid Pathfinder provides several variations of asynchronous A* Manhattan pathfinding. These variations can be used in conjunction with the primary pathfinding algorithm to reduce enemy unit clumping and encourage AI units to find paths with greater directional variety. This makes enemy pathfinding less predictable and improves strategical resiliance against players. This pathfinder is easily downloaded, unzipped, and configurable to XGrid instances.

Class Summary

  • Total Functions: 8

XGridPathfinder.h


#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ASync/Async.h"
#include "FNodeModule.h"
#include "Delegates/Delegate.h"
#include "XGridPathfinder.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnPathfindingCompleteDelegate, const TArray&, Path, float, Duration);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnPathfindingWithDensityCompleteDelegate, const TArray&, Path, float, Duration);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FOnOmnidirectionalPathfindingCompleteDelegate, const TArray&, DownPath, const TArray&, RightPath, const TArray&, LeftPath, const TArray&, UpPath, float, Duration);

UCLASS(Blueprintable)
class UXGridPathfinder : public UObject
{
  GENERATED_BODY()

public:
  UFUNCTION(BlueprintCallable, Category = "Pathfinding")
    TArray Pathfind(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, bool is_player_unit);

  UFUNCTION(BlueprintCallable, Category = "Pathfinding")
    void AsyncPathfind(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, bool is_player_unit);

  UPROPERTY(BlueprintAssignable, Category = "Pathfinding")
    FOnPathfindingCompleteDelegate OnPathfindingComplete;

  UPROPERTY(BlueprintAssignable, Category = "Pathfinding")
    FOnPathfindingWithDensityCompleteDelegate OnPathfindingWithDensityComplete;

  UPROPERTY(BlueprintAssignable, Category = "Pathfinding")
    FOnOmnidirectionalPathfindingCompleteDelegate OnOmnidirectionalPathfindComplete;

  UFUNCTION(BlueprintCallable, Category = "Pathfinding")
    TArray PathfindWithDensityMap(const TArray& grid, const TArray& DensityMap, int32 Width, int32 Height, FVector2D start, FVector2D end, float Density_Weight, bool is_player_unit);

  UFUNCTION(BlueprintCallable, Category = "Pathfinding")
    void AsyncPathfindWithDensity(const TArray& grid, const TArray& DensityMap, int32 Width, int32 Height, FVector2D start, FVector2D end, float Density_Weight, bool is_player_unit);

  UFUNCTION(BlueprintCallable, Category = "Pathfinding")
    TArray PathfindWithDirectionalVariety(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, TArray directions, bool is_player_unit);

  UFUNCTION(BlueprintCallable, Category = "Pathfinding")
    void AsyncPathfindWithDirectionalVariety(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, TArray directions, bool is_player_unit);

  UFUNCTION(BlueprintCallable, Category = "Pathfinding")
    void AsynchOmnidirectionalPathfind(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, bool is_player_unit);
};
          

XGridPathfinder.cpp


            #include "XGridPathfinder.h"

            // Heuristic Function (Manhattan Distance)
            float ManhattanDistanceEq(const FVector2D& A, const FVector2D& B)
            {
                return FMath::Abs(A.X - B.X) + FMath::Abs(A.Y - B.Y);
            }

            TArray UXGridPathfinder::Pathfind(const TArray& grid, int32 width, int32 height, FVector2D start, FVector2D end, bool is_player_unit)
            {
                TSet> OpenSet;
                TMap> ClosedSet;
                TSharedPtr StartNode = MakeShareable(new FNodeModule(start));
                TSharedPtr EndNode = MakeShareable(new FNodeModule(end));
                StartNode->G = 0;
                StartNode->H = ManhattanDistanceEq(start, end);
                OpenSet.Add(StartNode);

                while (OpenSet.Num() > 0)
                {
                    TSharedPtr CurrentNode = *OpenSet.CreateConstIterator();
                    for (const TSharedPtr& Node : OpenSet)
                    {
                        if (Node->F() < CurrentNode->F())
                        {
                            CurrentNode = Node;
                        }
                    }

                    if (CurrentNode->Position == EndNode->Position)
                    {
                        TArray Path;
                        while (CurrentNode->Parent.IsValid())
                        {
                            Path.Add(CurrentNode->Position);
                            CurrentNode = CurrentNode->Parent;
                        }
                        Algo::Reverse(Path);
                        return Path;
                    }

                    OpenSet.Remove(CurrentNode);
                    ClosedSet.Add(CurrentNode->Position, CurrentNode);

                    TArray Directions = { FVector2D(0, -1), FVector2D(1, 0), FVector2D(0, 1), FVector2D(-1, 0) };
                    for (const FVector2D& Direction : Directions)
                    {
                        FVector2D ChildPosition = CurrentNode->Position + Direction;
                        int32 Index = static_cast(ChildPosition.Y) * width + static_cast(ChildPosition.X);


                        int occupied = 0;
                        if (is_player_unit)
                            occupied = 1;
                        else
                            occupied = 2;

                        if (ChildPosition.X >= 0 && ChildPosition.X < width &&
                            ChildPosition.Y >= 0 && ChildPosition.Y < height &&
                            (grid[Index] == 0 || grid[Index] == occupied))
                        {
                            TSharedPtr ChildNode = MakeShareable(new FNodeModule(ChildPosition));
                            ChildNode->Parent = CurrentNode;

                            if (ClosedSet.Contains(ChildPosition))
                            {
                                continue;
                            }

                            float TentativeG = CurrentNode->G + 1;

                            if (OpenSet.Contains(ChildNode))
                            {
                                for (TSharedPtr& Node : OpenSet)
                                {
                                    if (*Node == *ChildNode && TentativeG < Node->G)
                                    {
                                        Node->G = TentativeG;
                                        Node->Parent = CurrentNode;
                                        Node->H = ManhattanDistanceEq(ChildPosition, end);
                                    }
                                }
                            }
                            else
                            {
                                ChildNode->G = TentativeG;
                                ChildNode->H = ManhattanDistanceEq(ChildPosition, end);
                                OpenSet.Add(ChildNode);
                            }
                        }
                    }
                }

                return {};
            }

            void UXGridPathfinder::AsyncPathfind(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, bool is_opponent)
            {
                double TimeInitial = FPlatformTime::Seconds();
                Async(EAsyncExecution::ThreadPool, [this, grid, Width, Height, start, end, is_opponent, TimeInitial]()
                    {

                        TArray Path = Pathfind(grid, Width, Height, start, end, is_opponent);
                        //FVector2D Start = Path[0];

                        double TimeAfter = FPlatformTime::Seconds();
                        double Duration = TimeAfter - TimeInitial;

                        // Switch back to the main thread to call the Blueprint event
                        AsyncTask(ENamedThreads::GameThread, [this, Path, Duration]()
                            {
                                OnPathfindingComplete.Broadcast(Path, Duration);
                            });
                    });

            }

            TArray UXGridPathfinder::PathfindWithDensityMap(const TArray& grid, const TArray& densityMap, int32 Width, int32 Height, FVector2D start, FVector2D end, float Density_Weight, bool is_player_unit)
            {

                TSet> OpenSet;
                TMap> ClosedSet;
                TSharedPtr StartNode = MakeShareable(new FNodeModule(start));
                TSharedPtr EndNode = MakeShareable(new FNodeModule(end));
                StartNode->G = 0;
                StartNode->H = ManhattanDistanceEq(start, end);
                OpenSet.Add(StartNode);

                while (OpenSet.Num() > 0)
                {
                    TSharedPtr CurrentNode = *OpenSet.CreateConstIterator();
                    for (const TSharedPtr& Node : OpenSet)
                    {
                        if (Node->F() < CurrentNode->F())
                        {
                            CurrentNode = Node;
                        }
                    }

                    if (CurrentNode->Position == EndNode->Position)
                    {
                        TArray Path;
                        while (CurrentNode->Parent.IsValid())
                        {
                            Path.Add(CurrentNode->Position);
                            CurrentNode = CurrentNode->Parent;
                        }
                        Algo::Reverse(Path);
                        return Path;
                    }

                    OpenSet.Remove(CurrentNode);
                    ClosedSet.Add(CurrentNode->Position, CurrentNode);

                    TArray Directions = { FVector2D(0, -1), FVector2D(1, 0), FVector2D(0, 1), FVector2D(-1, 0) };
                    for (const FVector2D& Direction : Directions)
                    {
                        FVector2D ChildPosition = CurrentNode->Position + Direction;
                        int32 Index = static_cast(ChildPosition.Y) * Width + static_cast(ChildPosition.X);

                        int occupied = is_player_unit ? 1 : 2;

                        if (ChildPosition.X >= 0 && ChildPosition.X < Width &&
                            ChildPosition.Y >= 0 && ChildPosition.Y < Height &&
                            (grid[Index] == 0 || grid[Index] == occupied))
                        {
                            TSharedPtr ChildNode = MakeShareable(new FNodeModule(ChildPosition));
                            ChildNode->Parent = CurrentNode;

                            if (ClosedSet.Contains(ChildPosition))
                            {
                                continue;
                            }

                            float TentativeG = CurrentNode->G + 1;
                            float DensityPenalty = densityMap[Index] * Density_Weight; // Add density score to the cost
                            TentativeG += DensityPenalty;

                            if (OpenSet.Contains(ChildNode))
                            {
                                for (TSharedPtr& Node : OpenSet)
                                {
                                    if (*Node == *ChildNode && TentativeG < Node->G)
                                    {
                                        Node->G = TentativeG;
                                        Node->Parent = CurrentNode;
                                        Node->H = ManhattanDistanceEq(ChildPosition, end);
                                    }
                                }
                            }
                            else
                            {
                                ChildNode->G = TentativeG;
                                ChildNode->H = ManhattanDistanceEq(ChildPosition, end);
                                OpenSet.Add(ChildNode);
                            }
                        }
                    }
                }

                return {};
            }

            void UXGridPathfinder::AsyncPathfindWithDensity(const TArray& grid, const TArray& DensityMap, int32 Width, int32 Height, FVector2D start, FVector2D end, float Density_Weight, bool is_player_unit)
            {
                double TimeInitial = FPlatformTime::Seconds();
                Async(EAsyncExecution::ThreadPool, [this, grid, DensityMap, Width, Height, start, end, Density_Weight, is_player_unit, TimeInitial]()
                    {

                        TArray Path = PathfindWithDensityMap(grid, DensityMap, Width, Height, start, end, Density_Weight, is_player_unit);
                        //FVector2D Start = Path[0];

                        double TimeAfter = FPlatformTime::Seconds();
                        double Duration = TimeAfter - TimeInitial;

                        // Switch back to the main thread to call the Blueprint event
                        AsyncTask(ENamedThreads::GameThread, [this, Path, Duration]()
                            {
                                OnPathfindingWithDensityComplete.Broadcast(Path, Duration);
                            });
                    });
            }

            TArray UXGridPathfinder::PathfindWithDirectionalVariety(const TArray& grid, int32 width, int32 height, FVector2D start, FVector2D end, TArray directions, bool is_player_unit)
            {
                TSet> OpenSet;
                TMap> ClosedSet;
                TSharedPtr StartNode = MakeShareable(new FNodeModule(start));
                TSharedPtr EndNode = MakeShareable(new FNodeModule(end));
                StartNode->G = 0;
                StartNode->H = ManhattanDistanceEq(start, end);
                OpenSet.Add(StartNode);

                while (OpenSet.Num() > 0)
                {
                    if (OpenSet.Num() > width * height)
                    {
                        UE_LOG(LogTemp, Warning, TEXT("Number of nodes in the open set exceeds maximum grid cells"));
                        return {};
                    }
                    TSharedPtr CurrentNode = *OpenSet.CreateConstIterator();
                    for (const TSharedPtr& Node : OpenSet)
                    {
                        if (Node->F() < CurrentNode->F())
                        {
                            CurrentNode = Node;
                        }
                    }

                    if (CurrentNode->Position == EndNode->Position)
                    {
                        TArray Path;
                        while (CurrentNode->Parent.IsValid())
                        {
                            Path.Add(CurrentNode->Position);
                            CurrentNode = CurrentNode->Parent;
                        }
                        Algo::Reverse(Path);
                        return Path;
                    }

                    OpenSet.Remove(CurrentNode);
                    ClosedSet.Add(CurrentNode->Position, CurrentNode);

                    TArray Directions = directions;
                    for (const FVector2D& Direction : Directions)
                    {
                        FVector2D ChildPosition = CurrentNode->Position + Direction;
                        int32 Index = static_cast(ChildPosition.Y) * width + static_cast(ChildPosition.X);


                        int occupied = 0;
                        if (is_player_unit)
                            occupied = 1;
                        else
                            occupied = 2;

                        if (ChildPosition.X >= 0 && ChildPosition.X < width &&
                            ChildPosition.Y >= 0 && ChildPosition.Y < height &&
                            (grid[Index] == 0 || grid[Index] == occupied))
                        {
                            TSharedPtr ChildNode = MakeShareable(new FNodeModule(ChildPosition));
                            ChildNode->Parent = CurrentNode;

                            if (ClosedSet.Contains(ChildPosition))
                            {
                                continue;
                            }

                            float TentativeG = CurrentNode->G + 1;

                            if (OpenSet.Contains(ChildNode))
                            {
                                for (TSharedPtr& Node : OpenSet)
                                {
                                    if (*Node == *ChildNode && TentativeG < Node->G)
                                    {
                                        Node->G = TentativeG;
                                        Node->Parent = CurrentNode;
                                        Node->H = ManhattanDistanceEq(ChildPosition, end);
                                    }
                                }
                            }
                            else
                            {
                                ChildNode->G = TentativeG;
                                ChildNode->H = ManhattanDistanceEq(ChildPosition, end);
                                OpenSet.Add(ChildNode);
                            }
                        }
                    }
                }

                return {};
            }

            void UXGridPathfinder::AsyncPathfindWithDirectionalVariety(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, TArray directions, bool is_opponent)
            {
                double TimeInitial = FPlatformTime::Seconds();
                Async(EAsyncExecution::ThreadPool, [this, grid, Width, Height, start, end, directions, is_opponent, TimeInitial]()
                    {

                        TArray Path = PathfindWithDirectionalVariety(grid, Width, Height, start, end, directions, is_opponent);
                        //FVector2D Start = Path[0];

                        double TimeAfter = FPlatformTime::Seconds();
                        double Duration = TimeAfter - TimeInitial;

                        // Switch back to the main thread to call the Blueprint event
                        AsyncTask(ENamedThreads::GameThread, [this, Path, Duration]()
                            {
                                OnPathfindingComplete.Broadcast(Path, Duration);
                            });
                    });

            }


            void UXGridPathfinder::AsynchOmnidirectionalPathfind(const TArray& grid, int32 Width, int32 Height, FVector2D start, FVector2D end, bool is_player_unit)
            {
                double TimeInitial = FPlatformTime::Seconds();
                Async(EAsyncExecution::ThreadPool, [this, grid, Width, Height, start, end, is_player_unit, TimeInitial]()
                    {

                        TArray DownFirst = { FVector2D(0, -1), FVector2D(1, 0), FVector2D(0, 1), FVector2D(-1, 0) };
                        TArray RightFirst = { FVector2D(1, 0), FVector2D(0, 1), FVector2D(-1, 0), FVector2D(0, -1) };
                        TArray LeftFirst = { FVector2D(-1, 0), FVector2D(0, -1), FVector2D(1, 0), FVector2D(0, 1) };
                        TArray UpFirst = { FVector2D(0, 1), FVector2D(-1, 0), FVector2D(0, -1), FVector2D(1, 0) };

                        TArray DownPath = PathfindWithDirectionalVariety(grid, Width, Height, start, end, DownFirst, is_player_unit);
                        TArray RightPath = PathfindWithDirectionalVariety(grid, Width, Height, start, end, RightFirst, is_player_unit);
                        TArray LeftPath = PathfindWithDirectionalVariety(grid, Width, Height, start, end, LeftFirst, is_player_unit);
                        TArray UpPath = PathfindWithDirectionalVariety(grid, Width, Height, start, end, UpFirst, is_player_unit);

                        double TimeAfter = FPlatformTime::Seconds();
                        double Duration = TimeAfter - TimeInitial;
                        
                        // Switch back to the main thread to call the Blueprint event
                        AsyncTask(ENamedThreads::GameThread, [this, DownPath, RightPath, LeftPath, UpPath, Duration]()
                            {
                                OnOmnidirectionalPathfindComplete.Broadcast(DownPath, RightPath, LeftPath, UpPath, Duration);
                            });
                    }); 
                    
            }
          

UI and Player Control Systems

To support meaningful player choices, XGrid offers a customizable UI and player control experience. Players can navigate available actions through a responsive and reversible interface, allowing them to review and backtrack decisions before committing to actions. This supports strategic play while maintaining clarity.

Asset Creation Pipelines

XGrid provides streamlined pipelines for building characters, items, skills, and AI behaviors. These pipelines are built to reduce repetition and accelerate iteration, allowing developers to prototype and expand game systems efficiently. The structure supports scalable asset integration that grows with your project.

Grid Zoning for Level Prototyping

The framework includes a zoning system that allows developers to use overlap volumes to define grid cell properties. This includes whether a cell is traversable, how terrain affects unit behavior, and whether the grid should snap to complex level geometry. This approach enables fast, visual prototyping of levels and mechanics without the need for complex custom logic.

Section is pending image assets.

Future sections coming soon~~~