I am participating in the nuggets Community game creative submission Contest. For details, please see: Game Creative Submission Contest

The game is introduced

This is a 2D puzzle game, players must dodge enemies and traps to reach the end of multiple levels

You can customize levels and retain data

Implementation technology

vue tailwindcss

This tour features

  • Custom levels
  • The enemy automatically takes on the enemy
  • Low technical force
  • you win!

The technical implementation

Initialization page

Create a JSON file to store the variables of the initial level (only one level…). Set the square size, initialize the variable speed set to 176, the width and height of the board is 4 speed, square width and height is 1 speed, square move 1 space is speed * 1, two space is speed * 2

<! -- -- -- > board
<div :style="{ width: `${speed * 4}px`, height: `${speed * 4}px` }">
    <! -- Every little square -->
    <div :style="{ width: `${speed}px`,height: `${speed}px`,}"></div>
</div>
Copy the code
const speed = ref(176);
Copy the code

Level is a JSON file that contains the variables of the first Level and is used to initialize a Level when there are no levels

level.json

[{"id": 1./ / the first level
        "speed": 176.// Block size
        "top": 528.// Main character top value
        "left": 0.// The main character left value
        "enemy_top": 0.// Enemy top value
        "enemy_left": 352.// Enemy left value
        "enemy_top_2": 528.// Enemy 2's top value
        "enemy_left_2": 352.// Left value of enemy 2
        "obstacle_top": 176.// Obstacle top value
        "obstacle_left": 352.// Block left value
        "trap_top": 352.// Trap top value
        "trap_left": 176.// Trap left value
        "spot_top": 0.// End top value
        "spot_left": 528// End left value}]Copy the code

Determine if there is data when loading the page and if not add it

import Level from ".. /.. /api/level.json";
let res = JSON.parse(localStorage.getItem("data"));
if(! res) {localStorage.setItem("data".JSON.stringify(Level));
}
Copy the code

Box setting

Use absolute positioning and transition-all to animate the block

<div class="absolute transition-all"></div>
Copy the code

Set specific top and left values for the small square, declare variables and set them to the small square

<! -- Finish, I use the spot prefix -->
<div :style="{ top: `${spot_top}px`,left: `${spot_left}px` }"></div>
<! "Enemy" I used the prefix "enemy 2" -->
<div :style="{ top: `${enemy_top}px`,left: `${enemy_left}px` }"></div>
Copy the code
const Level = JSON.parse(localStorage.getItem("data"));
const spot_top = ref(Level[index].spot_top);
const spot_left = ref(Level[index].spot_left);
const enemy_top = ref(Level[index].enemy_top);
const enemy_left = ref(Level[index].enemy_left);
Copy the code

The main character of mobile

The corresponding function is executed when the corresponding key is pressed

document.addEventListener("keydown".(e) = > {
  switch (e.key) {
    case "a":
      if (is_run.value) {
        moveProtagonistA();
      }
      break;
    case "w":
      if (is_run.value) {
        moveProtagonistW();
      }
      break;
    case "d":
      if (is_run.value) {
        moveProtagonistD();
      }
      break;
    case "s":
      if (is_run.value) {
        moveProtagonistS();
      }
      break;
    case "r":
      againGame();// Start over
      break; }});Copy the code

The top and left of each function are different, so I’ll pick one of them to explain in detail:

When you want the main character to move left

const moveProtagonistA = () = > {
  // Suicide judgment
  if (
    left.value == enemy_left.value + speed.value &&
    top.value == enemy_top.value
  ) {
    left.value -= speed.value;
    return false;
  }
  if (left.value == 0) {
    // boundary judgment
    left.value = -20;
    setTimeout(() = > {
      left.value = 0;
    }, 100);
    return false;
  }
  // Handicap judgment
  obstacle = obstacle_left.value + speed.value;
  if (top.value == obstacle_top.value && left.value == obstacle) {
    left.value = obstacle - 20;
    setTimeout(() = > {
      left.value = obstacle;
    }, 100);
  } else{ left.value -= speed.value; freeFindEnemy(enemy_top, enemy_left); freeFindEnemy(enemy_top_2, enemy_left_2); }};Copy the code

The function as a whole is much smaller, so let’s break it down:

Suicide judgment

When the protagonist moves, the enemy’s automatic capture function will also be enabled, so when the protagonist moves to the enemy, the enemy will stagger with the protagonist because of the enemy’s automatic capture. Thus, this logic is born, which is to judge that if the protagonist has an enemy in the next step, the enemy will stay where it is and install the enemy game over

 // Suicide judgment
  if (
    left.value == enemy_left.value + speed.value &&
    top.value == enemy_top.value
  ) {
    left.value -= speed.value;
    return false;// If the suicide is successful, prevent the following enemy judgment
  }
Copy the code

Border judgment

If it goes out of bounds, it will be blocked and give a warning that it is blocked. Since this example is intended to move left, the judgment condition is also left

if (left.value == 0) {
    // This effect can make the block bounce back a bit
    left.value = -20;
    setTimeout(() = > {
      left.value = 0;
    }, 100);
    return false;// If the boundary is encountered, it blocks the detection of enemies like the one below
  }
Copy the code

Obstacle judgment && enemy detection

If there is an obstacle in the level, when the hero touches the obstacle, he will have the same rebound effect as the boundary judgment to indicate that the path is blocked

If the hero is not blocked from moving, the normal move command is executed and the automatic call is executed

obstacle = obstacle_left.value + speed.value;
if (top.value == obstacle_top.value && left.value == obstacle) {
    // As above, bounce back
  left.value = obstacle - 20;
  setTimeout(() = > {
    left.value = obstacle;
  }, 100);
} else {
  left.value -= speed.value;// Move command
  freeFindEnemy(enemy_top, enemy_left);// Call enemy 1
  freeFindEnemy(enemy_top_2, enemy_left_2);// Call enemy 2
}
Copy the code

Perhaps you have already seen (DORA waving her hand) the two functions used at the end of the summoning. This function is the logic for automatic summoning

Automatic spotting,

Enemies automatically capture enemies when the protagonist moves

// Automatic attack
const freeFindEnemy = (Etop: any, Eleft: any) = > {
  let _top = top.value - Etop.value;
  let _left = left.value - Eleft.value;
  if (Math.abs(_top) > Math.abs(_left)) {
    if (_top > 0) {
      moveEnemyS(Etop, Eleft);
    } else{ moveEnemyW(Etop, Eleft); }}else {
    if (_left > 0) {
      moveEnemyD(Etop, Eleft);
    } else{ moveEnemyA(Etop, Eleft); }}};Copy the code

The function moveEnemy series is the direction of the enemy square movement, the logic is to judge the leading role from the enemy’s top and left to determine the direction of the enemy square, Etop and Eleft need to pass in the enemy’s top and left values, judge the edge of the distance to which action, there are greater than, less than or equal to two cases

By automatic detection of the enemy and extended out – enemy movement

The enemy move

The enemy movement also has four functions, basically the same as the hero movement, but the enemy will choose to bypass when they encounter obstacles, and the enemy will be “eaten” when they encounter traps.

Take an enemy moving down

const moveEnemyS = (Etop: any, Eleft: any) = > {
  // Trap judgment
  if (trap_top.value == Etop.value && trap_left.value == Eleft.value) return;
  // Obstacle detection judgment
  obstacle = obstacle_top.value - speed.value;
  if (Etop.value == obstacle && Eleft.value == obstacle_left.value) {
    // Determine if obstacles are encountered
    let _left = left.value - Eleft.value;
    if (_left > 0) {
      Eleft.value += speed.value;
    } else{ Eleft.value -= speed.value; }}else{ Etop.value += speed.value; }};Copy the code

The first is the judgment of the trap. If the top and left of the enemy are consistent with the trap, the enemy is judged to have fallen into the trap, and all the movement of the enemy will be terminated

The next step is obstacle. If there is an obstacle in the direction the enemy is about to go, judge the distance to the protagonist to avoid to the left or right

Victory and defeat

After the victory and defeat is certainly want to terminate all action, just all the action is triggered by leading mobile function, so declare a variable is used to control the game first, and then through the buttons in judging the variables, if the game is underway in triggering the mobile function, if the game did not start or has failed skip trigger events, namely no response

case "a":
  Is_run is a declared variable that is false if the game fails or does not start
  if (is_run.value) {
    moveProtagonistA();
  }
  break;
Copy the code

Win is triggered when the victory condition is met (i.e. the protagonist hits the finish line), displaying win and setting is_run to false

// Whether the protagonist's topleft overlaps with the endpoint's topleft
if (top.value == spot_top.value && left.value == spot_left.value) {
    winShow.value = true;
    is_run.value = false;
  }
Copy the code

When the failure condition is met (i.e. the protagonist encounters enemy 1 or 2 or a trap), lose is triggered, displaying the word LOSE and setting IS_run to false

if (
    (top.value == enemy_top.value && left.value == enemy_left.value) ||
    (top.value == enemy_top_2.value && left.value == enemy_left_2.value) ||
    (top.value == trap_top.value && left.value == trap_left.value)
  ) {
    is_run.value = false;
    loseShow.value = true;
    return;
  }
Copy the code
  • The last return is a truncation, and when a lose is triggered the execution is stopped (otherwise win will continue).

Edit levels

Discoloration in and out

16 black blocks, judge the color by moving the mouse in and out

<div
  v-for="(item, index) in blockList"
  :key="index"
  :style="{ width: `${speed}px`, height: `${speed}px`, background: item.background, }"
  @mousemove="editMove($event, item)"
  @mouseleave="editLeave"
  class="transition-all"
></div>
<! -- transition-all -->
Copy the code
const editMove = (event, item) = > {
  // Do nothing if the box is already selected
  if(! item.is_confirm) {for (let i in blockList.value) {
      // Select the appropriate box to change color
      if (blockList.value[i].id == item.id) {
        blockList.value[i].background = "";
      } else if (blockList.value[i].is_confirm) {
        blockList.value[i].background = "";
      } else {
        blockList.value[i].background = "# 000"; }}}};const editLeave = () = > {
  for (let i in blockList.value) {
    // Do nothing if the box is already selected
    if (blockList.value[i].is_confirm) {
      blockList.value[i].background = "";
    } else {
       // Select the appropriate box to change color
      blockList.value[i].background = "# 000"; }}};Copy the code

Since squares cannot be changed color after being set, these two methods are needed to determine the squares that have been set

Click Settings

First click on the left legend to make the color selected, and then click on the box to make it change color

legend

<div
  v-for="(item, index) in legendList"
  :key="index"
  class="flex mb-4 items-center text-xl"
  @click="colorClick($event, item)"
>
  <div class="legend_sign" :class="item.color"></div>
  <div class="w-10"></div>
  <div
    class="transition-all p-2 rounded-lg"
    :class="color == item.color ? color : ''"
  >
    {{ item.introduce }}
  </div>
</div>
Copy the code
const legendList = [
  {
    id: 0.color: "bg-green-500".introduce: "The end"}, {id: 1.color: "bg-red-500".introduce: "The enemy"}, {id: 2.color: "bg-blue-500".introduce: "Hero"}, {id: 3.color: "bg-gray-500".introduce: "Obstacles"}, {id: 4.color: "bg-purple-500".introduce: "Trap",},];Copy the code

Color logic

<! -- The same div that moved the color in and out -->
<! Class ="item.color" -->
<div
  v-for="(item, index) in blockList"
  :key="index"
  :style="{ width: `${speed}px`, height: `${speed}px`, background: item.background, }"
  :class="item.color"
  @click="editClick($event, item)"
  @mousemove="editMove($event, item)"
  @mouseleave="editLeave"
  class="transition-all"
></div>
Copy the code
const editMove = (event, item) = > {
  if(! item.is_confirm) {for (let i in blockList.value) {
      if (blockList.value[i].id == item.id) {
        // The main point is these two sentences
        blockList.value[i].background = "";
        blockList.value[i].color = color.value;
      } else if (blockList.value[i].is_confirm) {
        blockList.value[i].background = "";
      } else {
        blockList.value[i].background = "# 000"; }}}};const editClick = (event, item) = > {
  / / add json
  switch (color.value) {
    case "bg-green-500":
      if(json.spot_top ! =9999) {
        tips.value = "There can only be one destination.";
        return;
      }
      json.spot_top = item.top;
      json.spot_left = item.left;
      break;
    case "bg-red-500":
      if(json.enemy_top ! =9999) {
        if(json.enemy_top_2 ! =9999) {
          tips.value = "There can only be two enemies.";
          return;
        }
        json.enemy_top_2 = item.top;
        json.enemy_left_2 = item.left;
        break;
      }
      json.enemy_top = item.top;
      json.enemy_left = item.left;
      break;
    case "bg-blue-500":
      if(json.top ! =9999) {
        tips.value = "There can only be one protagonist.";
        return;
      }
      json.top = item.top;
      json.left = item.left;
      break;
    case "bg-gray-500":
      if(json.obstacle_top ! =9999) {
        tips.value = "There can only be one obstacle.";
        return;
      }
      json.obstacle_top = item.top;
      json.obstacle_left = item.left;
      break;
    case "bg-purple-500":
      if(json.trap_top ! =9999) {
        tips.value = "There can only be one trap.";
        return;
      }
      json.trap_top = item.top;
      json.trap_left = item.left;
      break;
    default:
      tips.value = "Please select color ~";
      return;
  }
  // State is reserved
  for (let i in blockList.value) {
    if (blockList.value[i].id == item.id) {
      blockList.value[i].background = "";
      blockList.value[i].color = color.value;
      blockList.value[i].is_confirm = true;
    } else if (blockList.value[i].is_confirm) {
      blockList.value[i].background = "";
    } else {
      blockList.value[i].background = "# 000"; }}};Copy the code

The first is to save the color by clicking on the legend, and then when the mouse moves into the black block, it will no longer be white, but the selected color. When clicking, the color can be fixed to the black block

Since style has a higher priority than class (background is bigger than BG-red-500), we need to remove the background color when floating:

blockList.value[i].background = "";
blockList.value[i].color = color.value;
Copy the code

You want to keep the color when you click, so you want to change the color when you click, and not change the color when you hover over it

blockList.value[i].background = "";
blockList.value[i].color = color.value;
blockList.value[i].is_confirm = true;
Copy the code

Is_confirm has appeared once or twice above and indicates whether the block is set, if so nothing is done to it

const editMove = (event, item) = > {
  if (!item.is_confirm) {
    ...
  }
};
Copy the code

Save the checkpoint

Remember the location of each set block, place it in local storage when you click save level, and a new level is created

GIF save level

When clicking on a block, record the top left and color of the block to pass data into an array, and limit the number of blocks. Here’s an example:

switch(color.value){
  case "bg-blue-500":
      if(json.top ! =9999) {
        tips.value = "There can only be one protagonist.";
        return;
      }
      // Put the top lef of the protagonist in the corresponding place
      json.top = item.top;
      json.left = item.left;
      break;
}
Copy the code

Adds the array to local storage when you click save level

const Level = JSON.parse(localStorage.getItem("data"));
let json = {
  id: Level.length + 1.speed: 176.top: 9999.left: 9999.enemy_top: 9999.enemy_left: 9999.enemy_top_2: 9999.enemy_left_2: 9999.obstacle_top: 9999.obstacle_left: 9999.trap_top: 9999.trap_left: 9999.spot_top: 9999.spot_left: 9999}; .const saveClick = () = > {
  Level.push(json);
  localStorage.setItem("data".JSON.stringify(Level));
  button_text.value = "Saved successfully";
  router.push("/main");
};
Copy the code

So far the whole game is over, rushed out, also don’t expect to come on stage [laugh and cry], if it helps you that’s the best