初始化
用腳手架新建一個react專案:
npx create-react-app snake-demo
關于安裝和創建、啟動,可以看我的這篇文章:React入門:基本環境搭建
首先畫一個框,當作邊界,再初始化貪吃蛇,就像這樣:
左上角兩個小黑方塊就是貪吃蛇了

這里貪吃蛇的移動是以網格做單位的,把游戲區域的寬高各分成100份,貪吃蛇每次移動就前進2個網格,即2%
先寫一個具體的div來測驗一下將要實作的效果和樣式
<div className="App">
<div className="snake-dot" style={{top:0, left:0}}></div>
<div className="snake-dot" style={{top:0, left:'2%'}}></div>
</div>
樣式:
.App {
height: 600px;
width: 600px;
border: 1px solid #ccc;
margin: 10px auto;
position: relative;
}
.snake-dot {
height: 2%;
width: 2%;
background-color: black;
border: 1px solid #fff;
position: absolute;
}
得到上述圖片的效果后,我們把這個初始snake的定位資料放到state中,方便管理;并把snake-dot單獨拎出來,改成動態生成
import React, { Component } from 'react'
import { Snake } from './components/Snake';
const initState = {
snakeDots:[
[0, 0],
[2, 0]
]
}
class App extends Component {
state = initState;
render() {
return (
<div className="App">
<Snake snakeDots={this.state.snakeDots} />
</div>
);
}
}
export default App;
把snake-dot的動態生成放到子組件Snake.js中:
import React from 'react'
export const Snake = ({snakeDots}) => {
return (
<>
{
snakeDots.map((dot, index)=>{
let dotStyle = {
left:`${dot[0]}%`,
top:`${dot[1]}%`
}
console.log(dotStyle)
return (
<div className="snake-dot" key={index} style={dotStyle}></div>
)
})
}
</>
)
}
這樣初始化就完成了
貪吃蛇的移動
貪吃蛇的上下左右移動根據鍵盤輸入的上下左右(↑↓←→)鍵來確定,初始化時默認是向右移動的
先來看看鍵盤上下左右鍵的code值
| Key | Code |
|---|---|
| 左箭頭 | 37 |
| 右箭頭 | 39 |
| 上箭頭 | 38 |
| 下箭頭 | 40 |
游戲開始時,默認是向右走的,因此,我們增加一個表示方向的state:direction,走的速度是每隔200ms前進一步
const initState = {
snakeDots: [
[0, 0],
[2, 0]
],
direction: "RIGHT",
speed: 200
}
當檢測到上下左右鍵按下時,更新state中的方向,貪吃蛇沿該方向前進一步;
若沒有按鍵按下,就沿著當前方向一路前進
貪吃蛇的頭部(head)是小黑方塊的最后一塊,不斷地刪去第一個小黑方塊并在direction方向上新增一小黑方塊;視覺上,貪吃蛇就有不停向前移動的效果

componentDidMount() {
// 監聽鍵盤按下事件
document.onkeydown = this.onkeydown;
// 每隔200ms前進一步
setInterval(this.onMove, this.state.speed);
}
onkeydown = (e) => {
// 根據按下的鍵盤鍵更新direction
switch (e.keyCode) {
case 37:
this.setState({ direction: "LEFT" });
break;
case 38:
this.setState({ direction: "UP" });
break;
case 39:
this.setState({ direction: "RIGHT" });
break;
case 40:
this.setState({ direction: "DOWN" });
break;
default:
this.setState({ direction: "RIGHT" });
}
}
onMove = () => {
// 根據direction指示的方向前進
let newSnakeDots = [...this.state.snakeDots];
let header = newSnakeDots[newSnakeDots.length - 1];
switch (this.state.direction) {
case "UP":
header = [header[0], header[1] - 2];
break;
case "DOWN":
header = [header[0], header[1] + 2];
break;
case "LEFT":
header = [header[0] - 2, header[1]];
break;
case "RIGHT":
header = [header[0] + 2, header[1]];
break;
default:
header = [header[0] + 2, header[1]];
}
// 刪去第一個小黑塊,并在direction方向上新增新小黑塊,更新snakeDots
newSnakeDots.shift();
newSnakeDots.push(header);
this.setState({ snakeDots: newSnakeDots })
}
效果:

邊界限制與得分計算
當觸碰邊界或者撞擊到自身時,游戲結束,彈窗提示,分數為貪吃蛇的長度-2(初始長度為2,其余均是吃到食物后變長的,吃一個食物長長1格)
componentDidUpdate() {
this.checkIfBordered();
}
checkIfBordered = () => {
let newSnakeDots = [...this.state.snakeDots];
let header = newSnakeDots[newSnakeDots.length - 1];
if (header[0] < 0 || header[0] > 98 || header[1] < 0 || header[1] > 98) {
alert(`觸碰邊界,游戲結束,你的得分是:${newSnakeDots.length - 2}`);
this.setState(initState);
}
}
食物隨機出現
由于我們規定,貪吃蛇觸碰到邊界時算游戲結束,因此,食物不能出現在邊界上,否則,一吃到食物就代表觸壁結束游戲(可以改進為:貪吃蛇每次前進1%,當食物被觸碰比例達50%時可以當作貪吃蛇吃掉該食物,馬上掉頭避免觸壁,以后再改吧~)
因此,食物出現的范圍在top:2%~96%,left的范圍也是如此

