Zosia Korcz

...a girl who wants to make games.

Lost in Cyberspace Co-op VR game

26 Sep 2017
Lost in Cyberspace Co-op VR game Lost in Cyberspace Co-op VR game

This year, Js13kGames contest has a new category called: A-Frame. A-Frame is a library that helps building virtual reality experiences. I, together with Bartek decided to try it and build a cooperative game around a theme of Lost. I want to share with you some interesting fragments and pitfalls we encountered while developing Lost in CYBERSPACE.

The game is designed for 2 players. The aim is to cooperate to reveal the cyberspace’s map and hack a specific node. One of the player takes role of a hacker who enters the virtual reality maze, wonders around and gives the other player their found codes. Another player - a navigator, uses a terminal and enters the codes received from the hacker. Depending on the number and type of code, the navigator reveals the map of the cyberspace and gives instructions about connections, traps and target to the hacker. The most important part of the game is good and fast communication between the players.

Let’s dive now into some interesting fragments from the codebase.

Generating a random network

Every time when a player starts a new game, the new network is randomly generated. The whole maze’s structure has to be computed in such a way that it may be encoded into 4 codes and easily decoded in the terminal. That’s why it is represented by an object with 4 parameters: connections, sectors (also called as colors), traps and target. Those 4 values are later on used to calculate code values shown in the maze.

Based on that object, the 3D VR maze is generated:

The maze shown in the inspector The maze shown in the A-Frame Inspector

As you can see, there are 4 sectors in the maze, each of them shown in different colors: pink, yellow, blue and green. The orange thin lines cross the node where the camera is located and where the hacker starts their journey.

The randomNetwork method returns an object’s representation of a maze

function randomNetwork() {
  let traps = randomTraps();
  let target = randomTarget();
  let walls = randomWalls();

  while (
    // prevent target from appearing on traps
    traps.trapsXY.some((xy) => xy.join() === target.join()) ||
    // and make sure it appears in a dead end
    walls.connections[target[0]][target[1]] !== 1
  ) {
    traps = randomTraps();
    target = randomTarget();
  }

  return {
    colors: randomColors(),
    traps: traps,
    target: target,
    walls: walls
  };
}

As mentioned before, the randomNetwork method returns object’s representation of a maze described by 4 parameters: colors, traps, target and walls. It is sufficient for walls and colors to be generated only once but traps and target have to be regenerated as long as they meet specific requirements. Target for instance cannot be placed in the same node with trap and it always has to be a dead end so it will be more challenging to reach it.

The randomTraps() method looks like this:

function randomTraps() {
  let trapsSeed = [
    randomInt(16), //generate random integer from 0-15
    randomInt(16),
    randomInt(16),
    randomInt(16),
  ];

  return createTrapsObject(trapsSeed);
}

function createTrapsObject(trapsSeed) {
  let trapsXY = [];

  for (let i = 0; i < 4; i++) {
    // first seed is number 0-15 (from the code)
    let seed1 = trapsSeed[i];
    // second seed is computed as shifted sum of 2 next seeds
    let seed2 = (i*4 + trapsSeed[(i + 1) % 4] + trapsSeed[(i + 2) % 4]) % 16;

    // make sure both seeds are not the same (so traps don't overlap)
    if (seed1 === seed2) {
      seed2 = 15 - seed1;
    }

    [seed1, seed2].forEach(seed => {
      let xy = [seed % 4, ~~(seed / 4)];
      if (i === 1 || i === 3) {
        xy[0] = xy[0] + 4; // move x coord for sectors B and D
      }
      if (i === 2 || i === 3) {
        xy[1] = xy[1] + 4; // move y coord for sectors C and D
      }
      trapsXY.push(xy);
    });
  }

  return {
    trapsSeed: trapsSeed,
    trapsXY: trapsXY
  };
}

First, four random integers which represents a sector are computed and saved into trapsSeed array. There are 16 nodes in each quarter of the maze, so the random range is from 0 to 15. The traps' coordinates in all of the sectors are based on 2 seed values. The first one is just a previously generated integer. The second seed is calculated as shifted sum of the 2 next integers from the trapsSeed array. Then, after making sure that traps won’t overlap, each seed is used to calculate the coordinates of traps. The coordinates’ array called xy is built based on seed value modulo 4 and the integer value of seed divided by 4. The last thing before pushing those coords to the returned array is to shift them by adding 4 towards either right or bottom dependently on the current sector.

The above example shows a method of generating the traps object only. For the sake of simplicity I will omit the whole method of generating the maze’s objects.

Creating unique shared codes

