Introduction
Finally, the hardest part of the game is finished. The rules are set up and tested. The move generation algorithm (hopefully) works correctly. The board supports making and unmaking moves. Our focus now will be to allow the player to interact with our game.
Inputs and outputs
Any videogame out there can be seen as a black box. The innards of this box contain the current state of the game and the rules and systems we have implemented so far. The player cannot see these elements inside the box, and that’s reasonable. We don’t want to overwhelm the player with the complexity of our mechanics. The point is to make the game fun and simple to understand.
![]() |
---|
Games as a black box. Taken from [1] |
However, this black box is pretty useless and boring by itself. There’s no way to change its inner state from the outside.
That’s why we need a set of inputs that change the state of the black box. These inputs must be carefully designed so they give enough freedom to the player to interact with the game but not too much to break it.
Also, there’s no point in altering the state of the game if we don’t know it has changed. That’s where a set of outputs helps us convey useful information about the game state.
Game Loop
In the previous figure, we can observe three fundamental steps that occur during a game:
- Collecting the inputs from the player.
- Processing the inputs and change the state according to the game rules.
- Generate outputs that describe the updated state of the game.
The core of interactive experiences is the constant repetition of these three steps. It is what sets videogames apart from other pieces of media (movies, paintings, music, etc), and even from other types of software.
The fact that these moments occur in the same order in an endless cycle brings the idea of a game loop.
There are two definitions of what a game loop is.
The first is precisely the three-step process we described above. This process is done by the machine in which the game is running and it is implemented in code. This book [2] talks about this programming pattern in detail.
For example, let’s say we are playing the classic Super Mario Bros. on the NES. The game loop is programmed inside the NES and consists of capturing button presses on the controller, processing them to transform game variables, and rendering the game state on the screen.
![]() |
---|
Simple Game Loop. Taken from [2] |
The second definition is most commonly referred to as a gameplay loop. In game design, a gameplay loop is the sequence of actions that the players do repeatedly in the game. This definition focuses on the player’s actions rather than the machine
Following the Super Mario Bros. example, the gameplay loop could be boiled down to running, seeing an obstacle (enemy, pit, hazard), jumping to avoid or remove the obstacle, and running again, all until you reach the end of the level.
![]() |
---|
Gameplay Loop of Super Mario Bros. |
Our job will be to implement the game loop of our chess game by receiving the player’s input moves, processing them with the rules we have created, changing the state of the board, and then displaying the board to the player.
Exercise
Try to think of what the game loop and gameplay loop of chess look like. Create a small diagram of this sequence.
Recalling the design
Let’s get a quick reminder of the architecture we have built so far.
We start with the Game
class. This class will be responsible for implementing the three-step game loop we talked about previously.
The Game
class points to the MoveInput
class in order to retrieve the player’s inputs, validate them, and apply moves on the Board
class. Then, the resulting board will be displayed with the help of the UI
module.
Side note: I took
Board
out of the Move Generation module because its main purpose isn’t generating moves but providing information about the board and support making moves.
This architecture separates inputs, game logic, and outputs into their modules. This proves to be beneficial because we can change the implementation of any module without affecting the rest of the system. For example, we could easily switch between a point-and-click input vs. a keyboard input to receive moves. We could also change from a 2D user interface to text prompts in the console.
Making a point-and-click input
Encapsulating logic that handles input into a single class allows us to choose any input method we want. For the sake of simplicity, I’ll go with the most common one: the player has to click a square with a piece, and then click another square to perform a move.
I encourage you to be creative and implement your way of listening to player input. I already mentioned the keyboard idea, but you could go as crazy as capturing voice commands in the player’s microphone and translating them into moves.
For starters, since we are using the p5.js library, all the elements of our game will be drawn on top of a canvas. A canvas [4] is a special HTML element that’s used to render graphics. It is part of the DOM hierarchy of the webpage in which the game runs.
We can use the select()
[5] function provided by p5.js to find our canvas. This function returns a p5.Element
object that contains a mouseClicked()
function. We can pass a callback to mouseClicked()
that will execute every time there’s a click on the canvas.
class MoveInput{
constructor() {
//listen to click events on the main canvas
select('canvas').mouseClicked(this.#handleClick);
}
#handleClick(){
//process input
}
}
The first step to deciding what to do with a mouse click is to figure out where it landed.
Let’s recall that the Board
class has a Quadrille
object from the p5.quadrille.js [6] library that holds the arrangement of pieces on the board. This is convenient because the Quadrille
class provides functionality to determine the position of the mouse over the quadrille.
However, the Quadrille
object is hidden behind the Board
class public interface. It’d be unwise to expose it to other classes just for this scenario. Instead, we could create a new Quadrille
object that is the same size as the board and sits on top of it.
class MoveInput{
#quadrille;
constructor() {
this.#quadrille = createQuadrille(NUMBER_OF_FILES,
NUMBER_OF_RANKS);
//listen to click events on the main canvas
select('canvas').mouseClicked(()=>{
this.#handleClick();
});
}
#handleClick(){
}
}
Then we use the screenRow()
and screenCol()
functions. These functions receive as parameters the screen coordinates of the mouse in pixels, the position of the quadrille’s upper-left corner, and the cell length.
For the coordinates, p5.js provides the mouseX
and mouseY
global properties, which keep track of the mouse’s position relative to the top-left corner of the canvas.
The position of the top-left corner of the quadrille must be the same as the board. We will pass this information on to the constructor.
The cell length can be retrieved from global UI settings.
Finally, we do some transformations to the result in order to get the clicked rank and file.
class MoveInput{
#quadrille;
constructor(globalBoardPositionX, globalBoardPositionY) {
this.#quadrille = createQuadrille(NUMBER_OF_FILES,
NUMBER_OF_RANKS);
//listen to click events on the main canvas
select('canvas').mouseClicked(()=>{
this.#handleClick(globalBoardPositionX, globalBoardPositionY);
});
}
#handleClick(boardPositionX, boardPositionY){
//process input
let clickedRank = 8 - this.#quadrille.screenRow(mouseY,
boardPositionY,
BOARD_UI_SETTINGS.SQUARE_SIZE);
let clickedFile = 1 + this.#quadrille.screenCol(mouseX,
boardPositionX,
BOARD_UI_SETTINGS.SQUARE_SIZE);
}
}
Now that we know the exact rank and file the player clicked, it’s time to process it. The following diagram shows a decision tree on how to handle a click input. Don’t forget this is my personal design. You can take a different course of action depending on how you want your game to work.
I challenge you to implement this decision tree in code! Consider that we need the Board
object to check if there’s a piece in the start square. Also, note that the screenRow()
and screenCol()
functions return numbers less than 0 or greater than the board size if the player clicks beyond the board limits.
Exercise
Complete the
handleClick()
function by turning the previous decision tree into code.#handleClick(board, boardPositionX, boardPositionY) { let clickedRank = 8 - this.#quadrille.screenRow(mouseY, boardPositionY, BOARD_UI_SETTINGS.SQUARE_SIZE); let clickedFile = this.#quadrille.screenCol(mouseX, boardPositionX, BOARD_UI_SETTINGS.SQUARE_SIZE) + 1; //... decision tree here ... }
Solution
First, we declare two variables that hold the start and destination square of the move. We will check their value later for the conditionals.
class MoveInput{
#moveStart = null;
#moveDestination = null;
constructor(){
// ...
}
#handleClick(board, boardPositionX, boardPositionY){
// ....
}
}
To check whether we clicked inside the board, we test if the result from the screenRow()
and screenCol()
functions is inside the expected range for ranks and files.
#handleClick(board, boardPositionX, boardPositionY) {
//get clicked square
let clickedRank = 8 - this.#quadrille.screenRow(mouseY, boardPositionY, BOARD_UI_SETTINGS.SQUARE_SIZE);
let clickedFile = this.#quadrille.screenCol(mouseX, boardPositionX, BOARD_UI_SETTINGS.SQUARE_SIZE) + 1;
//if click is not within board limits
let isClickWithinBoardLimits = 1 <= clickedRank &&
clickedRank <= NUMBER_OF_RANKS &&
1 <= clickedFile &&
clickedFile <= NUMBER_OF_FILES;
// ...
}
If the click is not within limits, we check if a move was previously started and cancel it. Then we end the program flow.
#handleClick(board, boardPositionX, boardPositionY) {
//get clicked square
let clickedRank = 8 - this.#quadrille.screenRow(mouseY, boardPositionY, BOARD_UI_SETTINGS.SQUARE_SIZE);
let clickedFile = this.#quadrille.screenCol(mouseX, boardPositionX, BOARD_UI_SETTINGS.SQUARE_SIZE) + 1;
//if click is not within board limits
let isClickWithinBoardLimits = 1 <= clickedRank &&
clickedRank <= NUMBER_OF_RANKS &&
1 <= clickedFile &&
clickedFile <= NUMBER_OF_FILES;
if (!isClickWithinBoardLimits) {
//if move was started
if (this.#moveStart !== null) {
//cancel it
this.#cancelMove();
}
return;
}
// ...
}
To set the start square of the move, there must be a piece in the selected square and a start square shouldn’t be selected already. We request board
for pieces in the clicked square.
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//if move start is not set and there's a piece in the selected square
let pieceInClickedSquare = board.getPieceOnRankFile(clickedRank, clickedFile) !== null;
if (this.#moveStart === null && pieceInClickedSquare) {
//set this square as the move start
this.#moveStart = {
rank: clickedRank,
file: clickedFile
}
}
// ...
}
To set the destination square, the start square must be selected already and the destination square not.
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//else if move start is set and destination is not
else if (this.#moveStart !== null && this.#moveDestination === null) {
//set this square as the move destination
this.#moveDestination = {
rank: clickedRank,
file: clickedFile
}
}
}
Additionally, the destination square must be different from the start square. If they match, we cancel the move because it is invalid and we stop the program there.
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//else if move start is set and destination is not
else if (this.#moveStart !== null && this.#moveDestination === null) {
//if the start square and destination square are the same
if ((this.#moveStart.rank === clickedRank && this.#moveStart.file === clickedFile)) {
//cancel move
this.#cancelMove();
return;
}
//set this square as the move destination
this.#moveDestination = {
rank: clickedRank,
file: clickedFile
}
}
}
When the start and destination squares are set, we generate the move. Then, we unset both variables to be ready to listen for the next move.
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//else if move start is set and destination is not
else if (this.#moveStart !== null && this.#moveDestination === null) {
//if the start square and destination square are the same
if ((this.#moveStart.rank === clickedRank && this.#moveStart.file === clickedFile)) {
//cancel move
this.#cancelMove();
return;
}
//set this square as the move destination
this.#moveDestination = {
rank: clickedRank,
file: clickedFile
}
//create move
let inputMove = new Move(this.#moveStart.rank,
this.#moveStart.file,
this.#moveDestination.rank,
this.#moveDestination.file);
//unset start and destination
this.#moveStart = null;
this.#moveDestination = null;
}
}
Finally, the cancelMove()
function just unsets the value of the start and destination square. Seems odd how we didn’t use this function at the end of the previous code. However, there’s a reason for this that I’ll explain shortly.
#cancelMove() {
this.#moveStart = null;
this.#moveDestination = null;
}
The full function looks like this:
#handleClick(board, boardPositionX, boardPositionY) {
//get clicked square
let clickedRank = 8 - this.#quadrille.screenRow(mouseY, boardPositionY, BOARD_UI_SETTINGS.SQUARE_SIZE);
let clickedFile = this.#quadrille.screenCol(mouseX, boardPositionX, BOARD_UI_SETTINGS.SQUARE_SIZE) + 1;
//if click is not within board limits
let isClickWithinBoardLimits = 1 <= clickedRank &&
clickedRank <= NUMBER_OF_RANKS &&
1 <= clickedFile &&
clickedFile <= NUMBER_OF_FILES;
if (!isClickWithinBoardLimits) {
//if move was started
if (this.#moveStart !== null) {
//cancel it
this.#cancelMove();
}
return;
}
//if move start is not set and there's a piece in the selected square
let pieceInClickedSquare = board.getPieceOnRankFile(clickedRank, clickedFile) !== null;
if (this.#moveStart === null && pieceInClickedSquare) {
//set this square as the move start
this.#moveStart = {
rank: clickedRank,
file: clickedFile
}
}
//else if move start is set and destination is not
else if (this.#moveStart !== null && this.#moveDestination === null) {
//if the start square and destination square are the same
if ((this.#moveStart.rank === clickedRank && this.#moveStart.file === clickedFile)) {
//cancel move
this.#cancelMove();
return;
}
//set this square as the move destination
this.#moveDestination = {
rank: clickedRank,
file: clickedFile
}
//create move
let inputMove = new Move(this.#moveStart.rank,
this.#moveStart.file,
this.#moveDestination.rank,
this.#moveDestination.file);
//unset start and destination
this.#moveStart = null;
this.#moveDestination = null;
}
}
Notifying input
The logic for handling input and translating it into moves is there, but, how do we go about making other classes aware that the player made a move?
The easiest option would be that theMoveInput
class points to every class that needs to be notified about player input.
Then, when a move is created, the MoveInput
class will notify each class accordingly.
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//create move
let inputMove = new Move(this.#moveStart.rank,
this.#moveStart.file,
this.#moveDestination.rank,
this.#moveDestination.file);
this.#notifyInput(inputMove);
// ...
}
#notifyInput(move){
game.moveInput(move);
UI.update();
board.makeMove(move);
moveRecord.record(move);
}
Even though this approach works, it is far from the best solution. The MoveInput
class knows too much about the rest of the codebase, which makes it highly coupled and difficult to maintain. Notifying new classes or changing what they do with the input means modifying the code on MoveInput
.
We could remedy this by defining more generic methods in the target classes.
Nonetheless, this makes the classes’ public interface inconsistent. Why would UI have a method that fidgets with input? Besides, there’s the risk of calling these methods outside of the MoveInput
class, which isn’t what we want.
Let’s try reversing the relationship.
Now every class that needs to know about input will point to the MoveInput
class. It’s a noticeable improvement because we have a lot of classes that depend on just one class (high fan-in, low fan-out [7]) rather than one class depending on many other (high fan-out, low fan-in). As a rule of thumb, you want to avoid high fan-out because it makes classes more interdependent and more prone to errors upon modification. The only exceptions are high-level classes that bring the whole program together in a single control flow, like the Game
class for instance.
All good, right? Well… there’s still something missing.
Input can’t be notified because the MoveInput
class doesn’t reference other modules. Therefore, classes have to manually ask if the player made a move.
class Game{
update(){
let move = moveInput.getInput();
if(move != null){
processMove(move);
}
}
}
This has to be done constantly so the game is responsive to the player’s input. As a consequence, the program is wasting cycles checking for input when most of the time the player might not be doing anything while they ponder their next move.
Moreover, if we wanted to listen to other actions like canceling a move or selecting a square, the code gets longer and more complicated.
class Game{
update(){
if(moveInput.moveCanceled()){
// move canceled. Do something...
}else if(moveInput.squareSelected && moveInput.getMove() == null){
// start square selected. Do something...
}else if(moveInput.getMove() != null){
//player made a move. Do something...
}
}
}
This repeats for every class that wants to listen to the same events.
If we are not worried about performance, complexity, or code repeatability, then this solution might be enough.
However, we can do something better by utilizing a commonly known programming pattern called the Observer pattern.
Observer pattern
Imagine you come across an interesting website that hosts a library of newsletters. Among all the options, you find a newsletter that recurrently posts news about your favorite videogame. You want to be notified whenever new content is up.
You could visit the newsletter from time to time looking for updates. However, this is inconvenient. Life is so busy that you rarely get the chance to check the website. After a couple of days, you finally get some free time to visit the newsletter, only to realize you missed a limited-time in-game event that offered incredible rewards to players.
The website could take the matter into their hands and decide to send notifications about everything that happens on the website to its visitors. You are at work looking for an important email your boss told you to read. Shockingly, you find yourself drowned in hundreds of emails from that website that aren’t even related to the newsletter you are interested in. Sadly, you decide to block the website as spam and you never heard of your favorite game again.
Please no. Taken from [8] |
Time is wasted on sending information that’s irrelevant or looking for the information you want. There should be an easy way to receive notifications on specific parts of the website and discard the rest.
Fortunately, the website introduces a subscription feature. This subscription grants that you receive instant notifications just from the newsletter you subscribe to.
Voilá! Problem solved! That’s basically what the observer pattern is about.
The observer pattern defines classes that behave as publishers and ones that behave as subscribers. Publishers define a set of events that might be of interest to subscribers. Subscribers will link themselves to the publisher’s events to receive a notification when any of those events happen. Generally, publishers are not aware of who their subscribers are. They just send a broadcast message that’s targeted towards anyone who happens to be listening.
![]() |
---|
Observer pattern illustrated. Taken from [9] |
I won’t go into detail on the specifics of the Observer pattern because there are plenty of resources out there (like this article from Refactoring Guru [9]) that explain it better than me.
I believe it is more valuable to demonstrate how the observer pattern is implemented in JavaScript and how you can create custom events for your application.
Events in Javascript
Events are occurrences during an application that trigger the execution of some code. Javascript supports events due to their extensive use in websites. Elements like buttons, menus, text fields, toggles, etc, must trigger actions that change the layout or content of the webpage.
The EventTarget
[4] interface is used on any class that publishes events. Objects that inherit from EventTarget
extend the addEventListener()
function, which allows listeners (another word for subscribers) to follow the object’s events.
Let’s see how that looks:
class MoveInput extends EventTarget{
//... class code ...
}
class Game {
#moveInput;
constructor{
this.#moveInput = new MoveInput();
this.#moveInput.addEventListener("inputEvent",this.processInput);
}
processInput(event){
//code to react to move input
}
}
The Game
class wants to listen to player input. Hence, it adds a listener to an event called "inputEvent"
. Then, it specifies that when "inputEvent"
is triggered, the code inside onMoveInput()
is executed.
"inputEvent"
is a string that represents the name of the event type. There are a couple of event types by default, like “click”, which watches for mouse clicks.
The processInput()
function works as a callback. A callback is a function that is passed as an argument to another function. Then, the callback is executed inside the second function. Remember that Javascript supports first-class functions [4], meaning functions can be treated as variables and function arguments. This is useful because we can use the variable whenever we want (when an event occurs, for example), which will effectively run the code inside the callback.
processInput()
callback receives an event
object, which provides details about the event that triggered the callback.
We want to define custom event types related to player input. Here are the ones I defined:
class MoveInput extends EventTarget{
static inputEvents = {
onMoveInput: "user:move-input",
onSquareSelected: "user:square-selected",
onMoveCanceled: "system:move-canceled",
onMoveStartSet: "user:move-start-set",
onMoveDestinationSet: "user:move-destination-set"
}
}
You can name the event type as you please. The name of my event types describe the source of the event (“user” for actions performed by the user, and “system” for actions done by the code) and a short description of the event. This helps with debugging because we can identify the event type that generates errors.
Here’s an example of how to subscribe to an event of the MoveInput
class:
class Game {
#moveInput;
constructor{
this.#moveInput = new MoveInput();
this.#moveInput.addEventListener(MoveInput.inputEvents.onMoveInput,this.processPlayerInput);
}
processPlayerInput(event){
// ...
}
}
Now, whenever MoveInput
triggers an event object that identifies with the event type onMoveInput
, processPlayerInput
will execute.
Notice how the event types are not coupled to any particular input implementation. Our input could be a mouse or a keyboard, and the Game
class won’t be aware of that. All that matters is that it will be ready to listen to those events when the MoveInput
class decides to fire them.
Creating and triggering events
We have just defined what event types will be available for clients. We still have to create the event object and trigger it in the appropriate place.
Event objects implement the Event
interface. Therefore, we create an event the same way we instance an object.
Let’s look at an example for the "onMoveInput"
event type.
class MoveInput extends EventTarget{
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//create move
let inputMove = new Move(this.#moveStart.rank,
this.#moveStart.file,
this.#moveDestination.rank,
this.#moveDestination.file);
let moveInputEvent = new Event(MoveInput.inputEvents.onMoveInput);
this.dispatchEvent(moveInput);
// ...
}
}
The constructor of the Event
interface receives the name of the event type that identifies the event object.
The dispatchEvent()
function is provided by the EventTarget
interface. This function officially fires the event object, which will execute all callbacks subscribed to the event type that the event object identifies with. An important detail is that this process is synchronous. That means the code in handleClick()
will be paused until all callbacks are executed. This might seem irrelevant to you but, just keep in mind if you see some code running earlier or later than expected.
Also, this might seem obvious, but make sure you fire the event the moment the action actually occurs.
For example, where would you trigger the onMoveCanceled
event? You might think this is the right place:
class MoveInput extends EventTarget{
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//unset start and destination
this.#moveStart = null;
this.#moveDestination = null;
let moveCanceled = new CustomEvent(MoveInput.inputEvents.onMoveCanceled);
this.dispatchEvent(moveCanceled);
}
#cancelMove() {
this.#moveStart = null;
this.#moveDestination = null;
let moveCanceled = new CustomEvent(MoveInput.inputEvents.onMoveCanceled);
this.dispatchEvent(moveCanceled);
}
}
Even though we set moveStart
and moveDestination
to null in both functions, the code’s intent on handleClick()
is not to cancel the move. The move was already created and we are setting these variables to null so we can listen to the next move. In cancelMove()
, we do want to cancel the move, so it’s reasonable to fire the event here. This is the reason why cancelMove()
is not used at the end of handleClick()
.
Custom Events
Usually, when we trigger an event, we want to send some additional data related to the event that might be of interest to client classes.
How do we accomplish this?
Well, there’s an interface that inherits from Event
called CustomEvent
. This interface declares the details
property, which can contain any information we want to pass along with the event.
Going back to the onMoveInput
example:
class MoveInput extends EventTarget{
#handleClick(board, boardPositionX, boardPositionY) {
// ...
//create move
let inputMove = new Move(this.#moveStart.rank,
this.#moveStart.file,
this.#moveDestination.rank,
this.#moveDestination.file);
let moveInputEvent = new CustomEvent(
MoveInput.inputEvents.onMoveInput,
{ detail: { move: inputMove } });
this.dispatchEvent(moveInput);
// ...
}
}
class Game{
#moveInput;
constructor{
this.#moveInput = new MoveInput();
this.#moveInput.addEventListener(MoveInput.inputEvents.onMoveInput,this.processPlayerInput);
}
processPlayerInput(event){
//get input move
let inputMove = event.detail.move;
//do stuff with the input move ...
}
}
The custom event is initialized the same way as a normal one, except we assign a literal object to the detail
property. This object has a property called move
which points to the move object we created. Then, the Game
class can access this information through the event
object.
Bind and scope
One last minor but crucial detail…
We explained that callbacks are functions that are called inside another function. Let’s review that idea closely:
class Game {
#moveInput;
constructor{
this.#moveInput = new MoveInput();
this.#moveInput.addEventListener(MoveInput.inputEvents.onMoveInput,this.processPlayerInput);
}
processPlayerInput(event){
//get input move
let inputMove = event.detail.move;
//if the move is legal, make it on the board
if (this.isMoveLegal(inputMove)) {
this.board.makeMove(inputMove);
}
}
}
Here we attach a callback called processPlayerInput()
to the onMoveInput
event. This callback retrieves the input move, verifies it is legal, and applies it to the board. This should work flawlessly, right?
Not so fast tiger. Remember the definition of callback. Where will this callback be executed exactly?
Exactly! Within the scope of the MoveInput
object instance. More precisely, during the execution of dispatchEvent()
.
So, if the callback is within the scope of another class, what does the this
keyword point to?
Initially, we want this
to point to the Game
class so we can access its properties, like board
, and methods, like isMoveLegal()
. However, because we are inside the scope of MoveInput
, this
will point to the MoveInput
class and we won’t be able to access those elements when the callback runs.
This results in an error because those properties and methods are not defined by MoveInput
.
![]() |
---|
The program cannot find the definition of isMoveLegal() within MoveInput |
That’s a bummer. How can we solve this inconvenience?
Of course, I wouldn’t be telling you this if there were not a solution.
There is a function called bind which allows us to decide what the this
keyword will point to. This way we can provide the right context in which the callback can execute correctly.
class Game {
#moveInput;
constructor{
this.#moveInput = new MoveInput();
this.#moveInput.addEventListener(
MoveInput.inputEvents.onMoveInput,
this.processPlayerInput.bind(this));
}
When adding the event listener, we call the bind()
function on processPlayerInput()
and pass the Game
object. Now,this
will point to Game
and the callback will be able to access its properties.
Conclusion
Input is a huge topic in game development and software engineering. There are infinite ways to capture player input depending on the tech stack, target application, and design of the user experience, among other factors. I hope this example offered you some inspiration to implement an input system for your game and future projects. Most programming languages support the use of events, so you should not have any problem using them in your setup.
References
- [1] Bharambe, A. (2016). Game Architecture and Design. SlidePlayer. https://slideplayer.com/slide/10945729/
- [2] Nystrom, R. (2014). Game loop. Game Loop · Sequencing Patterns · Game Programming Patterns. https://gameprogrammingpatterns.com/game-loop.html
- [3] Davies, C. (2018, January 3). How Super Mario Bros. World 1-1 teaches you everything you need to know. Mainstream404. https://mainstream404.wordpress.com/2018/01/03/how-super-mario-bros-world-1-1-teaches-you-everything-you-need-to-know/
- [4] Web technology for developers: MDN. MDN Web Docs. (n.d.). https://developer.mozilla.org/en-US/docs/Web
- [5] select. p5.js. (n.d.). https://p5js.org/reference/p5/select/
- [6] Charalambos, J. P. (n.d.). Quadrille API. Objetos. https://objetos.github.io/docs/api/
- [7] Hearth, C. (2022, August 29). Design your codebase with low fan-out, high fan-in classes. Hearthside. https://calebhearth.com/fan-out-vs-fan-in
- [8] Goofygoober. (2018, April 6). Emails spirited away gif. Tenor. https://tenor.com/es/view/emails-spirited-away-gif-11576770
- [9] Observer. Refactoring.Guru. (n.d.). https://refactoring.guru/design-patterns/observer