Excalibur: A graphical interface with Ionic React

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 us on Twitter and Instagram to be caught up on our 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!