Part 2/2 - Game in ReactJS - Cuzzle

In these articles I would like to share with you the process of development of the latest game that I developed called Cuzzle (cube + puzzle = Cuzzle) ReactJS, this game was inspired by the original game called cuzzle developed by Redline Games

In the first part, we discuss the game and its options, in this part we are going to talk about the technical aspects.

You can play the game online here: https://cuzzle-react.vercel.app/

1. Stack.

As you know the game is developed in ReactJS, other main libraries that I used were:

  • Create React App (CRA): This is a library that I have used previously for other games/projects, this is an easy starting point for a react project because we have all our environment configured, for example, we have webpack, hot reloading, service workers (via workbox) and other features ready to be used, for this type of project I think is the best way to learn React.

  • React Spring: Another library in my toolbox of favorite libraries, this library allows us to create animations, in my case I use this library for cubes movements, I like how we can use a hook to indicate movement, for this project I used useSpring hook, in this hook we can indicate the type of animation that we need, also we have an event (onRest) that indicates when the animation ends.

  • Nuka Carousel: This is a good library to manage carousels, I like the simplicity that this library has. I have used this library in the list of levels.

  • Reach Router: This is a library from the same developers of react-router, as the name implies it’s possible to set different routers/pages, in the case of the game we have five routes.

  • qrcode-decoder and qrcode.react: The first library helps to read the information of our QR code, with the second we can create the QR, in this QR we save our level (actually the information we save is the URL, as mentioned in the first part the level created in the editor is saved in the URL).

  • howler: Is an audio library. It defaults to Web Audio API and falls back to HTML5 Audio. This makes working with audio in JavaScript easy and reliable across all platforms.

  • react-keyboard-event-handler: A React component for handling keyboard events (keyup, keydown and keypress), it's only available in the desktop version.

  • sweetalert: A beautiful, responsive, customizable, accessible replacement for Javascript's popup boxes (alert, confirm and prompt).

  • share-api-polyfill: This is a polyfill for Web Share API which can also be used on the desktop, it is very useful because if the browser does not support the Web Share API, the user can still share the information without problems.

Also I used other packages like prop-types and lodash.clonedeep

Other devDependencies used were:

  • Vercel: This package help to publish the game in the service with the same name, this service is very good, it’s simple to publish a project, I’m using this service for several years (even when the name of the service was now)

  • Serve: Sometimes was necessary to test the game on real devices, in that case, I used this package to create an URL that I can use on my local (when I needed to share the URL with other people I used ngrok)

  • CodeSpaces This is a great tool, because this allows us to have a complete development environment in the cloud, I had the option to open the project on different devices through the browser or open it in VS code, this was very useful for me, because I had the option to develop the game on different computers (even on an iPad).

2. CSS.

Most of the interface of the game was developed in CSS, sometimes I use SVGs in this case for the icons, I know there are a lot of good libraries to use with CSS, in my case I used “regular CSS” In this case, each component has their CSS file (if it’s necessary) sometimes when I need to change something in the interface I used the style property or using the className method, but also I used the option to share variables between CSS and JS, in this case with the setProperty (create a CSS variable o modify its value).

In games like this it is possible to use canvas, sometimes it's the best option, for this particular game (actually almost all the games I have developed in react don't use canvas) I only use CSS, for me CSS is enough, yes, the performance is sometimes not the best, but for me the idea is to learn, not just ReactJS but CSS as well.

3. Technical challenges.

This is the second isometric game that I have developed (I previously developed a game with isometric style Hocus). My previous games/projects were mostly 2D, the challenges in this project were mainly related to the depth of the cubes.

Component creation

With ReactJS (and other libraries/frameworks based on components) it's "easy" to create new components (and reuse them), in this game we have several components, but it was a special challenge creating visual components like:

  • Floors: As we saw in the first part we have different types of floors, but at the end of the day it is just one component that changes its behavior based on its props
import "./styles.css";
import { SIZE_FLOOR } from "../../../utils/constants";
import PropTypes from "prop-types";
import React from "react";
import Variants from "./variants";

