UE4 2D combat framework plugin

Description

As someone who considers himself an avid fighting game player, it was inevitable to develop a fighting game system. I have set out to develop an easy to use and streamlined tool that can aid in the development and fast prototyping of 2D beat 'em ups and fighting games. Information about the requirements and how to create robust systems for these games is quite scarce. But I was able to find some resources that deemed very useful in my endeavour to create tools and an adequate combat system. You can read more about this further down the page.

If you are interested in the code, you can find the repositopry for this project here

 

Tech
  • C++
  • Unreal Engine 4
Work
  • Scripting engine for composing behaviors with instructions
  • Input buffer system for various input commands
  • Hitbox editor
  • Fighting game-esque physics system
  • Plugin for Unreal Engine 4
Team

Programming

  • Akli Lounès Touati
Details

Why develop scripting tools?

High-level scripting is very important in game development. It can enable people to create content for a game even if they are not comfortable with programming. It can also save you a lot of time. You want to be able to easily add, modify and remove logic to parts of your game without waiting for a compiler and having to think too much about it. this is especially true for beat 'em ups and fighting games. Characters in these games have a lot of states. In some games, the number of states per character is easily in the hundreds or even in the thousands. There is an average of 20 playable characters in fighting games. These characters need to be programmed and often fine-tuned on the frame. This process takes a lot of time. Doing this in a low-level language like C++ is very counter-intuitive.

Timelapse of me recreating part of Ryu's move-set in this framework in roughly an hour.

Why Unreal Engine 4?

I am aware that Unreal Engine 4 is mainly used for 3D game creation, but I noticed that many modern triple-A fighting games are made using Unreal Engine 4. This made me very curious about the engine and I wanted to learn more about it.  

The Input System

The input buffer

Input in a fighting game is very important. Players must be able to buffer input for leniency and complex commands. The tech for doing this is called an input buffer. An input buffer stores player input in a list. This list has to be parsed to deduce what action the player wants to do. It is kinda similar to pattern recognition, timing plays a huge role in an input buffer. You must differentiate between many different types of commands (Adjacent commands, charge commands, mixed command, etc). I have written tools and helper functions to define input conditions and parse the input buffer.

Screenshot_3

Easily define input conditions for actions.

buffer2
#include "Input/InputUtility.h"
#include "Input/Condition/InputCondition.h"
#include "Input/InputBuffer/BufferInputItem.h"


/// <summary>Converts the given input bitflag to fighting game direction notation</summary>
/// <param name="sel">Bitflag that represents the players input</param>  
/// <returns>DirectionNotation, Numpad representation of directional input</returns>  
DirectionNotation UInputUtility::ConvertInputBitflagToDirectionNotation(uint32 inputBitflag)
{
    //5+(raw horizontal axis)+(3*raw vertical axis) is the magic FG notation formula
    uint8 direction = 5;

    if (inputBitflag & (1 << (32 - ((int32)CardinalDirection::NUM - (int32)CardinalDirection::UP))))
        direction += 3;
    if (inputBitflag & (1 << (32 - ((int32)CardinalDirection::NUM - (int32)CardinalDirection::DOWN))))
        direction -= 3;
    if (inputBitflag & (1 << (32 - ((int32)CardinalDirection::NUM - (int32)CardinalDirection::LEFT))))
        direction -= 1;
    if (inputBitflag & (1 << (32 - ((int32)CardinalDirection::NUM - (int32)CardinalDirection::Right))))
        direction += 1;

    return (DirectionNotation)direction;
}

/// <summary>Converts the given input bitflag to a string representation</summary>
/// <param name="sel">Bitflag that represents the players input</param>  
/// <returns>String of 1's and 0's</returns> 
FString UInputUtility::ConvertInputBitflagToBinaryString(uint32 inputBitflag)
{
    std::bitset<32> x(inputBitflag);
    FString InputString;

    for (int i = 0; i < x.size(); i++)
    {
        InputString.AppendInt(x[i]);
    }

    return InputString;
}

