Post

贪吃蛇网页小游戏

贪吃蛇网页小游戏

git 远程作业仓库

这是远程作业仓库,目前是已经到了课程尾声、集大成的版本,刚开始开发的时候用w1-starter这个branch。 我自己做的版本在这里。

刚打开starter分支,发现里面有经典3剑客,js html css。

CSS 文件的作用是为一个游戏界面定义样式,每个样式规则的功能如下:

  • body:
    • 设置整个页面为 100% 的视窗高度(100vh)和宽度(100vw),保证页面内容填满整个视窗。
    • 使用 display: flex 来让内容居中,justify-content: centeralign-items: center 实现水平和垂直居中对齐。
    • 去除页面默认的外边距(margin: 0),并设置灰色背景(background-color: gray)。
  • #game-board:
    • 游戏主界面,黑色背景,宽度和高度都设置为 100vmin,保证在宽高变化时保持正方形。
    • 使用 display: grid 创建 21×21 的网格布局,以便在每一单元格中放置游戏元素(如蛇和食物)。
  • .snake:
    • 为蛇的样式定义绿色背景(background-color: lime),以及一个黑色边框,边框宽度设置为 0.25vmin,确保在不同屏幕大小下视觉效果保持一致。
  • .food:
    • 定义食物的样式,背景为红色,边框颜色为灰色,宽度同样为 0.25vmin,保证和蛇的单元格大小一致,且具有视觉对比。

这个样式文件简洁明了,且响应式很好,适用于基于 CSS 网格布局的简易游戏界面。

测试实例:更改js文件,使得console.log 可以用interval函数进行更新显示。

html文件中加入<script src=”game.js”>,得以应用js的脚本。

step1

snake.js:用数组,(x: 10,y:10),来表示蛇的每一个关节,然后pop去掉最后一个关节,复制一个头,unshift这个新头到数组第一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// TODO 1.1: Create the snake
const snakeBody = [
  {x:11 , y:11},
  {x:11 , y:10},
  {x:11 , y:9},
]
// TODO 1.2: Create a function that updates the snake
// Hint: Search for the documentation for the Array pop() and unshift() methods
// in the MDN docs.
const updateSnake = () => {
  snakeBody.pop();

  const newHead = {...snakeBody[0]};
// ...,展开运算符,是复制snakeBody[0]的内容。不加...,复制的就是引用。
  newHead.x += 0;
  newHead.y += 1;

  snakeBody.unshift(newHead);
}
// Don't change this function!
const drawSnake = (gameBoard) => {
  for (let i = 0; i < snakeBody.length; i++) {
    const segment = snakeBody[i];
    const snakeElement = document.createElement('div');
    snakeElement.style.gridRowStart = segment.y;
    snakeElement.style.gridColumnStart = segment.x;
    snakeElement.classList.add('snake');
    gameBoard.appendChild(snakeElement);
  }
};

game.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const SNAKE_SPEED = 5;

const gameBoard = document.getElementById('game-board');

const main = () => {
  update();
  draw();
};

setInterval(main, 1000 / SNAKE_SPEED);

const update = () => {
  console.log('Updating');
  // TODO 1.3: Update the snake here
  updateSnake();
};

const draw = () => {
  gameBoard.innerHTML = '';
  drawSnake(gameBoard);
};

做好了1.1~1.3,刷新页面, 贪吃蛇一共有3个关节,无限向下移动。

step2

input.js:处理用户输入,按下不同方向键,向不同方向移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let inputDirection = {x: 0,y: 1};//默认方向

window.addEventListener('keydown', (event) =>{
//左右移动时,下一次移动只能上下。上下同理。
    if(event.key === 'ArrowUp' && inputDirection.x !== 0){
        inputDirection = {x: 0, y: -1 };
    } else if (event.key === 'ArrowDown'&& inputDirection.x !== 0){
        inputDirection = {x: 0, y: 1 };
    } else if (event.key === 'ArrowLeft'&& inputDirection.y !== 0){
        inputDirection = {x: -1, y: 0 };
    } else if (event.key === 'ArrowRight'&& inputDirection.y !== 0){
        inputDirection = {x: 1, y: 0 };
    }

})
//获取用户按下的方向键所代表的当前的方向
const getInputDirection = () =>{
    return inputDirection;
}

进入上一步中改变移动的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const updateSnake = () => {
  // Remove tail segment
  snakeBody.pop();

  // Add new head segment
  const newHead = { ...snakeBody[0] };

  // TODO 2.2: Set the new head's position using the user's inputs 
  const snakeDirection = getInputDirection();
  newHead.x += snakeDirection.x;
  newHead.y += snakeDirection.y;

  snakeBody.unshift(newHead);
};

step3

food和蛇变大的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//此代码定义了处理网格上蛇游戏各个方面的函数,
//包括检查某个位置是否被蛇占据、生成食物位置以及让蛇生长。