One of the trickiest part of the game was generating the unique codes in such a way that they will correspond to each other on two remote devices. There is no unique url parameter or id that has to generated and shared between a pair of players. The magic lies in the method of generating the codes found on nodes in the maze.

As mentioned before, each maze is described by 4 parameters: connections, sectors, traps and target. Based on the network’s object representation, the unique codes for each of those parameters are generated and hidden in the maze, one in each sector.

Example code found in the maze Example code found in the maze

Each of such codes 0xTCCCC has the same structure:

  • 0x prefix to make it looks fancy ;),
  • T hex value [0-F] defining type of the code. T % 4 gives a number 0-3: (0: colors, 1: walls, 2: traps, 3: target)
  • C 4 hex values [0-F] defining the code value (depends on the code type)

This is how to generate such a code:

function hexRandomMod4(n) {
  return (n + randomInt(4) * 4).toString(16).toUpperCase();
}

function colorsToCode(colors) {
  let type = hexRandomMod4(0);
  let code = '0x' + type + colors.map(hexRandomMod4).join('');

  return code;
}

As mentioned before, the code consists of the 0x prefix, type and color values. The type is a hex value of randomly generated number from 1-4, multiplied by 4 and added to a specific n number, in this example - 0. The rest of a division by 4 of such a number will give 0. During decoding, it will decide about the type of a given code. The last part of the code is the value. It is a string combined with a hex values generated the same way as type, where the added n number is just the value given in the colors object - one for each sector.

After finding the code, the hacker shares it with the navigator who enters it into the terminal. If the entered codes are valid, the exact same maze should be shown in the terminal. Based on that the navigator may deduce where the hacker currently is and guide them to the target node.

Decoding unique shared codes

When the navigator enters the code into the terminal, it is decoded, interpreted and shown on the map. The decoding method of colors is just the reverse of encoding:

function networkFromCodes(codes) {
(...)
parsed = parseCode(code);

if (parsed) {
  let type = parsed.shift() % 4;

  try {
    switch(type) {
      case 0:
        network.colors = decodeColors(parsed);
        network.colors.code = code;
        break;
        (...)
}


function parseCode(code) {
  code = code
    .replace('0x','')
    .split(''); // turn into array of hex characters

  if (code.length !== 5) {
    throw new Error('Code length is not valid.');
  }

  code = code
    .map(x => parseInt(x, 16)) // parse hex values
    .filter(n => !isNaN(n)); // get only numbers

  if (code.length !== 5) {
    throw new Error('Code contains invalid characters');
  }

  return code;
}

function decodeColors(values) {
  let colors = values.map(n => n % 4);

  let hasDuplicates = colors.some((c,i) => colors.indexOf(c) !== i);

  if (hasDuplicates) {
    throw new Error('Duplicated colors in different sectors');
  }

  return colors;
}

In the parseCode method, the 0x prefix is removed and the rest of the code is split into an array of hex characters. Then each of the character is parsed into an integer. If there are no invalid characters, the parseCode method returns an array of parsed values. The first value in the returned array indicates the type. If the remainder from the division of type and 4 equals 0, the decodeColors method is called. The body of that function just goes through every value indicating color and calculates it modulo 4. Before returning decoded sectors, the decodeColors method validates if all of the colors are unique.

Here is the example of a valid color code:

// 0xC16F8
// C % 4 = 0 (code's type defines color, C for color ;)
// 1 % 4 = 1 (color of sector A is 1)
// 6 % 4 = 2 (color of sector B is 2)
// F % 4 = 3 (color of sector C is 3)
// 8 % 4 = 0 (color of sector D is 0)

and incorrect one:

// 0x44206
// 4 % 4 = 0 (code's type defines color)
// 4 % 4 = 0 (color of sector A is 0)
// 2 % 4 = 2 (color of sector B is 2)
// 0 % 4 = 0 (color of sector C is 0)
// 6 % 4 = 2 (color of sector D is 2)
// Code is invalid as it has duplicated colors

I only demonstrated colors here but connections, traps and target are implemented in a quite similar way.

After entering a correct code, the maze’s map is shown in the terminal.

The mazes map is shown in the terminal The mazes map with revealed sectors shown in the terminal.

The more hints are shown on the terminal map, the easier it becomes for the hacker to reach the correct node and hack it.

Conclusion

I hope that you’ve learnt something today and maybe even felt motivated to try building a VR game of your own. I intentionally didn’t show any fragments of A-Frame code because I wanted to focus on the most crucial parts of the game, not the library itself. If you are willing to learn it’s basics, here is an A-Frame examples’ page and an introduction tutorials to get started.

If you have any questions about the Lost in Cyberspace game, investigate its codebase by yourself and as always feel free to ask about it in the section below.

comments powered by Disqus