1. Los Pingheros
  2. News

Los Pingheros News

DevBlog #2: Input to actions architecture 🐧

[h3]Hey Pingheros,[/h3]

Today, I want to share with you how we implemented our input handling stack using Unity and Photon Quantum for our online multiplayer brawler game Los Pingheros at Hectiq. Photon Quantum is a deterministic simulation engine that handles rollback, which allows to only send inputs over the network.

Even though the description and examples will be explained with Unity and Photon Quantum, most of the concepts are generalizable and help decouple inputs from your game logic.

[h2]Global architecture[/h2]

From pressing a button to seeing the character move in our game, the inputs go through different transformations.



If you follow the path from pressing a button to having the character do either a slap or a grab, it looks like this:

- Pressing Button X.
- Remapping Button X to Slap InputAction.
- Transforming Slap InputAction to Slap SimInput.
- Transforming Slap SimInput to a Slap CharacterAction or a Grab CharacterAction depending on the current state of the character as we plan to use the same input to trigger different actions.

Here is a bit of colourful animations of the slap and slide actions so you have a better idea of what is going on in the game.



[h2]Dynamic remapping (raw inputs to input actions)[/h2]

Unity “new” (well, not that new anymore…) input system allows you to bind raw inputs to actions. It allows to map several different buttons from different devices to the same callback.



In our example, raw inputs are directly mapped to input actions here. This helps iterating on your mappings quite easily as you do not have to write/edit code while you test different mappings. It also allows you to handle some low-level stuff such as dead-zones, quick button press vs hold, & so.

We did not do dynamic remapping yet, but if you plan to do it, that would be where you are supposed to plug it. Actions in Unity would become rawer, such as South, and you would then map this raw action to the associated input action such as Slide using a remapping table or something similar.

However, this leads in you needing to handle low level logic by yourself (e.g. the press vs. hold logic) as it is probably action dependant, not input dependant.

[h2]Input ingestion (InputAction to SimInput)[/h2]

The way input are ingested with Photon Quantum simulation is through a callback which is called at the beginning of each simulation frame. You fill up an input structure composed of buttons & Vectors that are send to each other clients.


input
{
bool IsPointer;

button Slap;
button Slide;
button SlideCharge; // Keyboard only
button Grab;
FPVector2 Move;
FPVector2 Aim;
FPVector2 ActionDirection;
...
}


On the Unity side, you hold an input struct that you fill up with every Unity’s input event. This struct is then polled from the simulation and reset for gathering inputs coming after.

Buttons are a bit more than booleans, they are accessed in the simulation through IsDown, WasPressed, and WasReleased properties. If you need some hold/quick press logic on the simulation side, you have to do it yourself. Even though filling the input struct is pretty straightforward, it comes with some important tricks that you might find useful.

[h3]Device specifics:[/h3]

We decided to add a boolean specifying if the input comes from a pointer type (mouse, trackpad, &so.). More information about this in the interpretation section.

[h3]Sub-frame press-release:[/h3]

There is a huge chance that the rate of your simulation is fixed, meaning that the input gathering rate from Unity and from your simulation are out of sync. This might lead to issues where you receive several raw inputs impacting the same sim-input in contradictory ways. The most impactful example of this we encountered is when you press, then release an input between two simulation input polling.
By pressing the slap input, you fill up your buffer input structure setting up the slap button to the “down” state, then you release it, putting the button back to the “up” state. This means that you should not use IsDown only when checking for inputs inside the simulation, as that would lead to you dropping the input entirely. More information in how to handle this in the interpretation section.

[h3]Sub-frame position precision:[/h3]

If your game and your player base is competitive, it is possible that they develop really good aim-click coordination (this is even more true with mouse players), and thus, the need to retain the click position of the exact time they clicked. Otherwise, you could end up with something like this:
Player clicks, leading to the slap input to be triggered, then continue to move their mouse, registering the current cursor position at a rate way higher that your simulation rate. You would then send a position input associated with the last recorded position, and you end up with the feeling that the button press is lagging.
To counteract this, we decided to separate the ActionDirection vector from the AimVector. The former represent the direction when the last action was triggered, while the later represents the latest recorded aiming position. This gives you the best of precision for both the action and your orientation.

[h2]Interpretation (Simulation inputs to character actions)[/h2]

The last step is to convert the inputs received by the simulation every frame into actions for your character to execute. This goes with problematics such as buffering inputs, or transforming the same input into different actions depending on the context.

In our game, a CharacterAction looks like this:


