项目体验
这个小游戏主要包括积分面板,食物,蛇,还有我们的游戏控制器这四个部分,分为四个类来写。
1.项目环境
2.项目配置
package.json
{
"name": "demo",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve"
},
"author": "",
"license": "ISC",
"keywords": [],
"description": "",
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"babel-loader": "^9.1.2",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.30.2",
"css-loader": "^6.7.4",
"html-webpack-plugin": "^5.5.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"postcss": "^8.4.23",
"postcss-loader": "^7.3.0",
"postcss-preset-env": "^8.4.1",
"style-loader": "^3.3.3",
"ts-loader": "^9.4.3",
"typescript": "^5.0.4",
"webpack": "^5.83.1",
"webpack-cli": "^5.1.1",
"webpack-dev-server": "^4.15.0"
}
}
tsconfig.json
{
"compilerOptions": {
"module": "es2015",
"target": "es2015",
"strict": true,
"sourceMap": false,
//当有错误时不生成编译后文件
"noEmitOnError": true
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules"
]
}
webpack.config.js 打包配置
//引入一个包
const path = require("path");
//引入html插件
const HTMLWebpackPlugin = require("html-webpack-plugin")
//引入编译清除上一次文件插件
const {CleanWebpackPlugin} = require("clean-webpack-plugin")
//webpack中所有的配置文件都放在module.exports中
module.exports = {
entry: "./src/index.ts",
//指定打包文件所在目录
output: {
//指定打包文件的目录
path: path.resolve(__dirname, "dist"),
//打包后的文件
filename: "bundle.js",
//告诉webpack不使用箭头
environment: {
arrowFunction: false
}
},
// mode: 'development',// 设置mode
mode: 'production',// 设置mode
//指定webpack打包时要使用的模块
module: {
//指定要加载的规则
rules: [
{
//test指定的是规则生效的文件
test: /\.ts$/,
//要使用的loader
use: [
//配置babel 适配更多浏览器
{
//指定加载器
loader: "babel-loader",
//设置babel
options: {
//设置预定义的环境
presets: [
[
//指定环境的插件
"@babel/preset-env",
//配置信息
{
targets: {
"chrome": 88,
},
//指定corejs的版本
"corejs": "3",
//使用core js的方式"usage"表示按需加载
"useBuiltIns": "usage"
}
]
]
}
}
, "ts-loader"],
//要排除的文件
exclude: /node-modules/
},
//设置less文件的处理
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
//引入postcss
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
browsers: "last 2 versions"
}
]
]
}
}
},
"less-loader"
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
// title:"这是一个自定义title"
template: "./src/index.html"
})
],
//用来设置引用模块
resolve: {
extensions: [".ts", ".js"]
}
}
index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>贪吃蛇title>
head>
<body>
<div id="main">
<div class="screen">
<div id="snake">
<div>div>
div>
<div id="food">
<div>div>
<div>div>
<div>div>
<div>div>
div>
div>
<div class="count">
<h4 class="countText">SCORE: <span id="score">0span>h4>
<h4 class="countText">LEVEL: <span id="level">1span>h4>
div>
div>
body>
html>
index.less
@backColor: #cefdb5;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
//background-color: @backColor;
text-align: center;
padding-top: 100px;
//display: flex;
}
#main {
display: inline-block;
width: 560px;
height: 620px;
background-color: @backColor;
border: #0e0e0e 18px solid;
padding: 20px;
border-radius: 40px;
text-align: center;
.screen {
height: 415px;
width: 460px;
margin: 12px auto auto;
border: #0e0e0e 4px solid;
position: relative;
#snake {
& > div {
width: 15px;
height: 15px;
background-color: #000;
border: 1px @backColor solid;
position: absolute;
}
div:first-child {
background-color: #67d01b;
border-radius: 4px;
}
}
#food {
width: 15px;
height: 15px;
//background-color: red;
border: 1px @backColor solid;
position: absolute;
left: 40px;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-content: space-between;
& > div {
width: 6px;
height: 6px;
background-color: #0a7142;
transform: rotate(45deg);
}
}
}
.count {
margin-top: 50px;
padding: 0 10px;
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
font-size: 20px;
font-weight: 600;
h4 {
display: inline-block;
}
}
}
export class Food {
//定义一个属性表示食物对应的元素
elementD: HTMLElement;
constructor(elemet: HTMLElement) {
//获取页面中的元素给elementD
// document.getElementById("#food")!;
this.elementD = elemet;
this.change()
}
get X() {
return this.elementD.offsetLeft;
}
get Y() {
return this.elementD.offsetTop;
}
//修改食物位置
change() {
//生成一个随机的位置
// console.log(this.elementD)
//食物的位置最小是 0 x 最大是460 y最大是420 390 435;
let top = Math.round(Math.random() * 26) * 15
let left = Math.round(Math.random() * 29) * 15;
this.elementD.style.left = left + 'px'
this.elementD.style.top = top + 'px'
}
}
我们的主角,主要逻辑难点在蛇身的移动与碰撞检验
export class Snake {
//表示蛇头元素
head: HTMLElement;
//蛇的身体
bodies: HTMLCollection
element: HTMLElement
constructor() {
this.element = document.getElementById("snake")!
this.head = document.querySelector("#snake>div") as HTMLElement;
this.bodies = this.element.getElementsByTagName('div');
}
get X() {
return this.head.offsetLeft
}
get Y() {
return this.head.offsetTop
}
//设置蛇头的坐标
set X(val: number) {
if (this.X == val) {
return
}
if (val < 0 || val > 435) {
throw new Error("你撞墙了")
}
this.head.style.left = val + "px"
}
set Y(val: number) {
if (this.Y == val) {
return
}
if (val < 0 || val > 390) {
throw new Error("你撞墙了")
}
this.head.style.top = val + "px"
}
//蛇增加身体
addBody() {
this.element.insertAdjacentHTML("beforeend", ``)
}
moveBody(x: number, y: number) {
//遍历所有的身体 头已经改过了 不需要改
for (let i = 1; i < this.bodies.length - 1; i++) {
let currentEle = this.bodies[i] as HTMLElement;
let x1 = parseInt(currentEle.style.left.substring(0, currentEle.style.left.indexOf("px")))
let y1 = parseInt(currentEle.style.top.substring(0, currentEle.style.top.indexOf("px")))
if (this.X == x1 && this.Y == y1 && i > 1) {
throw new Error("你撞上了自己的身体!")
}
}
for (let i = this.bodies.length - 1; i > 0; i--) {
let houEle = this.bodies[i] as HTMLElement;
let qianEle = this.bodies[i - 1] as HTMLElement;
if (i == 1) {
houEle.style.left = x + 'px';
houEle.style.top = y + 'px';
} else {
houEle.style.left = qianEle.style.left;
houEle.style.top = qianEle.style.top;
}
}
}
}
export class ScorePanel {
score: number = 0;
level: number = 1;
scoreEle: HTMLElement;
levelEle: HTMLElement;
MaxLevel:number;
UpScore:number;
constructor(scoreEle: HTMLElement, levelEle: HTMLElement,maxLevel:number=10,upScore:number=5) {
this.scoreEle = scoreEle;
this.levelEle = levelEle;
this.MaxLevel=maxLevel;
this.UpScore=upScore;
}
addScore() {
this.score++;
this.scoreEle.innerHTML = this.score + ""
if (this.score%this.UpScore==0){
this.addLevel()
}
}
addLevel() {
if (this.level >= this.MaxLevel) {
return
}
this.level++;
this.levelEle.innerHTML = this.level + ""
}
}
整个游戏的运行与控制逻辑都在这里,里面有按键触发逻辑等,是用来调度蛇与食物和记分板的一个控制类
//游戏控制器
import {Food} from "./Food";
import {Snake} from "./Snake";
import {ScorePanel} from "./ScorePanel";
export class GameControl {
food: Food;
snake: Snake;
scorePanel: ScorePanel
direction: string = ""
time: any
isLive: boolean = true
constructor(food: Food, scorePanel: ScorePanel, snake: Snake) {
this.food = food;
this.snake = snake;
this.scorePanel = scorePanel;
this.init();
this.run();
}
init() {
//banging键盘按下的事件
document.addEventListener("keydown", this.keydownHandler.bind(this))
}
/**
* ArrowUp
* ArrowDown
* ArrowLeft
* ArrowRight
*/
keydownHandler(event: KeyboardEvent) {
let a = " ArrowUp ArrowDown ArrowLeft ArrowRight "
let b = "wasd"
let key = event.key;
if (a.indexOf(" " + key + " ") != -1 || b.indexOf(key) != -1) {
// console.log(event.key)
//上的时候不可以按下
//左的时候不可以按右
switch (this.direction) {
case "ArrowUp":
case "Up":
case "w":
switch (key) {
case "ArrowDown":
case "Down":
case "s":
return;
}
break
case "ArrowDown":
case "Down":
case "s":
switch (key) {
case "ArrowUp":
case "Up":
case "w":
return;
}
break
case "ArrowLeft":
case "Left":
case "a":
switch (key) {
case "ArrowRight":
case "Right":
case "d":
return;
}
break
case "ArrowRight":
case "Right":
case "d":
switch (key) {
case "ArrowLeft":
case "Left":
case "a":
return;
}
break
}
this.direction = event.key
// this.run()
}
}
/**
* 蛇移动方法
*
*/
run() {
//获取蛇现在的坐标
let x = this.snake.X;
let y = this.snake.Y;
let x1 = this.snake.X;
let y1 = this.snake.Y;
switch (this.direction) {
case "ArrowUp":
case "Up":
case "w":
// 向上移动top减少
y -= 15;
break;
case "ArrowDown":
case "Down":
case "s":
y += 15
break;
case "ArrowLeft":
case "Left":
case "a":
x -= 15
break;
case "ArrowRight":
case "Right":
case "d":
x += 15
break;
}
try {
this.snake.X = x;
this.snake.Y = y;
//判断是否吃到食物
this.checkEat(x, y);
this.snake.moveBody(x1, y1)
} catch (e: any) {
this.isLive = false
alert(e.message)
return
}
// console.log(x, y)
if (this.time && this.isLive) {
clearTimeout(this.time)
}
this.time = setTimeout(this.run.bind(this), 250 - (this.scorePanel.level - 1) * 50)
}
checkEat(X: number, Y: number) {
if (this.food.X == X && this.food.Y == Y) {
//吃到食物了
this.scorePanel.addScore()
//重新刷新食物点位
this.food.change()
//蛇要增加一节
this.snake.addBody()
}
}
}
用来引入所有需要的模块,包括样式模块
import './style/index.less'
import {Food} from "./modules/Food";
import {ScorePanel} from "./modules/ScorePanel";
import {GameControl} from "./modules/GameControl";
import {Snake} from "./modules/Snake";
let foodEle = document.getElementById("food")!;
let food = new Food(foodEle);
let scorePanel = new ScorePanel(document.getElementById("score")!,
document.getElementById("level")!)
let snake = new Snake();
let gameControl = new GameControl(food,scorePanel,snake);