const Floor = React.memo(
  ({
    style = {},
    size = SIZE_FLOOR,
    type = "",
    animated = true,
    shake = false,
    up = false,
  }) => (
    <div
      className={`floor-wrapper ${shake ? "shake" : ""} ${up ? "up" : ""}`}
      style={{ ...style, width: size, height: size + 20 }}
    >
      {type && <Variants type={type} animated={animated} />}
      <div className="floor-base" />
      <div className="floor" />
    </div>
  )
);

Floor.propTypes = {
  style: PropTypes.object,
  size: PropTypes.number,
  type: PropTypes.string,
  animated: PropTypes.bool,
  shake: PropTypes.bool,
  up: PropTypes.bool,
};

export default Floor;

In this example we can see the <Floor /> component, but also we can see the <Variants /> component.

import { SUFIX_ARRIVAL_POINT } from "../../../../utils/constants";
import ArrivalPoint from "./arrivalPoint";
import Portal from "./portal";
import Switch from "./switch";

const Variants = (props) => {
  let finalType = props.type || "";
  const extendprops = {
    ...props,
  };

  if (props.type.includes(SUFIX_ARRIVAL_POINT)) {
    finalType = SUFIX_ARRIVAL_POINT;
    extendprops.color = props.type.split("-")[2] || "white";
  }

  const variants = {
    switch: <Switch />,
    "arrival-point": <ArrivalPoint {...extendprops} />,
    portal: <Portal {...extendprops} />,
  };

  return variants[finalType] || null;
};

export default Variants;
  • Cube: This component is simpler compare to floors, but only when it's a static cube.
import "./styles.css";
import { SIZE_FLOOR } from "../../../utils/constants";
import PropTypes from "prop-types";
import React from "react";

const Cube = React.memo(
  ({ style = {}, size = SIZE_FLOOR - 25, color = "", opacity = false }) => {
    return (
      <div
        className={`cube ${color}${opacity ? " opacity" : ""}`}
        style={{ ...style, width: size, height: size + 20 }}
      />
    );
  }
);

Cube.propTypes = {
  style: PropTypes.object,
  color: PropTypes.string,
  opacity: PropTypes.bool,
  size: PropTypes.number,
};

export default Cube;

As you can see it's just one div, I like to use just one div when possible, sometimes it's possible to do a lot of things with just one div and I think it's good, because as you know when we use less DOM elements it's good for performance.

Who creates the "magic" to show a cube is CSS, in this case using the transform and linear-gradient properties with the before pseudo-element

Cube depth

In a 2D game, the elements are at the same level, but in games like this, depth is an important aspect to take into consideration.

As you know in CSS we have the concept of stacking context, when we have two elements with absolute positions the element that is in the right position and/or below by default will be on top of the other element, if we want to change that behavior it is necessary to use the z-index property, but for it to work all elements need to be in the same stacking context.

I use the z-index only in the component where the user can move the cubes, in the editor wasn’t necessary because all the elements are static at that point, all the floors render from left to right and top to down, in this case, all the floors have an absolute position (the positions were calculated by a helper) due to behavior of rendering by default a floor have the right position and depth with respect to the other floors, I mean the next floor will have a greater depth than the previous one, the same strategy was applied for the cubes with one exception, in this case, the cubes have a z-index (the same z-index), the reason is, it was necessary to make sure the cubes are located above the floors, I don’t use another container for the cubes, the floors and the cubes are in the same container.

Movement of the cubes

This was one of the most challenging parts of the game because I needed to move the cube or cubes (sometimes the player can move two cubes at the same time), to do that I used the react-spring library, with this library and its useSpring hook I can indicate the move I need (left, top), the move only happens if it passes some validations

A cube only can move if:

There is a floor:, if the player wants to move a cube to a new position, that position needs to have a floor, it sounds fun to say that, but a level code was necessary to have such validation.

In front, there is another cube and there is a floor: In this case, the validation of the floor is made for the second cube, this means when the main cube has in front of another cube, it’s necessary to know if the new position of the second cube has a floor.