const getRandomFood = ()=>{
// 食物出現的范圍是:[2, 96] 推算程序:[0,95)->[2, 97)->[1, 48.5]->[1,48]->[2,96]
let max = 95;
let min = 2;
let x = Math.floor((Math.random() * max + min) / 2) * 2;
let y = Math.floor((Math.random() * max + min) / 2) * 2;
return [x,y]
}
const initState = {
snakeDots: [
[0, 0],
[2, 0]
],
direction: "RIGHT",
speed: 200,
food:getRandomFood() // 隨機生成食物的位置
}
食物的展示:
import { Food } from './components/Food';
render() {
return (
<div className="App">
<Snake snakeDots={this.state.snakeDots} />
<Food food={this.state.food} />
</div>
);
}
import React from 'react'
export const Food = ({food}) => {
let foodStyle = {
left:`${food[0]}%`,
top:`${food[1]}%`
}
return (
<div className="food" style={foodStyle}></div>
)
}
食物的樣式:
.food {
height: 2%;
width: 2%;
background-color: red;
border: 1px solid #fff;
position: absolute;
}
判斷是否吃到食物
貪吃蛇的頭(head)和食物重疊時,代表吃到
貪吃蛇變長,食物的位置變成新的head,重新放置食物
componentDidUpdate() {
// 每次更新都判斷一下
this.checkIfBordered();
this.checkIfEated();
}
checkIfEated = ()=>{
let newSnakeDots = [...this.state.snakeDots];
let header = newSnakeDots[newSnakeDots.length - 1];
// head與食物重疊時,代表吃到食物
if(header[0]==this.state.food[0] && header[1]==this.state.food[1]){
console.log("吃到了")
newSnakeDots.unshift([]);
this.setState({snakeDots:newSnakeDots});
this.setState({food:getRandomFood()});
}
}
效果:

完整代碼:
新建檔案夾component存放組件:

App.js
import React, { Component } from 'react'
import { Snake } from './components/Snake';
import { Food } from './components/Food';
// 隨機獲取食物位置
const getRandomFood = ()=>{
// [2, 96]: [0,95)->[2, 97)->[1, 48.5]->[1,48]->[2,96]
let max = 95;
let min = 2;
let x = Math.floor((Math.random() * max + min) / 2) * 2;
let y = Math.floor((Math.random() * max + min) / 2) * 2;
console.log("food-position",x,y)
return [x,y]
}
// 另存state以便重新初始化
const initState = {
snakeDots: [
[0, 0],
[2, 0]
],
direction: "RIGHT",
speed: 200,
food:getRandomFood()
}
class App extends Component {
state = initState;
// 監聽與計時器
componentDidMount() {
document.onkeydown = this.onkeydown;
setInterval(this.onMove, this.state.speed);
}
// 更新時檢測是否出界或吃到食物
componentDidUpdate() {
this.checkIfBordered();
this.checkIfEated();
}
// 判斷鍵盤按鍵,更新direction
onkeydown = (e) => {
switch (e.keyCode) {
case 37:
this.setState({ direction: "LEFT" });
break;
case 38:
this.setState({ direction: "UP" });
break;
case 39:
this.setState({ direction: "RIGHT" });
break;
case 40:
this.setState({ direction: "DOWN" });
break;
default:
this.setState({ direction: "RIGHT" });
}
}
// 根據direction移動
onMove = () => {
let newSnakeDots = [...this.state.snakeDots];
let header = newSnakeDots[newSnakeDots.length - 1];
switch (this.state.direction) {
case "UP":
header = [header[0], header[1] - 2];
break;
case "DOWN":
header = [header[0], header[1] + 2];
break;
case "LEFT":
header = [header[0] - 2, header[1]];
break;
case "RIGHT":
header = [header[0] + 2, header[1]];
break;
default:
header = [header[0] + 2, header[1]];
}
newSnakeDots.shift();
newSnakeDots.push(header);
this.setState({ snakeDots: newSnakeDots })
// console.log(this.state.snakeDots)
}
// 判斷是否出界
checkIfBordered = () => {
let newSnakeDots = [...this.state.snakeDots];
let header = newSnakeDots[newSnakeDots.length - 1];
if (header[0] < 0 || header[0] > 98 || header[1] < 0 || header[1] > 98) {
alert(`觸碰邊界,游戲結束,你的得分是:${newSnakeDots.length - 2}`);
this.setState(initState);
}
}
// 判斷是否吃到食物
checkIfEated = ()=>{
let newSnakeDots = [...this.state.snakeDots];
let header = newSnakeDots[newSnakeDots.length - 1];
if(header[0]==this.state.food[0] && header[1]==this.state.food[1]){
console.log("吃到了")
newSnakeDots.unshift([]);
this.setState({snakeDots:newSnakeDots});
this.setState({food:getRandomFood()});
}
}
render() {
return (
<div className="App">
<Snake snakeDots={this.state.snakeDots} />
<Food food={this.state.food} />
</div>
);
}
}
export default App;
Snake.js:
import React from 'react'
export const Snake = ({snakeDots}) => {
return (
<>
{
snakeDots.map((dot, index)=>{
let dotStyle = {
left:`${dot[0]}%`,
top:`${dot[1]}%`
}
return (
<div className="snake-dot" key={index} style={dotStyle}></div>
)
})
}
</>
)
}
Food.js:
import React from 'react'
export const Food = ({food}) => {
let foodStyle = {
left:`${food[0]}%`,
top:`${food[1]}%`
}
return (
<div className="food" style={foodStyle}></div>
)
}
index.css:
.App {
height: 600px;
width: 600px;
border: 1px solid #ccc;
margin: 10px auto;
position: relative;
}
.snake-dot {
height: 2%;
width: 2%;
background-color: black;
border: 1px solid #fff;
position: absolute;
}
.food {
height: 2%;
width: 2%;
background-color: red;
border: 1px solid #fff;
position: absolute;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/297662.html
標籤:其他
上一篇:2021-9-4 爆肝一整天,關于2D橫板游戲中攀爬樓梯的考慮
下一篇:Unity 檔案的增刪查改
