Welcome back! In the previous blog post, you set up the canvas and created drawings on it. Now in this post, you'll draw the game board.
What Is A Game Object?#
The game features multiple drawings on the canvas. These distinct drawings take up space both horizontally and vertically. They may be made up of other smaller related drawings. These distinct drawings will be referred to as "game objects".
Why Do We Need Game Objects?#
Looking back at the breakdown of the game's UI. Three components can be identified:
- Status Area
- Game Board
- Play Again Button
All of these components share the following things in common:
- They have position
- They have dimensions
- All drawn on the canvas
When referring to these components in a generalised way, we'll be considering these common traits and properties only. In the context of this game, the general name for these components will be "Game Object". The components listed above are all game objects.
To represent this relationship in code, you'll create a
GameObject class. The component classes will inherit from the
GameObject class. This will avoid you from rewriting the common logic and properties shared across all the components.
Creating The GameObject Class#
Unlike with the HTML elements, you will have to implement the size and positioning of game objects yourself. You'll also have to draw the game objects yourself too.
Create a directory under the
src directory called
src/components directory, create a new file called
src/components/GameObject.js, add the following to the file:
Inheriting The GameObject Class#
Now that you've created the
GameObject class, in the
src/components directory, create a new file named
Board.js. After, in that file, create a
Board class that inherits from the
To test out whether the
GameObject is being inherited by
Board correctly, you'll recreate the last post's white rectangle drawing using the
Add a method called
render() to the
Board class that will render a white rectangle and then restore the state of the context object:
Notice how the
height fields were not defined in
Board. This is because they were inherited from
Create a new file in
index.js and fill it with the following contents:
This code block above exports the
Board class as a module, making it available to import from
src/components/index.js. There will be more components in the future. This change will simplify the code required to import multiple components over time.
Return to the
FrontEnd class. Import the
Board class from
Add a field called
start() method in the
- Set the
boardfield to a new instance of the
- Call the
Notice that the
Board uses the same constructor defined in
If you check the game with your web server, you'll see the same white rectangle drawn on the canvas as the one you drew in the previous blog post.
Drawing The Game Board#
Awesome! You've figured out how to create your game object and draw it on the canvas. Now it's time to draw the actual game board.
BoardConfig from the constants file:
Then create a new method in the
Board class called
After, perform the following steps:
- Move the code that draws the white rectangle into the
- Replace the fill style with the
BACKGROUND_COLORfield defined in the imported
FrontEnd class, import
BoardConfig from the
Add a method called
createBoard() then do the following:
createBoard(), create a local variable that stores a new instance of the
Boardclass, calls the
render()method on it then returns it.
- Rewrite the
start()method so that the
boardfield is set to the result returned from calling the
The board is now created with the positioning and dimensions from mockups of the game.
Now, with a web server running, if you check your game in your browser, you'll see that a blue rectangle has been rendered on the canvas.
Now you're ready to draw the slots on the board.
Add a parameter called
nextBoard to the
nextBoard will contain the state of the board. It's an array of arrays of numbers representing the tokens in each board position. These will be used to render the board tokens in the slots.
Next, add a method called
renderSlots() to the
Board class. To keep the upcoming drawing commands simple, you'll modify the point of origin where you start drawing the slots. This way, you won't have to consider padding in later drawing commands in
renderSlots(). To do this, you'll use the
Start drawing the slots.
Set the stroke colour and line width:
this.context.strokeStyle = BoardConfig.SLOT_OUTLINE_COLOR; this.context.lineWidth = 2;
Store the radius of a slot in a local variable called
Update import statements in
src/components/Board.js to include
TokenColor from the
constants directory and
Constants from the
; ; ;
Loop through each slot on the board and:
- Calculate the positioning:
- Obtain the token colour to render the slot with
for ; rowIndex < Constants.BoardDimensions.ROWS; rowIndex++
You now have the values you need to render each slot. To do so, add a method called
renderSlot() to the
renderSlot() right after the
switch block in
Add a call to
renderSlots() in the
Lastly, in the
FrontEnd class, update the
board.render() method call in
createBoard() so that you pass in the current board state:
If you check your browser now while your server is running, you'll see empty slots drawn on the board:
Making The Game Playable#
You can attempt to hardcode a board state argument when calling the
render() method on the
board variable. The game will render the board state accordingly.
However, you currently aren't able to update the board state in-game.
One way to update the board state in-game is by responding to clicks on the canvas.
Unlike with HTML elements, you can't just rely on the built-in DOM events system to handle clicks on our game objects. Since you're drawing on the canvas, you'll have to handle the hit detection yourself and come up with your own event-handling interface.
Introducing Your Event Handling API#
In this game, you'll detect clicks by listening for the click event on the body of the page and then pass on the event data to relevant game objects to be processed.
To get started with this, add a new method to the
GameObject class called
handleClick() will be overridden by the
GameObject class' inheritors. The inheritors will add logic to the
handleClick() method for handling event data from clicks.
Implementing Your Event Handling API#
Now, in the
Board class, add a field called
columnSelected. This will be used to store a callback. The callback will contain logic to run that will be defined in the
After that, add a method called
setColumnSelectionHandler(), which will be used to set the logic that will run from the callback:
You now have the prerequisites for overriding
handleClick() in the
Add the following methods to the
trySelectColumn() method figures out if the player clicked a column. If the player has clicked on a column, it will run the
columnSelected callback with the selected column's position index. For example, the leftmost column would have an index of
0, and the third column would have an index of
Adding the callback logic#
Finally, you'll set up the click events in the
FrontEnd class and handle them.
start method, add an event listener for clicks on the document's body. This will call the
handleClick() method on the board with event data:
Now, you'll add the logic that will run in the
Add a field called
gameOver in the
FrontEnd class. Set its value to
false in the
FrontEnd class' constructor:
Update the import statements in
src/FrontEnd.js to include
Constants from the
; ; ;
Create the following methods:
createBoard(), add a line before the call to
render() on the board. In that line, call the
If you check your browser now while your server is running, you'll now be able to place tokens on the board, based on which column you clicked on.
Congratulations! This was a long post (maybe the longest in this blog series so far!). Hopefully, it was worthwhile to you now that you've made a playable four-in-a-row game.
However, it's not quite clear what's going on in the game. The board stops updating when a player wins or the game ends in a draw. There's no indication of the current status of the game.
In the next post, you'll fix that problem. You'll add the status area component to the game. This will enable players and spectators to be aware of the current status of the game at any time.