component CharacterActions
{
FPVector3 MoveVector;
FPVector3 ActionDirection;

bool Slide;
bool SlideCharge;
bool Slap;
bool Grab;
bool Mash;
...
}


[h3]State based input transformation[/h3]

Those actions are decorrelated from the inputs and are as meaningful as possible for the character itself. The two main reasons for this are:


    to simplify the logic in the state machine.
    to simplify the interfacing with the AI we use for bots. Indeed our bot system use the exact same character as the players, except for the logic that creates character actions every frame. Having meaningful character actions helps the logic understanding of AI development and make it resilient to changes linked to how we translate inputs to character actions.


In our game, as often, the character has a state machine controlling what it is doing and what it can or cannot do. As a consequence, it appears important to take the current state of the character into consideration when interpreting inputs.

Here is an example to illustrate this: somebody grabbed you, so you cannot slap. In that case, you want to interpret the input as a mash action to help breaking free from your foe.



If we did the separation between slap & mash outside of the simulation, we would have used a state of the character at the time of the button press, that might not be the state of the character at interpretation time because of rollback or network latency. Thus, making our input drop.

All our states in the character state machine implement the InputsToCharacterAction method to interpret inputs depending on the context.


public unsafe bool InputsToCharacterAction(Frame f, in Input input, ref CharacterActions characterActions, EntityRef e){}


The following paragraphs are exemple on how we leverage this method to solve different problems.

[h3]Handling “Hold” inputs[/h3]

As stated previously in the Sub-frame press-release paragraph, it is not advised to use the IsDown property only as it can lead to inputs being missed. You would need to use it in combination with WasPressed or WasReleased this way.


characterActions.Slap = input.Slap.WasPressed;
if (previousInput.Slide.IsDown)
characterActions.Slide = input.Slide.IsDown && !input.Slide.WasReleased;
else
characterActions.Slide = input.Slide.IsDown || input.Slide.WasPressed;


Our slap action do not have any logic when keeping the button pressed. Thus, only using WasPressed works like a charm.

Our slide action though is continuous, and need the slide input to be held. As a result, we protect it from sub frame press-release or release-press by keeping the state of the previous input.

[h3]Device type branching[/h3]

Maybe you noticed that the Sim inputs have a MoveVector and an ActionDirection and that the AimVector from sim inputs has been laid off.

This is because during the interpretation phase, input vectors are becoming actions vectors with a logic that depends on the state and on the type of input device used.
Basically, with a controller, the MoveVector is controlled with the left stick and the AimVector is controlled with the right stick while ActionDirection is the AimVector at the moment where we pressed another input.

With a Keyboard/Mouse device, the MoveVector is controlled with WASD and the AimVector is controlled with the mouse while ActionDirection is determined by the mouse position at the exact moment we pressed another input.

But, when implementing certain features, we figured out that keyboard/mouse users sometimes use the mouse for aiming purposes, and sometimes for movement purposes. Sometimes, we also want the MoveVector to trigger another action only with the controller because it would feel weird with the mouse.

Thus, depending on the state, we may have something like this:


if (input.Slide.IsDown && input.IsPointer)
if (input.ActionDirection != FPVector2.Zero)
characterActions.MoveVector = input.ActionDirection.XOY;
else
characterActions.MoveVector = input.Aim.XOY;
else
characterActions.MoveVector = input.Move.XOY;


This allow to use the `MoveVector` action to control the slide independently of what actually triggered the action.

[h3]Handling input buffer[/h3]

In dynamic games, it is quite frustrating when one of your input is skipped because you pressed a button a frame or 2 before your character become activable. As we emphasis the decision making more than the frame-perfect execution, we implemented an input buffer system.


component BufferedInputs
{
SerializableQueueMetadata QueueMetadata;
array[6] QueueStorage;
}


We implemented a serializable queue to store the inputs that are not consumed. It is up to you how you decide to implement that, though we might share how we did it on another snowy day.


var inputQueue = new QueueRef(bufferedInputs->QueueStorage, &bufferedInputs->QueueMetadata);
if (inputQueue.Count == inputQueue.Capacity)
inputQueue.Dequeue();
inputQueue.Enqueue(input);
CharacterActions outCharacterAction = default;
CharacterActions.DirInputToCharacterActions(ref input, ref outCharacterAction, in *filter.pCharacterStateMachine, in *filter.pCharacterStats);
for (int i = 0; i < inputQueue.Count; i++)
{
if (filter.pCharacterStateMachine->InputsToCharacterAction(f, in inputQueue[ i ], ref outCharacterAction, filter.Entity))
{
for (int ii = 0; ii < i + 1; ii++)
inputQueue.Dequeue();
//Quantum.Log.Info($"Used buffered inputs from {inputQueue.Count} frames ago.");
break;
}
}