//游戏的网格大小。此处的网格为 21x21,这意味着位置范围从 (1,1) 到 (21,21)。
const GRID_SIZE = 21;

//onSnake:检查给定的position(具有x和y坐标)当前是否被蛇的任何部分占据。
const onSnake = (position) => {
  for (let i = 0; i < snakeBody.length; i++) {
    if (equalPositions(position, snakeBody[i])) {
      return true;
    }
  }
  return false;
};
const equalPositions = (pos1, pos2) => {
  return pos1.x === pos2.x && pos1.y === pos2.y;
};

//growSnake:复制蛇的最后一段,加到array尾部。
const growSnake = () => {
  snakeBody.push({ ...snakeBody[snakeBody.length - 1] });
};

//getNewFoodPosition:找到蛇尚未占据的新食物位置。
const getNewFoodPosition = () => {
  let randomFoodPosition = randomGridPosition();
  while (onSnake(randomFoodPosition)) {
    randomFoodPosition = randomGridPosition();
  }
  return randomFoodPosition;
};

//Math.random生成 1 到 21 之间的随机x和y值
const randomGridPosition = () => {
  return {
    x: Math.floor(Math.random() * GRID_SIZE) + 1,
    y: Math.floor(Math.random() * GRID_SIZE) + 1,
  };
};

food:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TODO 3.1: Create the food
let food = {
  x: 5,
  y: 5,
};

// TODO 3.2: Create a function to update the food
const updateFood = () =>{
  if (onSnake(food)){
    growSnake();
    food = getNewFoodPosition();
  }
};

// Don't change me!
const drawFood = (gameBoard) => {
  const foodElement = document.createElement('div');
  foodElement.style.gridRowStart = food.y;
  foodElement.style.gridColumnStart = food.x;
  foodElement.classList.add('food');
  gameBoard.appendChild(foodElement);
};

step4

增加游戏逻辑。

蛇的逻辑更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//此代码定义了用于处理蛇的边界检查和自碰撞检测的函数

//出界
const outOfBounds = (position) => {
    return position.x < 1 || position.x > GRID_SIZE || position.y < 1 || position.y > GRID_SIZE;
}

const snakeOutOfBounds = () => {
    return outOfBounds(snakeBody[0]);
}

//检查蛇是否与自身发生碰撞
//工作原理:
//从索引 1 开始循环遍历蛇的身体各个部分(以避免将头部与自身进行比较)。
//使用该equalPositions函数检查头部(snakeBody[0])是否与任何其他段位于同一位置。
//true如果检测到碰撞则返回,否则返回false。
//此功能对于实现检测自碰撞的游戏逻辑至关重要,自碰撞通常会导致游戏结束。
const snakeIntersectSelf = () => {
    for (let i = 1; i < snakeBody.length; i++) {
        if (equalPositions(snakeBody[0], snakeBody[i])) {
            return true;
        }
    }
    return false;
}

游戏结束逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const SNAKE_SPEED = 5;
let gameOver = false;
const gameBoard = document.getElementById('game-board');

const main = () => {
  update();
  draw();
  // TODO 4.3, 4.4: Add Game Over Alert, and clear interval!
  if(gameOver){
    alert("Game Over!!");
    clearInterval(gameloop);
  }
};

// TODO 4.4: Define the interval ID
// HINT: ONLY EDIT THE LINE BELOW!
let gameloop = setInterval(main, 1000 / SNAKE_SPEED);

const update = () => {
  console.log('Updating');
  updateSnake();
  updateFood();
  // TODO 4.2: Update Game State
  gameOver = checkGameOver();
};

const draw = () => {
  gameBoard.innerHTML = '';
  drawSnake(gameBoard);
  drawFood(gameBoard);
};

// TODO 4.1: Create a function that checks if the game is over
const checkGameOver = () =>{
  return snakeOutOfBounds() || snakeIntersectSelf();
}

目前已经完成一遍的游戏,直到游戏结束,弹出窗口:游戏结束。

往后需要的工作:实现重启游戏的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 在文件开头添加初始状态常量
const INITIAL_SNAKE_BODY = [
  { x: 11, y: 11 },
  { x: 11, y: 10 },
  { x: 11, y: 9 },
];

// 修改 main 函数中的游戏结束处理
const main = () => {
  update();
  draw();
  if(gameOver){
    clearInterval(gameloop);
    if(confirm("游戏结束!是否重新开始?")) {
      resetGame();
    }
  }
};

// 添加重置游戏的函数
const resetGame = () => {
  gameOver = false;
  inputDirection = { x: 0, y: 1 }; // 重置蛇的移动方向
  // 重置蛇的身体
  snakeBody.length = 0;
  INITIAL_SNAKE_BODY.forEach(segment => snakeBody.push({...segment}));
  // 重置食物位置
  food = getNewFoodPosition();
  // 重新开始游戏循环
  gameloop = setInterval(main, 1000 / SNAKE_SPEED);
};
This post is licensed under CC BY 4.0 by the author.

Trending Tags