参考文章:https://www.bilibili.com/video/BV1Xy4y1v7S2?p=22
项目搭建
将之前的package.json,tsconfig.json,webpack.config.js复制到新项目中,同时新建src跟test目录。
然后修改package.json中的name,修改成现在的项目名称。
然后使用npm i
自动下载项目所需的包,或者直接使用图形化命令下载也行。
或
搭建完后测试一下是否正常。在src下新建main.ts与main.html,随意在main.ts中写一些代码。
写完后使用npm run build
来构建一下。正常完成后会构建一个dict文件夹。
配置less
使用npm i -D less less-loader css-loader style-loader
下载。然后修改webpack.config.js文件。
添加的配置信息
{ // 设置less文件处理
test: /\.less$/, // 指定规则生效的文件:以less结尾的文件
use:[ // 加载器从后往前执行
"style-loader",
"css-loader",
"less-loader"
],
exclude: /node-modules/ // 要排除的文件
}
测试
创建一个less文件,随意写些内容,然后再ts中引入。
使用npm run build
后页面变成绿色了,这代表less的代码执行成功。
css的兼容性处理
跟ts一样,css也要考虑兼容性问题。使用npm i -D postcss postcss-loader postcss-preset-env
下载相关构建。然后修改webpack.config.js文件。
{ // 引入postcss
loader: "postcss-loader",
options: { // 设定参数
postcssOptions:{
plugins:[ // 设置预定义的环境
[
"postcss-preset-env", // 指定环境插件
{ // 配置信息
browsers: "last 2 versions" // 兼容所有主流浏览器的最新的版本数
}
]
]
}
}
},
最终webpack.config.js中的less配置信息如下:
{ // 设置less文件处理
test: /\.less$/, // 指定规则生效的文件:以less结尾的文件
use:[ // 加载器从后往前执行
"style-loader",
"css-loader",
{ // 引入postcss
loader: "postcss-loader",
options: { // 设定参数
postcssOptions:{
plugins:[ // 设置预定义的环境
[
"postcss-preset-env", // 指定环境插件
{ // 配置信息
browsers: "last 2 versions" // 兼容所有主流浏览器的最新的版本数
}
]
]
}
}
},
"less-loader"
],
exclude: /node-modules/ // 要排除的文件
}
项目界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>贪食蛇</title>
</head>
<body>
<!--创建游戏主容器-->
<div id="main">
<!--设置游戏舞台-->
<div id="stage">
<!--设置蛇-->
<div id="snake">
<!--snake内部的div,表示蛇的各个部分-->
<div></div>
</div>
<!--设置食物-->
<div id="food">
<!--设置食物纹理-->
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<!--设置游戏积分面板-->
<div id="score-panel">
<div>
SCORE:<span id="score">0</span>
</div>
<div>
level:<span id="level">1</span>
</div>
</div>
</div>
</body>
</html>
设置样式
body{
background-color: #1E90FF;
display: flex;
font:bold 20px "Courier New"; // 统一设置字体
}
// 设置变量
@bg-color: #b7d4a8;
// 清除默认样式
*{
margin: 0;
padding: 0;
// 改变盒子模型的计算方式, 方便自动计算内容器大小
box-sizing: border-box;
}
// 设置主窗口的样式
#main{
width: 360px;
height: 420px;
background-color: @bg-color; // 设置主容器背景颜色
margin: 100px auto; // 位置下移100像素,水平自动居中
border: 10px solid black; // 设置边界
border-radius: 40px; // 设置圆角
display: flex; // 开启弹性盒模型
flex-flow:column; // 设置主轴方向
align-items: center; // 设置侧轴的对其方式
justify-content: space-around; // 设置主轴的对齐方式
//游戏舞台
#stage{
width: 304px;
height: 304px;
border: 2px solid black; // 设置边界
position: relative; // 开启相对定位
//设置蛇的样式
#snake{
&>div{
width: 10px;
height: 10px;
background-color: black;
border: 1px solid @bg-color;
position: absolute; // 开启绝对定位
}
}
}
//设置食物的样式
#food{
width: 10px;
height: 10px;
border: 1px solid @bg-color;
position: absolute; // 开启绝对定位
display: flex; // 开启弹性盒模型
flex-flow: row wrap; // row设置横轴为主轴, wrap表示会自动换行
justify-content: space-between; // 设置主轴和侧轴的空白空间分配到元素之间
align-content: space-between;
//left: 40px;
//top: 100px;
&>div{
width: 4px;
height: 4px;
background-color: black;
transform: rotate(45deg); // 设置旋转45度
}
}
//游戏记分牌
#score-panel{
width: 300px;
display: flex;
justify-content: space-between; // 设置主轴对齐方式
}
}
运行逻辑
完成FOOD类
新建一个Food.ts来存放该类
// 定义食物类FOOD
class Food{
// 定义一个属性表示食物所对应的元素
element: HTMLElement
stage
constructor() {
// 获取页面中food的元素并将其赋值给element
this.element = document.getElementById("food")! // !强制表示没有问题,不使用则提示可能找不到该页面元素
this.stage = document.getElementById("stage")!
// this.stage = document.evaluate('/html/body/div[@id="main"]/div[@id="stage"]', document, null, XPathResult.ANY_TYPE, null)
}
// 定义一个获取食物X的方法
get X(){
return this.element.offsetLeft
}
// 定义一个获取食物Y的方法
get Y(){
return this.element.offsetTop
}
// 修改食物的位置
change(){
// 生成一个随机位置
// 食物的位置最小是0,最大是290
// 蛇移动一次是一格,一格大小是10,所以食物的位置是10的倍数
const high = (Math.floor(this.stage.offsetHeight/10)*10 - 10)/10
const width= (Math.floor(this.stage.offsetWidth/10)*10 - 10)/10
let x = Math.round(Math.random() * width) * 10
let y = Math.round(Math.random() * high) * 10
this.element.style.left = x + "px"
this.element.style.top = y + "px"
}
}
// 默认模块暴露出去
export default Food
完成ScorePanel类
新建一个ScorePanel.ts来存放该类
// 定义表示计分牌的类
class ScorePanel{
// #score和level用来记录分数和等级
#score:number = 0 // 私有属性在前面加#
level:number=1
// #score_ele和level_ele分数很等级所在的元素,在构造中初始化
#score_ele:HTMLElement
level_ele:HTMLElement
// 设置变量来限制等级
max_level:number
// 设置一个变量表示升级分数
up_score:number
constructor(max_level:number = 10, up_score:number = 10) {
this.#score_ele = document.getElementById("score")!
this.level_ele = document.getElementById("level")!
this.max_level = max_level
this.up_score = up_score
}
//设置一个加分的方法
addScore(){
// 使分数自加
this.#score_ele.innerHTML = ++this.#score + ''
// 判断分数是多少
if(this.#score % this.up_score === 0){
this.levelUp()
}
}
// 提升等级的方法
levelUp(){
if (this.level < this.max_level){
this.level_ele.innerHTML = ++this.level + ''
}
}
}
// 测试代码
const sp = new ScorePanel()
for (let i=0;i<10;i++){
sp.addScore()
}
export default ScorePanel
完成Snake类
蛇的相关属性方法
class Snake{
// 表示蛇头的元素
head:HTMLElement
// 蛇的圣体
bodis:HTMLCollectionBase
// 蛇的容器
snake:HTMLElement
// 舞台对象
stage:HTMLElement
stage_width:number
stage_high:number
is_reback_way:boolean = false
constructor() {
this.snake = document.getElementById("snake")!
this.head = document.querySelector('#snake > div') as HTMLElement // as强制转化类型
// document.querySelectorAll('#snake > div') // 数组是固定死的
// this.bodis = document.getElementById("snake")!.getElementsByTagName("div")
this.bodis = this.snake.getElementsByTagName("div")
this.stage = document.getElementById("stage")!
this.stage_width= (Math.floor(this.stage.offsetWidth/10)*10 - 10) // 舞台的横向最大值
this.stage_high = (Math.floor(this.stage.offsetHeight/10)*10 - 10) // 舞台的纵向最大值
}
// 获取蛇的坐标(蛇头坐标)
get X(){
return this.head.offsetLeft
}
get Y(){
return this.head.offsetTop
}
// 设置坐标
set X(value){
if(this.X === value){ // 如果值相同,则不再修改
return
}
if (value < 0 || value > this.stage_width){
// 进入判断则说明蛇撞墙了,抛出一个异常
throw new Error('蛇撞墙了')
}
// 没有使用教程的方法
// 修改x时,是在修改水平坐标,蛇在左右移动,在向左移动时候,不能向右掉头,反之亦然。
// if(this.bodis[1] && (this.bodis[1] as HTMLElement).offsetLeft === value){
// // 如果发生掉头了,让蛇继续向反方向移动
// if (value > this.X){
// //如果新值value大于旧值X,则说明蛇在向右走,此时发生掉头,应该使蛇继续向左走
// value = this.X - 10
// }else{
// // 向左走
// value = this.X + 10
// }
// }
this.moveBody()
this.head.style.left = value + "px"
// 检查是否撞到自己
this.checkHeadBody()
}
set Y(value){
if(this.Y === value){ // 如果值相同,则不再修改
return
}
if (value < 0 || value > this.stage_high){
// 进入判断则说明蛇撞墙了,抛出一个异常
throw new Error('蛇撞墙了')
}
this.moveBody()
this.head.style.top = value + "px"
// 检查是否撞到自己
this.checkHeadBody()
}
// 设置蛇增加身体方法
addBody(){
// 向snake添加一个div
this.snake.insertAdjacentHTML("beforeend","<div></div>")
}
// 添加蛇身体移动方法
moveBody(){
// 将后面的身体设置为前边身体的位置,遍历获取所有的身体
for (let i=this.bodis.length-1; i > 0; i--){
// 获取前一个身体的位置
let x = (this.bodis[i - 1] as HTMLElement).offsetLeft;
let y = (this.bodis[i - 1] as HTMLElement).offsetTop;
// 将值设置到当前身体的位置
(this.bodis[i] as HTMLElement).style.left = x + 'px';
(this.bodis[i] as HTMLElement).style.top = y + 'px';
}
}
checkHeadBody(){
// 获取所有身体,检查是否和蛇头坐标是否发生重叠
for( let i = 1; i < this.bodis.length; i++){
let tmp = this.bodis[i] as HTMLElement
if ( this.X === tmp.offsetLeft && this.Y === tmp.offsetTop){
// 进入判断说明蛇头撞到身体
throw new Error('蛇撞自己')
}
}
}
}
export default Snake
GameContro键盘控制器
// 引入其他的类
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 游戏控制器,控制其他的所有类
class GameContro{
// 定义3个属性
snake:Snake
food:Food
score_panele:ScorePanel
// 存储蛇的移动方向,即按键的方向
direction: string = ""
// 记录游戏退出机制
is_live = true
constructor() {
this.snake= new Snake()
this.food = new Food()
this.score_panele = new ScorePanel()
this.init()
}
// 游戏初始化方法
init(){
// 绑定键盘按键按下事件
document.addEventListener("keydown", this.keyDownHandler.bind(this)) // 绑定this的对象为GameContro
// 调用run方法,来使移动
this.run()
}
// 创建一个键盘的响应函数
keyDownHandler(event:KeyboardEvent){
let up_key = ["ArrowUp", "Up", "w", "W"]
let down_key = ["ArrowDown", "Down", "s", "S"]
let right_key = ["ArrowRight", "Right", "d", "D"]
let left_key = ["ArrowLeft", "Left", "a", "A"]
if(up_key.indexOf(event.key) >= 0){ // number中0为False,其他都是True
if(this.direction === "Down" && this.snake.bodis.length !== 1){}else{ // 掉头操作无效处理
this.direction = "Up"
}
}
if(down_key.indexOf(event.key) >= 0){
if(this.direction === "Up" && this.snake.bodis.length !== 1){}else{
this.direction = "Down"
}
}
if(right_key.indexOf(event.key) >= 0){
if(this.direction === "Left" && this.snake.bodis.length !== 1){}else{
this.direction = "Right"
}
}
if(left_key.indexOf(event.key) >= 0){
if(this.direction === "Right" && this.snake.bodis.length !== 1){}else{
this.direction = "Left"
}
}
}
// 检查蛇是否吃到食物
checkEat(X:number, Y:number){
if((X === this.food.X) && (Y === this.food.Y)){
console.log('吃到食物')
// 食物位置重置
this.food.change()
// 分数增加
this.score_panele.addScore()
// 蛇身体增加一节
this.snake.addBody()
}
}
// 创建蛇移动的方法
run(){
// 向上,top减少;向下,top增加,向左,left减少;向右,left增加;
let x = this.snake.X
let y = this.snake.Y
//根据方向来计算x与y值
switch (this.direction){
case "ArrowUp":
case "Up": // ArrowUp也是沿用UP的处理方法
y -= 10
break
case "Down":
y += 10
break
case "Right":
x += 10
break
case "Left":
x -= 10
break
}
// 检查蛇头是否碰撞到食物
this.checkEat(x, y)
try{
//修改x,y值
this.snake.X = x
this.snake.Y = y
}catch(e:any){
alert(e.message + ' Game Over')
// 将is_live设置为false
this.is_live = false
}
// 开启自定时调度
this.is_live && setTimeout(this.run.bind(this), 300 - (this.score_panele.level - 1) * 30) // 递归
}
}
export default GameContro;
main.ts
// 引入样式
import './style/main.less'
// import Food from './mod/Food'
// import ScorePanel from './mod/ScorePanel'
// import Snake from './mod/Snake'
import GameContro from "./mod/GameContro";
new GameContro()