uint32 UInputUtility::SwapBits(uint32 n, uint32 p1, uint32 p2)
{
    /* Move p1'th to rightmost side */
    uint32 bit1 = (n >> p1) & 1;

    /* Move p2'th to rightmost side */
    uint32 bit2 = (n >> p2) & 1;

    /* XOR the two bits */
    uint32 x = (bit1 ^ bit2);

    /* Put the xor bit back to their original positions */
    x = (x << p1) | (x << p2);

    /* XOR 'x' with the original number so that the
       two sets are swapped */
    uint32 result = n ^ x;

    return result;
}

/// <summary>Checks if the the given sequence is found within the given input buffer</summary>
/// <param name="sel">Reference to subject pawn, an input buffer to validate preferably one owned by the subject pawn and a pointer to the sequence to match with the input buffer</param>  
/// <returns>Bool, whether the input sequence was found withing the given input buffer</returns> 
bool UInputUtility::ValidateInputSequence(AFightPawn* refObject, const TArray<UBufferInputItem*>& inputBuffer, UInputSequence* sequence)
{
    if (refObject->GameInstance == nullptr)
        return false;

    if (sequence == nullptr)
        return true;
    
    //copy valid time between inputs from the settings in the game instance through the refObject
    float validTimeBetweenInputsInSequence = refObject->GameInstance->ValidTimeBetweenInputsInSequence;

    //copy valid hold time per input in sequence from the settings in the game instance through the refObject
    float validHoldTimePerInputInSequence = refObject->GameInstance->ValidHoldTimePerInputInSequence;

    //index input in the Sequence we are checking NOTE: we start at sequence->Sequence.Num() - 1 because we check the sequence in the buffer backwards starting at the most resent input 
    int currentSequenceItemIndex = sequence->Sequence.Num() - 1;

    //index of input item in the buffer from where we start checking for the sequence
    int inputBufferItemToCheck = FindFirstValidBufferItemIndex(refObject, inputBuffer, sequence->Sequence[currentSequenceItemIndex]);

    //if the input item to check is out of the range of the buffer then we will set this variable to the last index of the buffer else we will set this to the inputBufferItemToCheck
    UBufferInputItem* LastValidInputInBuffer = inputBufferItemToCheck > inputBuffer.Num() ? inputBuffer[inputBufferItemToCheck] : inputBuffer[inputBuffer.Num() - 1];

    //iterate through the input buffer from the first valid index in the input buffer 
    for (int i = inputBufferItemToCheck; i < inputBuffer.Num(); i++)
    {
        //if the input item to check is a nullptr than we skip it
        if (inputBuffer[i] == nullptr)
            continue;

        //if the input item to check has on of the forbidden Directions then the sequence is invalid this we stop looking for this sequence
        if (ValidateInputDirections(inputBuffer[i], sequence->ForbiddenDirectons) && sequence->ForbiddenDirectons.Num() > 0)
            break;

        if (ValidateInputDirection(inputBuffer[i], sequence->Sequence[currentSequenceItemIndex].RequiredDirecton))
        {
            if (currentSequenceItemIndex < sequence->Sequence.Num() - 1)
                //if the time difference between this input and the previous valid input in the buffer is greater than the validTimeBetweenInputsInSequence, then the sequence in the buffer is invalid so we break out of the loop
                if (inputBuffer[i]->GetTimeDifferenceWithOtherItem(LastValidInputInBuffer) > validTimeBetweenInputsInSequence)
                    break;

            if (sequence->Type == SequneceType::TAP)
            {
                if (ValidateInputState(inputBuffer[i]->DirectionAtom, InputState::PRESSED) == false)
                    break;

                if (i > 0)
                    if (inputBuffer[i - 1]->ReleasedDirectionAtom.HoldTime > validHoldTimePerInputInSequence)
                    {
                        GEngine->AddOnScreenDebugMessage(-1, 1.5, FColor::Emerald, FString::SanitizeFloat(inputBuffer[i - 1]->ReleasedDirectionAtom.HoldTime));
                        break;
                    }
            }
            else if (currentSequenceItemIndex > 0 && currentSequenceItemIndex < sequence->Sequence.Num() - 1 && i > 0)
            {
                if (inputBuffer[i - 1]->ReleasedDirectionAtom.HoldTime > validHoldTimePerInputInSequence)
                {
                    GEngine->AddOnScreenDebugMessage(-1, 1.5, FColor::Emerald, FString::SanitizeFloat(inputBuffer[i - 1]->ReleasedDirectionAtom.HoldTime));
                    break;
                }
            }

            if (currentSequenceItemIndex == 0)
                return true;

            currentSequenceItemIndex--;
            LastValidInputInBuffer = inputBuffer[i];
        }
    }

    return false;
}

