贪吃蛇:
- 随机出现食物
- 根据键盘方向控制蛇头,开局只有蛇头,吃到食物,蛇身长度为二的时候, 水平方向移动,比如向左, 按向右方向键则无效
- 蛇碰墙壁则游戏结束
- 蛇身要跟随蛇头一起移动
目录:
效果:
相关依赖:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack", // 打包
"start": "webpack serve --open" // 配置devServe指令
},
"devDependencies": {
"@babel/core": "^7.16.7",
"@babel/preset-env": "^7.16.8",
"babel-loader": "^8.2.3",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.20.3",
"css-loader": "^6.5.1",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"postcss": "^8.4.5",
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.2.3",
"style-loader": "^3.3.1",
"ts-loader": "^9.2.6",
"typescript": "^4.5.4",
"webpack": "^5.66.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.3"
}
teconfig.json:
{
"compilerOptions": {
"module": "ES2015",
"target": "es2015",
"strict": true,
"noEmitOnError": true // ts报错的时候不允许编译成js
}
}
webpack.config.js
// 引入一个包
const path = require('path');
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",
environment: {
// 告诉webpack不要使用箭头函数去加载js,ie不支持
arrowFunction: false,
// 让webpack打包不要用const
const: false
}
},
// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的loader
use: [
// 配置babel
{
// 指定加载器
loader: 'babel-loader',
options: {
// 设置预定义的环境
presets: [
[
// 制定环境的插件
"@babel/preset-env",
// 配置信息
{
targets: {
"chrome": '88',
"ie": '9'
},
// 指定corejs的版本
/**
*
* 如果不使用这个,ie没有promise,则处理不了,
* ie没有promise,会自动引入corejs自己的版本的promise给ie用
*
*/
"corejs": "3",
// 使用corejs的方法,usage 表示按需加载
"useBuiltIns": "usage"
}
]
]
}
},
'ts-loader'
],
// 要排除的文件
exclude: /node-modules/
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
browsers: "last 2 versions"
}
]
]
}
}
},
'less-loader'
]
}
]
},
devtool:'source-map',
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html')
})
],
// 用来设置引用模块
resolve: {
extensions: ['.ts', '.js']
},
mode: 'production'
};
index.ts: 入口文件
import './index.less'
import GameControl from './module/GameControl'
new GameControl()
index.html: 页面结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="main">
<div class="stage">
<div class="snake">
<div></div>
</div>
<div class="food">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<div class="score_box">
<div class="score">
SCORE: <span>0</span>
</div>
<div class="level">
LEVEL: <span>1</span>
</div>
</div>
</div>
</body>
</html>
index.less: 样式文件
@bg-color: rgb(185, 218, 170);
.main {
width: 360px;
height: 420px;
margin: 100px auto;
border: 10px solid #000;
border-radius: 30px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
background-color: @bg-color;
box-sizing: border-box;
.stage {
width: 304px;
height: 324px;
border: 2px solid #000;
position: relative;
box-sizing: border-box;
.snake {
position: absolute;
&>div {
position: absolute;
width: 10px;
height: 10px;
background-color: #000;
border: 1px solid @bg-color;
box-sizing: border-box;
}
}
.food {
width: 10px;
height: 10px;
position: absolute;
top: 100px;
left: 30px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
&>div {
width: 4px;
height: 4px;
background-color: #000;
}
}
}
.score_box {
box-sizing: border-box;
width: 100%;
padding: 0 30px;
display: flex;
align-items: center;
justify-content: space-between;
}
}
面向对象区分模块
- 食物 Food.ts
- 分数盘 ScorePanel.ts
- 蛇 Snake.ts
- 游戏控制 GameControl.ts(引入上面三个模块,统一操作,再呗index.ts调用)
Food.ts
// 定义Food类
class Food {
element: HTMLElement
constructor() {
// 这里food编译器会认为获取不到,所以后面加个!表明一定存在
this.element = document.querySelector('.food')!
}
// 获取X
get X() {
return this.element.offsetLeft
}
// 获取Y
get Y() {
return this.element.offsetTop
}
// 随机改变食物的位置
change() {
// 生成一个随机的位置
// 食物最小位置是0,最大位置是290(应该不能写死)
// 蛇每次移动都是一格10,所以要求食物的位置必须是10的倍数
let stage: HTMLElement
stage = document.querySelector('.stage')!
// 获取去除父级border的宽高
let elementWidht = stage.clientWidth
let elementHeight = stage.clientHeight
let maxLeft = (elementWidht - this.element.offsetWidth) / this.element.offsetWidth
let maxTop = (elementHeight - this.element.offsetHeight) / this.element.offsetHeight
let top = Math.round(Math.random() * maxTop) * this.element.offsetHeight
let left = Math.round(Math.random() * maxLeft) * this.element.offsetWidth
this.element.style.left = left + 'px'
this.element.style.top = top + 'px'
}
}
export default Food;
ScorePanel.ts
class ScorePanel {
score = 0
level = 1
scoreEle: HTMLElement
levelEle: HTMLElement
// 最多的关卡
maxLevel: number
// 最大分数
upScore: number
constructor(maxLevel: number = 10, upScore: number) {
this.scoreEle = document.querySelector('.score span')!
this.levelEle = document.querySelector('.level span')!
this.maxLevel = maxLevel
this.upScore = upScore
}
// 增加分数
addScore() {
this.scoreEle.innerHTML = ++this.score + ''
if(this.score % this.upScore === 0) {
this.addLevel()
}
}
// 提升关卡
addLevel() {
if (this.level < this.maxLevel) {
this.levelEle.innerHTML = ++this.level + ''
}
}
}
export default ScorePanel;
Snake.ts
class Snake {
// 蛇容器
element: HTMLElement
// 蛇头
head: HTMLElement
// 蛇身体,包括头, HTMLCollection会实时更新元素
bodies: HTMLCollection
constructor() {
this.element = document.querySelector('.snake')!
this.head = document.querySelector('.snake > div') as HTMLElement
// 返回的是一个nodeList,固定获取
// document.querySelectorAll('.snake > div')
// 返回的是HTMLCollection,就算后续加入div,也可以获取
this.bodies = this.element.getElementsByTagName('div')
}
// 获取蛇头X
get X() {
return this.head.offsetLeft
}
// 获取蛇头Y
get Y() {
return this.head.offsetTop
}
// 设置蛇头X
set X(value: number) {
if (this.X === value) return
if (value < 0 || value > (document.querySelector('.stage')!.clientWidth - this.head.offsetWidth)) {
throw ('蛇撞墙了!')
}
// 身体跟随
this.moveBody()
this.head.style.left = value + 'px'
// 检查是否撞到自己
this.checkHeadBody()
}
// 设置蛇头Y
set Y(value: number) {
if (this.Y === value) return
if (value < 0 || value > (document.querySelector('.stage')!.clientHeight - this.head.offsetHeight)) {
throw ('蛇撞墙了!')
}
this.moveBody()
this.head.style.top = value + 'px'
this.checkHeadBody()
}
// 蛇增加身体
addBody() {
// 向element添加一个div
this.element.insertAdjacentHTML('beforeend', '<div></div>')
}
// 身体跟随移动
moveBody() {
for(let i = this.bodies.length-1; i > 0; i--) {
let bodies = this.bodies
let x = (bodies[i - 1] as HTMLElement).offsetLeft;
let y = (bodies[i - 1] as HTMLElement).offsetTop;
(bodies[i] as HTMLElement).style.left = x + 'px';
(bodies[i] as HTMLElement).style.top = y + 'px';
}
}
// 判断是否碰撞自己身体
checkHeadBody() {
[...this.bodies].forEach((item, index) => {
if(index === 0) return
let bd = (item as HTMLElement)
if(bd.offsetLeft === this.X && bd.offsetTop === this.Y) {
throw('撞到自己了')
}
})
}
}
export default Snake
GameControl.ts
import Food from './Food'
import ScorePanel from './ScorePanel'
import Snake from './Snake'
class GameControl {
Food: Food
ScorePanel: ScorePanel
Snake: Snake
// 存储箭头移动的方向
direction: string = 'right'
timeId: any
isLive: boolean = true
constructor() {
this.Food = new Food()
this.ScorePanel = new ScorePanel(10, 4)
this.Snake = new Snake()
this.init()
}
init() {
// 这回调事件绑定在document上的,所以this指向document,需要bind
document.addEventListener('keydown', this.keydownHandle.bind(this))
this.run()
}
// 键盘按下的回调函数
/**
*
* @param event
* event.key: chrome ie
* ArrowUp up
ArrowRight right
ArrowDown down
ArrowLeft left
*
*/
keydownHandle(event: KeyboardEvent) {
// 判断是否允许反方向
if (this.Snake.bodies[1]) {
if (this.direction === 'ArrowRight' && (event.key === 'ArrowLeft' || event.key === 'left')) return
if (this.direction === 'ArrowLeft' && (event.key === 'ArrowRight' || event.key === 'right')) return
if (this.direction === 'ArrowUp' && (event.key === 'ArrowDown' || event.key === 'down')) return
if (this.direction === 'ArrowDown' && (event.key === 'ArrowUp' || event.key === 'up')) return
}
this.direction = event.key
this.isLive && this.run()
}
// 蛇移动
run() {
clearTimeout(this.timeId)
let X = this.Snake.X
let Y = this.Snake.Y
switch (this.direction) {
case 'ArrowUp':
case 'up':
Y -= 10
break
case 'ArrowDown':
case 'down':
Y += 10
break
case 'ArrowRight':
case 'right':
X += 10
break
case 'ArrowLeft':
case 'left':
X -= 10
break
}
// 检查是否吃到食物
this.checkEat(X, Y)
try {
this.Snake.X = X
this.Snake.Y = Y
} catch (e) {
alert(e)
this.isLive = false
}
if (this.isLive) {
this.timeId = setTimeout(this.run.bind(this), 500 - (this.ScorePanel.level - 1) * 50)
}
}
// 检查是否吃到食物, 这里包含了食物,蛇本身,所以得放在gameControl处理
checkEat(x: number, y: number) {
if (this.Food.X === x && this.Food.Y === y) {
// 身体增加
this.Snake.addBody()
// 食物重新刷新
this.Food.change()
// 记分牌加一
this.ScorePanel.addScore()
}
}
}
export default GameControl