We start by adding the last incoming input to the queue, and we dequeue inputs until we find what we call a meaningful input. If that is the case, we dequeue all inputs up to our meaningful input.

If your attention was sharp, you realized that the InputsToCharacterAction method presented previously was returning a boolean. This is is the way for the current state of the character state machine to tell if there is a meaningful input to be consumed.


public unsafe bool InputsToCharacterAction(Frame f, in Input input, ref CharacterActions characterActions, EntityRef entityRef)
{
bool bIsMeaningfulInput = false;

characterActions.Slide = input.Slide.IsDown;
bIsMeaningfulInput |= characterActions.Slide;
characterActions.Slap = input.Slap.WasPressed;
bIsMeaningfulInput |= characterActions.Slap;

return bIsMeaningfulInput;
}
}


This is an example from our Slide state. You can see that Slap or Slideinput would be consumed instantly, while another input such as Grab would not be meaningful for that state as we cannot grab something while sliding, and would thus be kept in the input queue if no meaningful inputs are found afterward in the queue. If we find a meaningful input later in the queue, the Grab input would be dropped.

Another subtility is that the MoveVector is always the interpretation of the last incoming input (this is done in the DirInputToCharacterActions method call before the inputQueue loop). And filling up the MoveVector for character actions is never going to be considered as a meaningful input. This way, buffering an input for an action has no effect on your movement.

[h2]Conclusion[/h2]

Coming up with this architecture for our inputs was not something that came in a day, but this gives us a lot of flexibility in how we develop our actions and how we control them. We are interested in what you think about our implementation, or in how you did architecture your input flow.

Don’t hesitate to leave us comments on what topic you might want to see on this devlog for the future.

Also, if you are interested in our project, you can follow us on social media, or come play with us on LosPingheros on Steam.

Don’t freeze your brains up ‘til next time, and meet us on the Mexican ice of Los Pingheros !

Romain GROSSE
Hectiq

DevNews #1 - New menus, game mechanics, and the arrival of bots!

[h3]Hey Pingheros! 🐧[/h3]

We're starting our DevNews series, aiming to introduce you to our team and share the latest updates and our progress as we develop the game! 🪅🌶️

[h2]Menu[/h2]

You will be able to access the tutorial directly from the main game menu! This allows players to get warmed up before jumping into the battle. 😈



Keyboard and mouse players haven't been forgotten. They'll have a guide for their controls directly in the settings menu.



Pinghero customizations can be inspected in the menus, so you will be able to admire your collection even when you're not in a match.



[h2]Game[/h2]

Important gameplay change: the dash will now need to be charged before use. This leads to more anticipation of opponents' actions rather than just spamming dash combos.



The Pinghero's slap will now be able to deflect slides as well as snowballs.



You will now be able to perform actions while carrying an item, making it less punishing to have one in your wings!



[h2]Features[/h2]

Bots are coming to Mexico soon! We're working hard on their behavior to provide you with the best partners for your games or training sessions.
(Don't worry, they'll fall into the water from time to time too 💧🐧).

[h2]Other[/h2]

We launched our first stream on Twitch! 🥳

While we prepare more game content for you, we thought you'd enjoy a behind-the-scenes look at our development process. Romain, gameplay developer at Hectiq, worked on Los Pingheros for several hours, discussing their coding work.

Thank you for playing the first playtest! 🪅

[h3]Hey Pingheros![/h3]

A big thank you for participating in our playtest! Your time and feedback have been helpful in improving the game, and we will continue to make it better.

We hope you had a good time! And guess what?
The next playtest is coming very soon, with new features to explore!

See you on the ice again soon!

Hectiq Team 🐧

DevBlog #1: Hectiq & VCS: why we opted for GIT LFS 🐧

[h3]Hey Pingheros![/h3]

In the fast-paced world of game development, choosing the right Version Control System (VCS) is crucial for managing complex projects efficiently. With numerous assets, large binary files, and a need for seamless collaboration among distributed teams, game developers require a robust and flexible VCS. Among the various options available, Perforce, Git, and SVN (Subversion) are three of the most prominent choices. Each has its strengths and weaknesses, but selecting the best fit can significantly impact a project's success.

At Hectiq, our flagship project, Los Pingheros, a unique multiplayer snowball fight game set in a vibrant Mexico, draped in a unique snowy mantle, demands a VCS that can handle its complexity and scale. In this article, we'll briefly explore the key features and limitations of Perforce, Git, and SVN, and explain why we decided to adopt Git with Git Large File Storage (LFS) for our game development needs. Our goal is to provide insight into the decision-making process and highlight the advantages that Git with Git LFS brings to our workflow.

