A games leaderboard ranks players based on performance, fostering competition by showing top scores and achievements. It encourages players to improve and climb the ranks, enhancing engagement in the game.
In this tutorial, we’ll build upon the previous multiplayer UI by adding a leaderboard feature. To accommodate this addition, we’ll refactor some of the existing pages, making it easier to integrate the new leaderboard.
Additionally, we’ll enhance the Express server by introducing new endpoints that retrieve user stats for display on the frontend.
Source location
You can find the complete sample source within the ${Gameface package}/Samples/uiresources/UITutorials/MultiplayerUI directory.
src folder contains the UI source code.
api folder contains the Express server source code.
Ensure to run npm i before testing the sample.
Refer to the README.md file in this directory for information on running the sample locally or previewing it without building or starting any processes.
Getting started - Backend
We’ll start by updating the backend to include new endpoints that return relevant user stats for the frontend to display on the leaderboard page.
Add additional fields to the user
To store user stats, we’ll extend the user schema in our database by adding two new fields:
The stats object will store data such as games played, wins, and scores.
Creating endpoints to retrieve user stats
When the leaderboard page is accessed, all users’ data will be requested from the server. Additionally, when a user is selected from the table, their specific data will be retrieved.
To facilitate this, we’ll add two new routes to the user routes:
getUsersRankings method
This method will return the stats data for all users.
getUserRankings method
This method will return the stats for a specific user by passing their id in the route params.
Adding mock data to the newly registered users
Since our UI isn’t connected to a live game, we need to generate mock user stats when a new user registers. This can be done at the point of registration by adding mock data to our public database, which is used by the sample application.
Getting started - Frontend
On the frontend, we’ll add a new leaderboard page to display user stats fetched from the server. Additionally, we’ll reorganize our friends page from the previous tutorial for better structure.
Refactor friends files structure
To improve organization, we’ll move the AddFriends and Friends pages into a new Friends folder.
Add friends wrapper page
We’ll create a wrapper component in pages/Friends/FriendsPageWrapper to display both the friends list and add friends pages.
Here, NavLink elements are set up for easy navigation between the friends list and add friends pages. The gameface-scrollable-container has been moved from both individual pages to this wrapper for simplicity.
Note: Use the end attribute on the main route (/friends) to ensure the correct NavLink is highlighted when navigating between subroutes.
The Outlet will render the component for the active subroute. Next, we’ll adjust the routing in App.jsx.
Updating routes and adding leaderboard page
In App.jsx, we’ll refactor our existing routes into subroutes and include a route for the leaderboard page.
We’ve set the default home route to lead to the rankings subpage within the leaderboard page.
Leaderboard wrapper page
We’ll apply a similar approach to the leaderboard page wrapper as we did with the FriendsPageWrapper.
This wrapper contains the rankings table, displayed in the Outlet via the Rankings component that we set up earlier in the router. It also includes the PlayerStats component, which appears on the right when a user is selected from the table. The PlayerStats component shows additional details about the selected player, such as their games, wins, losses, and win rate.
State Sharing
To ensure the PlayerStats component correctly displays data for the selected player, we define a state in the leaderboard page wrapper using const [selectedPlayerId, setSelectedPlayerId] = useState(''); and pass it as context to the Outlet, which is then used by the Rankings component. In Rankings, you can access this context as follows:
Using setSelectedPlayerId updates the selectedPlayerId, which reflects in both the LeaderboardPageWrapper state and the PlayerStats component, enabling shared state between components in React. While you could use other state management approaches like redux, this method keeps things simple.
Rankings component
To display the rankings table, we’ll create a new Rankings page that renders within the LeaderboardPageWrapper’s Outlet.
This page includes a loader, a table with headers, the current user’s scores at the top, and a gameface-scrollable-container for the list of other users.
We begin by setting up component states, hooks, and the outlet context:
Upon mounting, this component fetches user rankings to display:
As mentioned earlier, shared state allows the player stats panel to open when a user is selected from the table.
To simplify the code, we create memoized variables for the current user’s index and data:
Finally, we render the UI:
UserRankItem component
The UserRankItem component is used to create the table preview on the Rankings page. It accepts the following props:
rank: The user’s rank to be displayed in the table.
userName: The name of the user.
userId: The user’s unique ID.
scores: The user’s scores.
className: Additional classes for the component’s wrapper div.
onClick: Event handler triggered when the component is clicked, typically used to open the player’s stats.
This component uses classes from the gameface-grid component to correctly align its contents.
You can use the UserRankItem component in different layouts within the table:
Table header
To create a table header, set the UserRankItem props like this:
User items
To display the current user at the top of the list, use UserRankItem as follows:
For other users, generate the items from the rankingsData array:
If the user in the data matches the current user, skip rendering their UserRankItem since it has already been displayed.
Display player stats when user is selected from the leaderboard
When a user is chosen from the leaderboard table, their stats will appear to the right of the table. To accomplish this, we’ll create a PlayerStats component. This component will show a loading indicator while fetching the user’s data from the server and, once ready, will display the number of games played, wins, losses, and win rate. Additionally, a donut chart will visualize the win/loss ratio.
We’ll start by defining a StatItem component, which will be responsible for displaying each individual stat and its corresponding value:
Then we can continue with the PlayerStats component. First we setup the states that the component will use and other things such as using the useDimensions hook that will be needed for making the chart responsive.
Next, we’ll build the PlayerStats component. This involves setting up the necessary state variables and using the useDimensions hook to make the donut chart responsive.
We then fetch the player’s data from the server when the component mounts:
To simplify rendering, we define two memoized variables:
Finally, we’re ready to render the PlayerStats component:
StatItem component
The StatItem component is stateless and serves to display data fetched from the server.
Loading indicator timeout
A loading indicator with a timeout is added to give the donut chart enough time to render and load the data.
useDimensions hook
To make the donut chart responsive, a useDimensions hook is used. This hook observes the dimensions of the chart’s parent element, updating the chart’s dimensions whenever the parent’s size changes.
Instead of watching for document resize, we use ResizeObserver to monitor changes to the dimensions of the chart’s parent element.
DonutChart component
We’ll use the d3 library to create a donut chart in React. A post describing how to use this library in Gameface with plain JavaScript can be found here.
Calculating the chart radius
The outer radius of the chart is recalculated dynamically based on the width and height of the parent element. This ensures the chart is responsive, adjusting each time the parent size changes.
Generating the pie
A new pie chart is generated whenever the chart data changes. The data passed from the PlayerStats component represents the user’s wins and losses, regenerating the pie whenever a different user is selected.
Generating the arcs
The arcs of the chart are generated based on the outer radius and the pie data. They will regenerate if either the radius or the user data changes.
Animating the arcs
To animate the arcs when created, the useEffect hook is used to handle the animation when the chart is mounted or the pie data changes.
Making the chart responsive
To make the chart responsive, the svg element needs the viewBox attribute set dynamically:
Additionally, the group holding the arcs should be moved to the center of the svg element: