In this tutorial, we will explore how to create a visually appealing hexagonal skill tree inspired by the Batman: Arkham Knight game.
This guide will walk you through the process of designing and implementing a skill tree using SVGs, CSS, and JavaScript.
By the end of this tutorial, you’ll have a dynamic and interactive skill tree for your game.
Source location
You can find the complete sample source within the ${Gameface package}/Samples/uiresources/UITutorials/HexagonalSkillTree directory.
Preparing the assets
Before we start coding, we need to prepare the assets for our skill tree. The easiest and most performant way to create the hexagons is by using SVGs.
Hexagon SVGs
To create the hexagons, we will head to Figma and design a hexagon shape. We will then export the hexagon as an SVG file.
The simplest way to create a hexagon in Figma is by selecting the polygon option in the creation tool and setting the edges count to 6:
And then exporting it as an SVG file from the bottom right section.
SkillTree background SVG
We will also need a background SVG for the skill tree. This SVG will serve as the base for our skill tree on which we will place the elements.
We will use the hexagon we just created in Figma and duplicate it as many times as we want and after that connecting them with the pen tool.
After you are done with the design, export the background SVG file. Feel free to experiment with the design and create a unique background for your skill tree.
Setting up the project
Let’s begin by setting up the project structure, which includes adding the SVG skill tree we just created and some basic styles.
Skill Tree movement
Since skill trees can be quite big we want to have the skill tree follow the mouse movement.
To achieve this we will add an event listener for the mousemove event and update the transform property of the container element.
We will calculate the percentage of the mouse position relative to the window size and then map it to the desired movement range.
In our case we want the maxium movement to be 10% of the window width and 30% of the window height. So the maximum translateX value will be -10% and the maximum translateY value will be -30%.
You SVG should now follow the mouse movement.
Creating the hexagons
Now that we have the basic structure set up, we can start creating the hexagons that will represent the skills in our skill tree.
One thing we changed is the separation of the svg from one path to six path elements for each side and the addition of the strike-dash class to the paths that we want to have a dashed stroke.
We will use this class to apply a dashed stroke to the hexagon sides, creating a unique hexagon shape.
Note: You can easily separate your hexagon svg into six different path elements for each side by provding it to an AI language model and asking it to do so.
Our hexagon elements will consist of a hexagon SVG as the outline shape of the element and a content div that will hold the skill icon, which will be placed as a mask image.
We will leverage the power of CSS masks to create different color icons for the skills while keeping the background transparent.
Categorizing the skills
We are going to categorize the skills by their type (strength, durability, health, etc) and apply different colors to the hexagons. We can easily do that by applying different classes to the hexagon elements.
And use the classes with the help of CSS variables to apply different colors to the hexagons.
And now simply apply the variable to the svg path element.
With this setup we can easily change the color of the hexagons based on the skill type as the --shadow-color variable will take the color from the category class placed on the parent.
Arrange the hexagons on the skill tree
Now that we have the hexagons created, we can start placing them on the skill tree. One way to do it is to manually position each hexagon on the skill tree background SVG
by setting the left and top properties of the hexagon elements untill they fit perfectly.
Mocking game data with a model object
Since we don’t have a game to provide us with the back-end data, we are going to mock some game data that will represent the skills and their relationships.
We will create a mock model object that will hold all the information about the skills we need.
And we should not forget to initialize the model in our index.js file.
The model we created has a skills array with objects that contain the skill data, specialSkills array because we have special skills that require multiple skills to be unlocked,
and a starterSkill object that represents the starting skill of the tree which will be unlocked by default.
Since the url for the mask image icons will come from our mocked model, we can remove the placeholder CSS we used previously.
Now for every category we will create a new class with different color. And also we will add a class for when the skill is locked.
Connecting model data to the html with HTML data-binding
Now that we have the model setup, we can start connecting the data to the HTML elements. We will loop through the skills array and create a hexagon element for each skill
by utilizing the power of data-binding .
Since we have 3 types of skills - starter, normal and combined we will need to handle them separately.
Starter skill
For the starter skill, we will need to extract from the model the x and y coordinates and the image url for the mask image.
To make our starter skill stand out we will apply a different color to each side of the hexagon. We will use the stroke attribute to apply the color to the sides directly.
And apply a conic gradient as the background, making it all seamlessly integrate with the stroke colors.
With that, we can now see the starter skill placed in the center of our skill tree!
Regular skills
Since we will be looping through the skills array we will need to create a new hexagon element for each skill. We can easily do that with the help of the data-bind-for attribute.
The ohter notable difference from the starter skill logic is that here we will dynamically create the class name based on the skill type,
which with the help of the CSS we set up earlier will apply different colors based on the categories of the skills.
And finally, we will use data-bind-class-toggle to conditionally apply the hex-locked class to the hexagon element if the skill is not unlocked.
This will make the hexagon appear grayed out if they are locked.
Combined skills
Lastly, we will create a hexagon element for each combined skill.
The combined skills will have a different color on both the left and right sides based on the category of the parent skill of each side.
We will achieve that by grouping the left and right sides of the hexagon svg path elements and applying a class to it.
And just as we did with the starter skill, because there will be more than one color for the background,
we will add separate styles for the background of the combined skills.
And with that we have all the skills placed on the skill tree!
If you comment out the data-bind-class-toggle attribute you will be able to see the color categories
Replacing the skill tree svg
Now that we have all the skills placed on the skill tree, we can remove the hexagons that we placed our elements upon and preserve only the lines connecting them.
Achieving this is very simple, we just need to head to Figma again and export the skill tree without the hexagons.
Adding effects and animations
To make our colorful skill tree even more engaging we can add some effects and animations to the hexagons.
Glow effect
To enhance the visual appeal of the skill tree we can add a glow effect to the hexagons making them more futuristic.
Doing that is very simple, we just need to add a filter property to the svg hexagon element.
Each hexagon will now have a glow effect that will change color based on the category of the skill.
We also need to handle the combined skills and the starter skill separately.
The starter skill will have a different glow effect. We want each side of the hexagon to have a different glow color. Unfortunately,
we can’t apply filter to a path element, so the approach we will go for is to put 2 of the same SVGs in the hex-content element and blur them, making the effect of a glow.
In the hex-starter element, inside hex-content we will add the following html:
And the following CSS:
Active skill effect
When a skill is hovered or selected we can add a unique animation making it stand out from the rest:
Let’s begin by firstly modifying the hex elements by adding a couple of SVGs again inside the hex-content element.
To decrease the amount of code, we are going to once again use data-bind-for to render the inner svgs that will be used for the animation.
And we should also create the model like so:
Starter hex:
Regular skills:
Combined skills:
We will now use the svg-outer and svg-inner classes to apply different kinds of animations for each SVG.
Let’s create a second CSS file to put our animations in as to not polute the global one.
Unfortunately, CSS variables can’t be used in keyframes, so we will have to set some default colors for the animations.
Now let’s add the animations to the hexagons:
We added a selected class to apply and run the animations only on the currently selected element.
We can now add the selected class to the hexagon element and its SVGs when it is hovered or clicked. We will again utilize the power of data-binding
to easily attach events to our skill tree items.
For more seаmless state management, we are going to create
an observable model
which will automatically update when its state changes.
After initialzing it we are going to set its value as our starter skill.
Starter skill:
Other skills:
As you can see, we’ve added a data-bind-focus attribute to the hexagon element. This attribute will call the makeActive function when the element is focused.
The makeActive function will expect the DOM element and the skill object from the model as arguments and there we will handle which element is active.
Like that, the code will almost work, what’s left is to make each of our hexagons focusable.
Adding keyboard navigation
We are going to use Gameface’s
Interaction manager library
and more specifically the Spatial navigation
to make our hexagons focusable and to easily extend our sample with keyboard navigation.
After installing it and putting it in our project, what’s left is to initialize it in our index.js file.
Here we say that we want to make all elements with the class hex focusable and that we want to allow a maximum of 30% overlap
between the current item and the others when deciding which element to focus next.
You can find out more about this parameter in the documentation .
The only thing left is to call this function after the model has loaded in the Ready event.
And that’s it! Now you can navigate through the hexagons using the keyboard arrows and see the active element animation respond.
Making it all interactive
Now that we have our skill tree set up, we can start making everything interactive and input responsive.
Adding tooltip with skill information
We are going to add a tooltip to display the skill’s information when the user hovers over a skill and a skill points counter to keep track of the available skill points the player has.
The tooltip will consist of the following elements:
Skill name
Skill cost
Skill description
Unlocked status that will show dynamically based on the skill’s unlocked state.
We are once again going to make use of the observable model that keeps track of the active element. Having this information we can use data-binding to fill out the textContent
of the elements we need with the properties of the currently active element.
Now to add the styles
The idea is to have the tooltip correspond to the skill category and also have a glow effect. We achieved this by using box shadows for the regular skills and linear gradients for the starter and combined skills.
Now when you hover over a skill, you will see the tooltip with the skill’s information dynamically updating upon interaction.
Adding skill unlock functionality
In order to make the interaction with the skill tree more engaging, we will make the paths connecting the skills fill with the corresponding color of the skill’s category
when a skill is unlocked.
We will slightly modify the SVG paths by giving them category classes, as well as a class that will help us keep track of which path corresponds to which skill of the category,
essentially making each path have a unique class that will help us identify when to color its stroke.
Let’s apply the styles to the paths, as well as handle the transition for when the stroke is filled.
With that out of the way we are now ready to combine the skill unlock functionality with the skill tree.
In the index.js file we will add an unlockSkill function that will handle all the logic.
In the unlockSkill function we do the following:
Check if the skill is already unlocked
Check if the player has enough skill points to unlock the skill
Check if the skill’s parent is unlocked
If all the conditions are met, we set the unlocked field in the model to true, update the model, and remove the hex-locked class from the skill element and the corresponding path to fill the svg’s path with color.
We also added the isSkillValid helper function that will help us determine if the skill’s is valid for unlocking.
Normal skill is considered valid if the parent is unlocked
Combined skill is considered valid if both parents are unlocked
Lastly we need to connect the unlockSkill function with our hexagon elements. The logic for unlocking will trigger when the element is clicked or the key ‘Enter’ is pressed.
For the combined skills, it’s the same as the regular skills, but we need to pass an additional argument to the unlockSkill function to indicate that the skill is combined.
Lastly, we need to handle the key press event for the ‘Enter’ key within the handleKeyPress function. Its logic is simple - if the ‘Enter’ key is pressed, we call the unlockSkill function.
And that’s it! Now you can interact with the skill tree by unlocking the skills and seeing the paths, skills and the tooltip fill with color.
Adding error Handling
As you have probably noticed, clicking on a skill that is locked and doesn’t meet the requirements will not show a visual cue.
We should notify the player that the skill can’t be unlocked and why. We will add a subtle locked animation to the skill element and display an error message.
For displaying the error message, we will use Gameface’s toast component.
Click if unsure how to add it to your project
First, we need to install it.
After installing it, we need to add the toast component to our project. Let’s first add the toast’s styles.
We will add them before our custom styles, so they can be easily overwritten.
And the toast’s script
Let’s add the toast to our index.html file.
Now let’s customize the toast’s styles to fit the theme of our project.
And add a custom slide-in animation for the toast, overwritting the default one.
The reason we added 2 animation that do the same thing is because we want to be able to retrigger the animation if the toast needs to be shown again before the previous one has finished.
Now let’s add the logic for showing the error message.
The other visual indicator for invalid operation is the locked animation on the skill element. We will add a simple animation that will make the skill element shake when the player tries to unlock a locked skill.
Apply it to the hex elements
What’s left is to add a function to trigger the animation and combine it with the toast error message in the unlockSkill function.
Now we have a fully interactive skill tree with error handling and visual cues for the player!
Quality of life improvements
Our skill tree is almost complete, but there is one quality of life improvement we can add to make the experience even better.
Currenly, if the player decided to navigate the tree using only the keyboard, the screen won’t follow them like it would if they were using the mouse.
We can fix this by enhancing the makeActive function by calling the translateSkillTreeIntoView function and providing it with the coordinates of the active skill.
In order to properly calculate the correct x and y coordinates, we have to subtract the position of the skill element from the position of the skill tree. That way we get the correct coordinates
relative to the viewport and not the tree SVG.