Ionic React for Game UI in Excaliburjs HTML5 game engine - 8/11
This is an open-source project on game development with a focus on the HTML5 Excalibur game engine. Check out this feature implementation on this commit in our Github repository!
You can read here the list of features we are going to develop for our Excalibur game, such as scene transitions. If you're interested in game development, follow me on Twitter and Instagram to be caught up on my latest news!
Table of Contents
In this tutorial: Ionic React
In this tutorial we integrate Ionic to our game, to manage the graphical interface.
In the last tutorial we've coded the level logic of the main scene of our game. Now, when the player takes the sword, the level is completed. We show a simple message displaying the score and a button to go to the the main menu of our game (which doesn't exist yet; we'll create it in this tutorial). We want to avoid handling by hand the DOM as much as possible: it would be great to take advantage of a library or a framework that does all the work by itself and that has a set of ready-to-use components. In this way we shouldn't worry about style and browser compatibility and we could focus on the game. There is a plenty of tools we can use; we've decided to use Ionic React (you can read here why). In this tutorial, we'll find out how to integrate it with our Excalibur game!
What we suppose you already know
In this tutorial we assume you have a basic knowledge of how React works. You can have a look at their documentation here We will also assume you know what Ionic is, here is its documentation.
What we are going to do
In this tutorial we'll use React to give the end-of-level message a nicer look. It won't be sophisticated design work, our goal is to show how to use React. We will also create a simple Menu Scene from which the user will be able to start the Play Level or change the settings. We'll implement sound and language settings (together with data persistence) in another tutorial!.
Ionic React set up
First thing first we need to install all the dependencies we need. In your terminal run
npm i @ionic/react react react-dom
npm i @types/react save--dev
These commands install the Ionic and React dependencies.
To use Ionic we also need to add some imports in the index.ts
file, as explained in their documentation:
...
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
...
setupIonicReact();
const domService: DOMService = Container.get(DOMService);
...
Remember to call setupIonicReact()
otherwise you won't see any Ionic style in your project!
Use React in the End of Level modal
As we've written, we won't do any sophisticated work while redesigning this modal. The purpose is understand how to integrate React in our game, to create it and attach it to the document
body. That said, the first thing we'll do is to create a ui
folder under the root src
one, at the same level of the services
directory. In the new folder we'll create an EndOfLevelModal.tsx
file which is the React component we'll use to display the score at the end of the play level:
export default function EndOfLevelModal({ score }: { score:number }) {
return (
<div>
<p>
Your score is
{score}.
</p>
</div>
);
}
As you can see, the content of this component is the same as the temporary one we've created in the last tutorial. Since we're going to use this new component, remember to go back to the index.html
and DOMService.ts
files and clean up a bit the temporary code we've written last time.
Now that we have the EndOfLevelModal
, we'd like to use it. To do so, go to the ModalService
which is responsible for opening the modal, and change its showEndOfLevelModal
method:
@Service()
export class ModalService {
private domService = Container.get(DOMService);
public showEndOfLevelModal(score:number) {
const container: HTMLElement = this.domService.getElement('modal-container');
ReactDOM.render(React.createElement(EndOfLevelModal, { score }), container);
this.domService.toggleElementVisibility('modal-container', true);
}
}
Before, this service asked for the coins-score HTML element
and changed its inner text. Now, the ModalService
uses the React API to create and render the EndOfLevelModal
component, appending it to the modal-container
document body element. In particular, the React.createElement
is creating the component passing it the score which is the number of coins taken while playing, while the ReactDOM.render
call is displaying it in the browser.
After adding some style to the EndOfLevelModal
(you can check them on Github), here's how the modal looks like:
The Last thing we need to add to the modal is a button to go to the Menu Level of our game, which doesn't exist yet. After having created it as we've done for the PlayLevel, we can add the button as follows:
export default function EndOfLevelModal({ score }: { score:number }) {
return (
<div className="full-size">
<div className="margin--auto width--60 full-height">
<div className="flex--vertical flex--justify-center flex-align-items--center">
<p>
Your score is
{score}
.
</p>
<IonButton
onClick={() => {
const game = Game.getInstance();
game.goTo('menuLevel');
}}
color="primary"
>
Go to the main menu
</IonButton>
</div>
</div>
</div>
);
}
As you can see, there's an ionic-button
which when clicked changes the scene to the menu one.
The Menu Level
As for the end-of-level modal, we'll create a simple menu to find out how to integrate an Ionic React UI with our Excalibur game. While the modal was a single isolated React component, for the main menu we'll create a complete Ionic App.
Following Ionic documentation, we'll start by creating an IonicReactUI.tsx
component which looks as:
export default function IonicReactUI() {
return (
<React.StrictMode>
<IonApp>
<MainMenu />
</IonApp>
</React.StrictMode>
);
}
The MainMenu
is a component we'll write right now, containing the 'Play' and 'Settings' buttons.
export default function MainMenu() {
return (
<div className="full-size white-background" id="main-menu">
<div className="margin--auto width--60 full-height">
<div className="flex--vertical flex--justify-center flex-align-items--center">
<p>Sword Adventure</p>
<IonButton
onClick={() => {
const game = Game.getInstance();
game.goTo('playLevel');
}}
color="primary"
>
Play
</IonButton>
<IonButton
onClick={() => {
alert('sound settings, to be implemented');
}}
color="primary"
>
Settings
</IonButton>
</div>
</div>
</div>
);
}
Now that we have our simple menu, we can think about how to render it. We want it to be displayed when the MenuScene
is started. It's a good start to make something similar to what we already did for the EndOfLevelModal
, that is calling the React API to create and render the component in a chosen HTMLElement
, which for us will be the menu
div element we've added in index.html
.
The question is then: where to write the code? Our first approach was to write the code in the onActivate
method of the MenuScene
:
export class MenuScene extends Scene {
private domService = Container.get(DOMService);
public onActivate(): void {
const root: HTMLElement = this.domService.getElement('menu');
ReactDOM.render(React.createElement(IonicReactUI), root);
this.domService.toggleElementVisibility('menu', true);
this.domService.toggleElementVisibility('modal-container', false);
}
public onDeactivate(): void {
// next step: clear ui HTML elements before going to other scenes
}
}
And this is a reasonable solution, except for one point: imagine we have a lot of scenes. We are choosing Ionic React as a framework and by writing the related code in their onActivate
method, we are spreading it everywhere. This makes it more difficult to maintain the code in the long term: for example, if the import of React would change from import React from 'react'
to import React from 'react-this-is-an-example'
we would need to look for that import a lot of files to correct it. Another chance is that one day we want to change the framework to, let's say, Angular. It would be a lot of work to look for React in every scene to change it.
We also foresee another problem that could emerge in the future. Imagine that the graphical interface is very slow to be built and rendered. Then the onActivate
method is not the best place to create it, because it would slow everything down while the player is playing, resulting in glitches or a blocked UI. The best solution is to first prepare the scene while a loading page is shown, and when everything is ready then to remove the loading page and show the scene. This aspect can be managed from within the IonicReactUI
component,
To write better code while considering the two topics that we've just written about, we can do two things:
- Use a service whose responsibility is to call the React-related code, as the
DOMService
does for the DOM API - Improve the
IonicReactUI
component so that it shows a loading page while it's not ready yet. This is more an improvement for heavy UIs. This is not our case, so we won't implement it.
A React service
By looking at the existing code we see that the ModalService
could be the right place where to move the code. We rename it to UIService
and add a method to it:
@Service()
export class UIService {
private domService = Container.get(DOMService);
public showEndOfLevelModal(score:number) {
const container: HTMLElement = this.domService.getElement('modal-container');
ReactDOM.render(React.createElement(EndOfLevelModal, { score }), container);
this.domService.toggleElementVisibility('modal-container', true);
}
public showMainMenu(){
const root: HTMLElement = this.domService.getElement('menu');
ReactDOM.render(React.createElement(IonicReactUI), root);
this.domService.toggleElementVisibility('menu', true);
this.domService.toggleElementVisibility('modal-container', false);
}
}
In the MenuScene.onActivate
method we'll simply call it:
export class MenuScene extends Scene {
private uiService = Container.get(UIService);
public onActivate(): void {
this.uiService.showMainMenu();
}
public onDeactivate(): void {
// next step: clear ui html elements before going to other scenes
}
}
At this stage, this is the result you should see when playing the game:
Karma and Jest configs for Ionic React
Last but not least, to write tests we need to change a little the Karma and Jest configurations.
About Karma: it's enough to make it aware of *.tsx
files, by adding them to the rules
and resolve
fields:
While for Jest, we need to instruct the tool to ignore the ionic-related node modules, otherwise, it will try to transform it is according to its rules:
Conclusions
In this tutorial, we've added an Ionic React simple interface to show the main menu for our game. The menu allows to go back to the Play Level or adjust the sound settings, which we do not have yet: the topic of the next tutorial is exactly the sound management and the data persistence of the user choice! We will add a data storage which is going to work both on computers and Android devices, so that if the user chooses not to play sound, at the next opening of our game, it still will be silenced. We can also use the storage to save the game state, as the number of coins that the player has already taken while playing the Play Level. Read here here the data persistence tutorial!
In the next tutorial we'll add a persistent data storage to remember user preferences and game state.
Did you like this article and wish there were more? Donate on Paypal so that I can write more!