/// <summary>Finds the index of the required input in the inbut buffer</summary>
/// <param name="sel">Reference to subject pawn, an input buffer to search for the first input preferably one owned by the subject pawn and the input stamp to search</param>  
/// <returns>int, Index at which the first valid input is found. if none is found the count to the input buffer is given to make for loops using this return value immediately exit their iteration</returns> 
int UInputUtility::FindFirstValidBufferItemIndex(AFightPawn* refObject, const TArray<UBufferInputItem*>& inputBuffer, FInputStamp inputStamp)
{    //copy motion life time from the settings in the game instance through the refObject
    float motionLifeTime = refObject->GameInstance->MotionLifeTime;

    //Iterate through the given input buffer
    for (int i = 0; i < inputBuffer.Num(); i++)
    {
        //cache the age of the current input item we are checking
        float inputAge = inputBuffer[i]->GetInputAgeFromBeginTime(UKismetSystemLibrary::GetGameTimeInSeconds(refObject));

        /*we return the index position of the input item in the buffer if the input item corresponds with the given input stamp (we only check direction ATM)
        and if the age of the current input is less then the motion life time*/
        if (ValidateInputDirection(inputBuffer[i], inputStamp.RequiredDirecton) &&
            inputAge < motionLifeTime)
        {
            return i;
        }
    }
    //if the input stamp did not correspond with any input item in the given buffer then we return the length of the buffer  
    return inputBuffer.Num();
}

/// <summary>checks if the given direction corresponds with the direction of the given input item</summary>
/// <param name="sel">input item to check, direction to check for</param>  
/// <returns>bool, weather the direction corresponds with the direction of the input item  </returns> 
bool UInputUtility::ValidateInputDirection(const UBufferInputItem* currentInput, const DirectionNotation requiredCurrentHoldDirection)
{
    //if the direction of current input corresponds with one of the valid direction we return true
    if (currentInput != nullptr)
        if (currentInput->DirectionAtom.Direction == requiredCurrentHoldDirection)
            return true;

    return false;
}

/// <summary>checks if  one of the the givens direction corrisponds with the direction of the given input item</summary>
/// <param name="sel">input item to check, directions to check for</param>  
/// <returns>bool, weather  one of the given directions corresponds with the direction of the input item  </returns> 
bool UInputUtility::ValidateInputDirections(const UBufferInputItem* currentInput, const TArray<DirectionNotation>& validHoldDirections)
{    //if no input directions are given then they are not required so we return true
    if (validHoldDirections.Num() == 0)
        return true;

    //iterates through the valid hold directions and if the direction of current input corresponds with one of the valid direction we return true
    for (DirectionNotation notation : validHoldDirections)
    {
        if (currentInput != nullptr)
            if (currentInput->DirectionAtom.Direction == notation)
            {
                return true;
            }
    }

    return false;
}