In front, there is another cube and in front of that cube, there is no other cube: This scenario is when we have three cubes, the main cube wants to move to a position where there is another cube, now it’s necessary to know if the new poison the second cube is available, if in that position there is another cube, there will be no movement.

Portal validation.

Validate this type of cube was also challenging, some considerations that were necessary to have:

Not accepting movement input while teleporting a cube: when the user indicates the movement, it is possible to maintain a sustained movement, this means that the user does not need to leave the cursor finger (or keyboard in desktop) to indicate a new movement, wit portals it’s a little different because when a cube enters to a portal, this cube change to a new position, at that moment we have an animation that shows the change, to prevent a strange behavior, the input from the user is ignored, when the teleportation animation ends, the inputs are enabled again.

Validate that the portal exit point is available: All the cubes in the game can be teleport, if a regular cube is teleported the exit of the portal is blocked for that cube, in that case when a cube enters a portal the cube moves normally, this means that the portal behaves like a regular floor, this is convenient sometimes for some levels.

Undo validation.

The game has the option to restart a level, but the undo action is different because it needs to show the level in the previous state, to achieve this action was necessary to create a new state, in this state we save the previous state of the level, but no all the complete state, this means, that it only saves the things that change at that moment, for example, if the user only moves a cube, it’s not necessary to save the state of the floors or the state of the other cubes.

For example when a cube or cubes move, in that case, we save in an object called floors, the previous position where the cube was, always we have at least a cube moving.

But when it’s necessary to validate undo option for floors we have to take into consideration the type of the floor, for example, if the floor is a shaking floor, when the undo option is applied it’s necessary to show the floor again in the same position that we have before, other case is when we have a switch floor, it’s necessary to save the floors that were related with the switch and hide that floors again.

For portals the logic was different, in this case, we don’t have the teleportation animation because it’s possible to press the undo button many times, in that case, we only save the previous position of the cube and when the user press the undo button, the cube returns to the previous position, in that case, we see an animation of the cube from the current position to its previous position.

Save level solution

The state that was used for the undo option also was utilized for the solution option, in this case, only it saves this data when we are in the editor, the same state was utilized because when the user uses the undo option, at the same time is removing that movement from the solution.

The solution is saved in an array, this array contains the valid moves (if a user made a movement and the cube doesn’t move that movement isn’t saved) to prevent a big solution the editor has a limit of 250 movements, but a potential array of 250 movements continues to be a big value, that’s why the solution has a special treatment to save, in this case, the array is converted to a string, eliminating the repeated values ​​and saving the number of times a value is repeated (when it is consecutive), when a level is loaded there is a conversion to the array again.

For example 37,27,12,42,32,44,23 is the solution for the level in the gif, in this case 37 means that it's necessary move the cube seven times to the position 3 (right-top)

Run solution.

The user can play many times a particular level, can restart it, and also can use the undo option the times that are necessary, but there are moments that a level is very complex, that is why the game has the option to show the solution of the level, that is the reason why in the editor to play the level and validate that the level has a solution, that solution is saved and later is executed to show the solution.

In other game that I developed Mr. Square, we have the same option to show the solution of the level, but in that case, the user needs to follow the movements to complete the level, but this game is different because the user doesn’t need to make any movement, all the movements are automatically executed, in this case the player only is a viewer, because of that, was necessary to disable all the inputs for the user, also if the user had made movements before, was necessary to restart el level before to start executing the solution.

Conclusions.

This game was a good challenge for me, this is the ninth game that I develop in ReactJS, with each game I learn something new, I do not consider myself a game developer, I do this type of project because it is fun and at the same time I learn new concepts of this library.

In these articles I wanted to share on a high level how I developed this game, the game code at the moment is private, just like the other games, my idea is maybe to create a course where I can show you how you can develop these games.

I'm not a native english speaker, actually this is my first articule in English, to be honest was more complex to me write these articules that develop the game 😅, but was a good experience, the next game (I have a new game in my head right now 😅) that I develop the idea is to write another articles in English, I know that I can improve day by day.

Thanks for reading these articles, you can find me at:

14