Quick Overview of Each VCS


[h2]Perforce[/h2]
[h3]Main Strengths and Common Use in Game Development:[/h3]
  • Centralized Version Control System: Perforce uses a centralized model, which is familiar and well-mastered by the Hectiq team.
  • Handling Large Binary Files: Perforce excels at managing large binary files, a common necessity in game development.
  • On-Premises Installation: Perforce can be installed on our own servers, providing control over our data.
  • Intuitive Tools and UI: The user interface is highly intuitive, making it easier for our team, particularly artists, to use.

[h3]Key Limitations:[/h3]
  • Cost: Perforce is expensive, costing $495 per user annually. As a startup, Hectiq aims to reduce expenses and dependencies on costly solutions.
  • Scaling Costs: While scaling is technically easy, the financial burden increases significantly with a growing team.
  • Branch Management: Working with branch is definitively not straight forward as with GIT

[h2]Git[/h2]
[h3]Main Strengths, Particularly with Git LFS:[/h3]
  • Distributed System: Git is a distributed VCS, allowing for greater flexibility in workflows.
  • Efficient Storage: For non-binary files, Git stores only the differences from previous versions, optimizing storage.
  • Developer Preference: Git is the preferred solution among programmers due to its powerful features and flexibility.
  • Open Source: Git is free to use, which is crucial for a cost-conscious startup.
  • Integration with DevOps Platforms: Git can seamlessly integrate with platforms like GitLab, enhancing our CI/CD processes.
  • Industry Standard: Git is widely adopted in the tech industry and continuously improved, with many solutions built on top of it, such as GitOps.

[h3]How It Handles Large Files Effectively:[/h3]
  • Git LFS: Large binary files are managed through Git LFS, which stores these files in a specific location on the server and maintains pointers to them. This approach simplifies the handling of large assets in game development.

[h3]Key Limitations:[/h3]
  • Git is easy to learn, hard to master
  • Artist tends to struggle handling conflicts

[h2]SVN (Subversion)[/h2]
[h3]Main Strengths:[/h3]
  • Centralized System: SVN is an older, centralized VCS, which some teams might find familiar.
  • Open Source: Like Git, SVN is free to use.
  • Handling Large Files: SVN manages large files effectively.

[h3]Key Limitations:[/h3]
  • Lack of Popularity: SVN is not the preferred VCS in the industry today. Opting for a less popular system can result in fewer available tools, community support, and resources.
  • Potential Drawbacks: Choosing a less popular VCS can lead to challenges in finding support and integrating with modern development tools.


Server Costs


Understanding where your VCS will be hosted is critical.

[h3]Perforce Hosting:[/h3]
  • Using Perforce's server is straightforward but comes at a higher cost.
  • Perforce can be installed on your own server, and it has a GUI tool to manage it

[h3]Git Hosting:[/h3]
  • Hosting a code repository on GitHub or GitLab is often free, but the costs for storing LFS files can be substantial.
  • Git can be installed on a local server too; in our team we use a docker instance of Gitlab on-premises

[h3]SVN Setup:[/h3]
  • Setting up SVN requires a specific IT skill set and ongoing maintenance, similar to Git and Perforce.

All three solution can be installed on a local server, but this approach has drawbacks, including maintenance time, backup, and security concerns.


Key Comparison Points


[h2]Distributed vs. Centralized[/h2]
  • Distributed: As a young, remote-friendly team, Hectiq has half of its team working remotely, the distributed approach allows us to work and change branches without being constantly connected.
  • Centralized: A centralized approach requires an internet connection to access the VCS, which can be limiting for remote teams.

[h2]Financial Costs[/h2]
  • Cost Optimization: The video game industry is financially challenging, so cost optimization is crucial.
  • Perforce Costs: Perforce is free for up to five users, but costs increase significantly beyond that. A team of 20 would incur a yearly expense of $10K.
  • Scaling Considerations: Hectiq aims to grow beyond a small indie developer, so we consider long-term costs in our decision-making.

[h2]Ease of Use[/h2]
  • Perforce: Easier for artists and less technical team members to use.
  • Git: More complex but powerful, with tools like SourceTree simplifying the experience. Git LFS adds commands that could be better integrated but are manageable with our IT skill set.


GIT and Artists: Structuring Repositories for Efficiency


In our project repository, among multiple folders, two particularly critical ones require special attention:
  1. ArtSource
  2. UnityProject