/// <summary>Checks weather the the given buttons are pressed in the given input item </summary>
/// <param name="sel">input buffer item to check, bottons to validate</param>  
/// <returns>bool, weather  one of the given directions corresponds with the direction of the input item  </returns> 
bool UInputUtility::ValidateInputButtons(const UBufferInputItem* currentInput, const TArray<Button>& requiredCurrentButtons)
{
    //create an empty bit mask
    uint32 bitmask = 0;
    // copy bitflag from input item
    uint32 currentInputBitFlag = currentInput->InputBitflag;

    //itereate through the given buttons and set the corresponding bits in the bitmak
    for (Button button : requiredCurrentButtons)
    {
        bitmask |= (1 << (uint32)button);
    }

    //sets the last 4 bits of currentInputBitFlag to 0 because we only want to check the bits that represent the buttons and not the cardinal directions 
    for (uint8 i = 0; i < (uint8)CardinalDirection::NUM; i++)
    {
        //set last 4 bits in currentInputBitFlag to 1.
        currentInputBitFlag |= (1 << (32 - (int32)CardinalDirection::NUM + (i)));
        //then flip the last 4 bits in currentInputBitFlag so that they become 0
        currentInputBitFlag ^= (1 << (32 - (int32)CardinalDirection::NUM + (i)));
    }

    return ((currentInputBitFlag & bitmask) == bitmask);
}

bool UInputUtility::ValidateInputState(const FInputAtom inputAtom, const InputState validButtonState)
{
    if (inputAtom.State == validButtonState)
        return true;

    return false;
}

/// <summary>Checks of the state of the given input atom is the same  </summary>
/// <param name="sel">input buffer item to check, bottons to validate</param>  
/// <returns>bool, weather  one of the given directions corresponds with the direction of the input item  </returns> 
bool UInputUtility::ValidateInputState(const FInputAtom inputAtom, const TArray<InputState>& validButtonStates)
{
    //if no valid button state is given then the valid state of PRESSED is assumed
    if (validButtonStates.Num() == 0)
    {
        if (inputAtom.State == InputState::PRESSED)
            return true;
        else
            return false;
    }

    //Iterate through the valid states
    for (InputState state : validButtonStates)
    {
        if (inputAtom.State == state)
        {
            return true;
        }
    }

    return false;
}

The Scripting

By now you have heard the term scripting quite a few times. For this framework, I have developed a scripting engine inspired by Mugen and the scripting engine used by Arc System Works (which you can learn about form this resource by dantarion). The behaviour of a state is composed of small pre-defined functions called instructions. UE4's blueprints are a good fit for defining these instructions. Instructions are stored inside tables. Tables define which instruction needs to be executed at which frame. When a character enters a new state it can query the table for which instruction to execute. Having all data separate makes it easy for characters to share behaviour. This is also the reason why you see lots of games have clone characters. You can easily develop characters based on other ones because the data already exists. No need to write new logic. Just adjust the already existing ones.

Screenshot_11

Instructions are blueprints. You can write any kind of logic you want and have it executed by a character.

Screenshot_5

The table editor for creating hitbox and behavior data.

States can be easily composed from tables and instructions.

The process

Beat 'em ups and fighting games look like very simple games to make, but looks can are very deceiving. It is true that these game do not have a large number of different systems, but rather have deep and dense systems that are intricately connected with each other. If something about a system is wrong the whole just falls apart. That is why proper planning was crucial for the development of this tool. I made sure to work out UML diagrams. This was very helpful because the editions of new systems are likely to conflict with existing ones. UML gave me a nice overview of how the current systems are connected to each other. This spared me a lot of time not rewriting systems because of inconsiderate design choices. 

Future plans

This is the first time I have created a robust system for 2D combat. I have learned how to manage lots of nested data, design a pleasant workflow and plan my architecture in advance to save my self a headache refactoring code. I am currently working on my own engine specifically catered towards fighting games. I am planning on creating externals tools and a domain-specific language for behaviour scripting. These tools are used to create character files. This way you can have logic independent of your game engine. I also want to dabble into networking so that I can implement rollback networking into my engine.