While the UnityProject folder doesn't necessitate specific handling as its files are universally needed across all development teams, the ArtSource (soon to be renamed to AssetsSource) presents unique challenges. This folder houses all the artistic assets and localization data, amounting to over 100 GB of files.
Why Download What You Don't Need?
For a programmer who doesn't need access to texture sources such as Substance files or PSDs, it's impractical to clone this entire folder. This situation conflicts with Git's principle that users should have access to the entire project history locally. The implications include:
  • Increased clone and pull times for all users.
  • Expanded storage requirements on local machines.

To mitigate these issues, we developed a script that allows users to clone the project while excluding unnecessary folders, significantly reducing the time and space required:

[h2]Example Script to Optimize Cloning[/h2]

batchCopy code
@echo off
setlocal

rem Define variables
set REPO_SSH=ssh://git@[LOCALDOMAIN:PORT]/hectiq/myproject.git
set CLONE_DIR="C:\Repository\MyProject"
set CERTIFICATE_DIR="C:\local.hectiq.crt"

rem Initialize the repository
cd %CLONE_DIR%
git init

rem Configure Git to exclude certain paths
git config --local lfs.fetchexclude "artsource,artsource_techart"
git config --local http.sslCAInfo %CERTIFICATE_DIR%

rem Add and fetch from remote
git remote add origin %REPO_SSH%
git fetch origin

rem Checkout development branch
git checkout dev

rem Update submodules
git submodule update --init --recursive

rem Open directory in Explorer
explorer %CLONE_DIR%

pause
endlocal

[h2]Adaptive Access for Specific Needs[/h2]
During development, a technical artist might need temporary access to the Character Rig Sources. To address this, we created a tool that allows team members to "opt-in" to access resources from other departments as needed.
This tool leverages the git config command to dynamically include or exclude folders from LFS tracking, facilitating efficient data management tailored to current needs:
git config --local lfs.fetchexclude "folderToExclude1,folderToExclude2"

Here's an illustration of the tool in action, demonstrating how developers can selectively sync data relevant to their specific tasks.

By integrating smart repository management practices and tools, we significantly streamline the development process, minimizing overhead and ensuring that all team members have exactly the resources they need, exactly when they need them.

Why We Chose Git with Git LFS


The primary reason we chose Git with Git LFS at Hectiq is our strong internal IT skill set. We easily set up a GitLab solution on our internal server, spending no more than five man-days over an entire year on infrastructure setup.
We have an internal server accessible via a VPN through WireGuard. All software on the server is installed using a Docker Compose file, simplifying maintenance. The server has a dedicated SSD for storing Git LFS files.
While we initially doubted the scalability of this system, we have successfully scaled to 10 users with minimal additional costs or IT time. This setup has saved us thousands of euros, but we strongly advise against this approach without skilled IT workforce, as it can divert focus from the main project development to infrastructure maintenance.
The last aspect that definitively convinced us to adopt Git is the easy integration with a robust DevOps platform like GitLab…This on his own is an entire subject and we will deep dive into it in future articles.

Conclusion


In conclusion, the decision to choose Git with Git LFS over Perforce and SVN was driven by our need for a cost-effective, flexible, and scalable solution that fits our remote-friendly team structure. The strong support and integration capabilities of Git, combined with our internal IT expertise, made it the best choice for Hectiq's game development needs. Our flagship project, Los Pingheros, a thrilling snowball fight brawler that brings the lively spirit of Mexico to a snowy battleground, benefits immensely from this setup, ensuring that we can manage its complexity and scale effectively. Using Git with Git LFS, we are well-equipped to maintain efficient, collaborative, and high-quality game development.

Share your thoughts


What is your opinion on the subject? Share your feedback and experience with us, and if you have further questions or needs do not hesitate to reach us. We are always ready to discuss further insight on this matter.

Los Pingheros Playtest is now live!

[h3]Hey Pingheros![/h3]

We're thrilled to launch our first Los Pingheros open playtest now!

To play the game, press the ”Request Access” on the Los Pingheros Steam page.

A subset of players who request access will be invited to participate via an email from Steam with download instructions, then we will gradually roll out more invites over time. Don't forget to check your email (and spam folder) to access the Playtest!

[h2]Join our playtest[/h2]
https://store.steampowered.com/app/2418600/Los_Pingheros/

In this playtest, you will get access to 3 maps and 3 game modes with up to 8 players online and local.

Other features are planned and will be implemented during this playtest phase. Follow us Steam to stay tuned!

[h2]Goal[/h2]

You can report a bug and share your feedback on the Discord server or through the in-game report tool.