项目主要用到的工具包括typescript,webpack,css拓展语言(less或者scss)博主这边使用的是scss。
1-使用命令npm install @babel/core @babel/preset-env babel-loader clean-webpack-plugin core-js css-loader html-webpack-plugin node-sass postcss postcss-loader postcss-preset-env sass-loader scss style-loader ts-loader typescript webpack webpack-cli webpack-dev-server --save-dev
来安装项目依赖。
"devDependencies": {
"@babel/core": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"core-js": "^3.8.3",
"css-loader": "^5.0.2",
"html-webpack-plugin": "^5.0.0",
"node-sass": "^4.12.0",
"postcss": "^8.2.5",
"postcss-loader": "^5.0.0",
"postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.2",
"scss": "^0.2.4",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.15",
"typescript": "^4.1.3",
"webpack": "^5.21.0",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2"
},
2-配置webpack运行环境和typecript运行环境
webpack.config.js
const path=require('path');
const HtmlWebpackPlugin =require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports={
entry: {
main: "./ts/test.ts",
},
devtool: 'inline-source-map',
output: {
path: path.resolve(__dirname,"demo"),
filename: "index.js",
environment:{
arrowFunction:false,
const:false
}
},
mode:'development',
module: {
rules: [
{
test:/\.ts$/,
use:[
{
loader: "babel-loader",
options: {
presets:[
[
"@babel/preset-env",
{
targets:{
"chrome":"88",
"ie":"11"
},
"corejs":"3",
"useBuiltIns":"usage"
}
]
]
}
},
"ts-loader"
],
exclude:/node_modules/
},
{
test:/\.scss$/,
use:[
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions:{
plugins:[
[
"postcss-preset-env",
{
browsers:"last 2 versions"
}
]
]
}
}
},
"sass-loader"
],
exclude:/node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: "./html/demo.html"
}),
new CleanWebpackPlugin(),
],
resolve: {
extensions: ['.ts','.js']
}
}
tsconfig.json文件
{
"include": [
"./ts/**/*"
],
"compilerOptions": {
"target": "es6",
"module": "es6",
"outDir": "./js",
"removeComments": true,
"noEmitOnError": true,
"strict": true
}
}
至此项目环境已经的搭建完成
页面html代码
<body>
<div class="container">
<!-- 主舞台 -->
<div class="main" id="main">
<!-- 蛇 -->
<div class="snake" id="snake">
<div></div>
</div>
<!-- 食物 -->
<div class="food" id="food"></div>
</div>
<!-- 积分板 -->
<div class="score">
<div class="score-item">
<span>SCORE:</span>
<span id="score">0</span>
</div>
<div class="score-item">
<span>LEVEL:</span>
<span id="level">1</span>
</div>
</div>
</div>
</body>
css样式
*{
margin: 0;
padding: 0;
list-style: none;
outline: none;
.container{
width: 360px;
height: 420px;
background: #b7d4a8;
margin: 100px auto;
border: 10px solid #333333;
border-radius: 40px;
display: flex;
flex-flow:column ;
align-items: center;
justify-content: space-around;
box-sizing: border-box;
.main{
width: 304px;
height: 304px;
border: 2px solid #333333;
position: relative;
box-sizing: border-box;
.snake > div{
height: 10px;
width: 10px;
background: #333333;
border: 1px solid #b7d4a8;
position: absolute;
box-sizing: border-box;
}
.food{
height: 10px;
width: 10px;
background: #cf2c2c;
border: 1px solid #b7d4a8;
position: absolute;
box-sizing: border-box;
left: 100px;
border-radius: 1000px;
top: 100px;
}
}
.score{
width: 304px;
display: flex;
align-items: center;
justify-content: space-between;
}
}
}
flex弹性布局
display: flex; ————设置为弹性布局
flex-flow:column ;————以竖直方向为主轴,以水平方向为副轴
align-items: center;————副轴方向居中分布
justify-content: space-around;————主轴方向空隙环绕分布
最后在ts入口文件引入import '../css/scss/index.scss'
分析:
食物对象,需要包含食物对象本身foodHtml
,位置信息x,y
等属性,
还需要包含获取当前食物位置事件getter()
,随机生成食物事件change()
等事件。
获取当前食物位置事件getter()
//获取食物的横纵坐标
get x(){
return this.foodHtml.offsetLeft;
}
get y(){
return this.foodHtml.offsetTop;
}
随机生成食物事件change()
change(){
//食物位置是随机生成的
//位置范围在0-290(main的宽度-食物的宽度),并且是10(食物的宽度)的倍数
//计算得出food的移动范围
let x=Math.round(Math.random()*29)*10;
let y=Math.round(Math.random()*29)*10;
this.foodHtml.style.left=x+'px';
this.foodHtml.style.top=y+'px';
}
其中位置信息是通过定位获取的,至于食物的产生位置是有限制的最小为0,最大是主舞台大小减去食物大小。所以这部分写成固定值是不合适的,为了增加可扩展性,增加了主舞台对象mainHtml
,食物大小foodWidth
,主舞台大小mainWidth
等属性,从而大大提升了游戏的可扩展性。
优化后的change()
change(){
//位置范围在0-290(main的宽度-食物的宽度),并且是10(食物的宽度)的倍数
let maxH=(this.mainWidth-this.foodWidth)/this.foodWidth;
let x=Math.round(Math.random()*maxH)*this.foodWidth;
let y=Math.round(Math.random()*maxH)*this.foodWidth;
this.foodHtml.style.left=x+'px';
this.foodHtml.style.top=y+'px';
}
Food 详细代码:
//定义食物对象
class Food {
//定义一个属性表示食物对应元素
foodHtml:HTMLElement;
//定义一个属性表示主舞台对应元素
mainHtml:HTMLElement;
//获取food元素和主舞台元素的宽度
foodWidth:number;
mainWidth:number;
constructor(){
//获取页面中的food元素并赋值给foodHtml
this.foodHtml=document.getElementById("food")!;
//获取页面中的main元素并赋值给mainHtml
this.mainHtml=document.getElementById("main")!;
this.foodWidth=this.foodHtml.offsetWidth;
this.mainWidth=this.mainHtml.clientWidth;
}
//获取食物的横坐标——左偏移量
get x(){
return this.foodHtml.offsetLeft;
}
//获取食物的纵坐标——上偏移量
get y(){
return this.foodHtml.offsetTop;
}
//生成事物的位置
change(){
//食物位置是随机生成的
//位置范围在0-290(main的宽度-食物的宽度),并且是10(食物的宽度)的倍数
//计算得出food的移动范围
let maxH=(this.mainWidth-this.foodWidth)/this.foodWidth;
let x=Math.round(Math.random()*maxH)*this.foodWidth;
let y=Math.round(Math.random()*maxH)*this.foodWidth;
this.foodHtml.style.left=x+'px';
this.foodHtml.style.top=y+'px';
}
}
export default Food;
分析:
计分板对象,需要包含分数score
,等级level
,计分板元素scoreHtml
levelHtml
等属性,
还包含分数增加事件addScore()
,等级上升事件addLevel()
等事件。
分数增加事件addScore()
//增加分数事件
addScore(){
this.scoreHtml.innerHTML=++this.score+"";
}
等级上升事件addLevel()
//提升等级是事件
addLevel(){
this.levelHtml.innerHTML = ++this.level + "";
}
以上基本操作已经完成,但是仔细想想等级怎么上升呢,总不能和分数增加一样吧,应该是达到多少分数就可以升级了,比如达到10,20,30都可以升级即达到10这个区间即可升级。
优化后的addScore()
addScore(){
this.scoreHtml.innerHTML=++this.score+"";
//分数到达等级提升的分数区间,等级提升
if(this.score%10===0){
this.addLevel();
}
}
另一方面应该设置等级上限,等达到等级就不在升级。
优化后的addLevel()
addLevel(){
//未达到等级上限提升等级
if(this.level<10) {
this.levelHtml.innerHTML = ++this.level + "";
}
}
最后为了提高游戏的可扩展性,我们定义升级的分数区间upLevelScore
和等级上限maxLevel
,之后的等级分数和等级上限我们的都可以自己来设置。
//增加分数事件
addScore(){
this.scoreHtml.innerHTML=++this.score+"";
//分数到达等级提升的分数区间,等级提升
if(this.score%this.upLevelScore===0){
//console.log(this.score%this.upLevelScore);
this.addLevel();
}
}
//提升等级是事件
addLevel(){
//未达到等级上限提升等级
if(this.level<this.maxLevel) {
this.levelHtml.innerHTML = ++this.level + "";
}
}
ScorePanel 详细代码
//定义记分牌对象
class ScorePanel {
//定义score分数和level等级属性
score:number;
level:number;
//设置等级上限
maxLevel:number;
//设置升级分数区间
upLevelScore:number
//定义score和level在页面中的元素
scoreHtml:HTMLElement;
levelHtml:HTMLElement;
//不传值默认为10
constructor(maxLevel:number=10,upLevelScore:number=10){
this.score=0;
this.level=1;
this.maxLevel=maxLevel;
this.upLevelScore=upLevelScore;
this.scoreHtml=document.getElementById("score")!;
this.levelHtml=document.getElementById("level")!;
}
//增加分数事件
addScore(){
this.scoreHtml.innerHTML=++this.score+"";
//分数到达等级提升的分数区间,等级提升
if(this.score%this.upLevelScore===0){
//console.log(this.score%this.upLevelScore);
this.addLevel();
}
}
//提升等级是事件
addLevel(){
//未达到等级上限提升等级
if(this.level<this.maxLevel) {
this.levelHtml.innerHTML = ++this.level + "";
}
}
}
export default ScorePanel;
分析:
蛇对象,需要包含蛇元素snakeHead
snakeBodies
snakeContainer
,分别是蛇头,蛇身体包含蛇头和蛇容器,蛇位置x,y
等属性,
以及包含获取蛇位置事件getter()
,设置蛇的位置setter()
,蛇身体增加事件addBody()
,蛇身体移动事件moveBody()
,监测碰撞身体事件isHitBody()
,撞墙事件等等。
获取蛇位置事件getter()
//获取蛇的位置(蛇头坐标)
get x(){
return this.snakeHead.offsetLeft;
}
get y(){
return this.snakeHead.offsetTop;
}
设置蛇的位置setter()
,这里包含了撞墙检测
//设置蛇头位置坐标
set x(x:number){
if(x===this.x){//同值什么都不做
return;
}
if(x<0||x>390){//撞墙到边界通过抛异常使游戏结束
throw new Error("游戏结束了");
}
//正常移动
this.snakeHead.style.left=x+"px";
}
set y(y:number){
if(y===this.y){//同值什么都不做
return;
}
if(y<0||y>390){//撞墙到边界通过抛异常使游戏结束
throw new Error("游戏结束了");
}
//正常移动
this.snakeHead.style.top=y+"px";
}
蛇身体增加事件addBody()
//蛇增加身体事件
addBody(){
this.snakeContainer.insertAdjacentHTML("beforeend","")
}
蛇身体移动事件moveBody()
,通过遍历蛇的身体将身体后段移向身体前段的位置由后向前依次定位。
//身体移动事件,通过遍历蛇的身体将身体后段移向身体前段的位置由后向前
moveBody(){
for(let i=this.snakeBodies.length-1;i>0;i--){
let x=(this.snakeBodies[i-1] as HTMLElement).offsetLeft;
let y=(this.snakeBodies[i-1] as HTMLElement).offsetTop;
(this.snakeBodies[i] as HTMLElement).style.left=x+"px";
(this.snakeBodies[i] as HTMLElement).style.top=y+"px";
}
}
监测碰撞身体事件isHitBody()
,就是遍历身体位置与头部位置进行比较如果有重合则游戏结束。
//监测身体碰撞事件,就是遍历身体位置与头部位置进行比较如果有重合则游戏结束。
isHitBody(){
for(let i=this.snakeBodies.length-1;i>0;i--){
let x=(this.snakeBodies[i] as HTMLElement).offsetLeft;
let y=(this.snakeBodies[i] as HTMLElement).offsetTop;
if(this.x===x&&this.y===y){
throw new Error("游戏结束了");
}
}
}
对撞墙检测部分进行扩展性优化,本来是引入主舞台和食物通过获取他们的大小来做限制,但是因为已经在Food类中做了限制,所以直接引入Food类优化了的设置蛇的位置setter()
//设置蛇头位置坐标
set x(x:number){
if(x===this.x){//同值什么都不做
return;
}
if(x<0||x>this.food.mainWidth-this.food.foodWidth){//到边界通过抛异常使游戏结束
throw new Error("游戏结束了");
}
//正常移动
this.snakeHead.style.left=x+"px";
}
set y(y:number){
if(y===this.y){//同值什么都不做
return;
}
if(y<0||y>this.food.mainWidth-this.food.foodWidth){//到边界通过抛异常使游戏结束
throw new Error("游戏结束了");
}
//正常移动
this.snakeHead.style.top=y+"px";
}
Snake详细代码
import Food from "./Food";
//定义射对象
class Snake{
food:Food;
//定义蛇头元素
snakeHead:HTMLElement;
//定义蛇身体
snakeBodies:HTMLCollection;
//定义蛇容器
snakeContainer:HTMLElement;
constructor(){
//querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素。
this.snakeHead=document.querySelector("#snake > div")! as HTMLElement;//类型断言
//getElementsByTagName() 方法可返回带有指定标签名的对象的集合。
this.snakeBodies=document.getElementById("snake")!.getElementsByTagName("div");
this.snakeContainer=document.getElementById("snake")!;
this.food=new Food();
}
//获取蛇的位置(蛇头坐标)
get x(){
return this.snakeHead.offsetLeft;
}
get y(){
return this.snakeHead.offsetTop;
}
//设置蛇头位置坐标
set x(x:number){
if(x===this.x){//同值什么都不做
return;
}
if(x<0||x>this.food.mainWidth-this.food.foodWidth){//到边界通过抛异常使游戏结束
throw new Error("游戏结束了");
}
//正常移动
this.snakeHead.style.left=x+"px";
}
set y(y:number){
if(y===this.y){//同值什么都不做
return;
}
if(y<0||y>this.food.mainWidth-this.food.foodWidth){//到边界通过抛异常使游戏结束
throw new Error("游戏结束了");
}
//正常移动
this.snakeHead.style.top=y+"px";
}
//监测事件
action(x:number,y:number){
this.moveBody();
this.x=x;
this.y=y;
this.isHitBody();
}
//蛇增加身体事件
addBody(){
this.snakeContainer.insertAdjacentHTML("beforeend","")
}
//身体移动事件
moveBody(){
for(let i=this.snakeBodies.length-1;i>0;i--){
let x=(this.snakeBodies[i-1] as HTMLElement).offsetLeft;
let y=(this.snakeBodies[i-1] as HTMLElement).offsetTop;
(this.snakeBodies[i] as HTMLElement).style.left=x+"px";
(this.snakeBodies[i] as HTMLElement).style.top=y+"px";
}
}
//监测身体碰撞事件
isHitBody(){
for(let i=this.snakeBodies.length-1;i>0;i--){
let x=(this.snakeBodies[i] as HTMLElement).offsetLeft;
let y=(this.snakeBodies[i] as HTMLElement).offsetTop;
if(this.x===x&&this.y===y){
throw new Error("游戏结束了");
}
}
}
}
export default Snake;
控制器需要将Food对象,ScorePanel对象,Snake对象引入从而时间对象之间的交互,比如蛇吃到食物增加分数上升等级等等。
控制器,需要包含游戏状态isLive
默认值为true,当蛇撞墙或者撞身体而报错时游戏结束而变成false,蛇移动方向direction
等属性,
以及包含操作蛇移动事件keyDownHandle()
move()
和监测吃到食物事件isEatFood()
操作蛇移动事件keyDownHandle()
,视频里获取的是键盘操作值,但是不同浏览器操作值可能不相同,所以博主这边获取的是键盘值,这里不同浏览器是相同的,左37上38右39下40。
但还是存在一种情况蛇会掉头,这种情况还不允许的,通过观察相对掉头方向键盘值之间差值为2,只需要把这个限制即可
if (code>36&&code<41&&Math.abs(code-this.preDirection)!=2){
this.direction=code;
this.preDirection=this.direction;
}
//获取键盘值
keyDownHandle(e:KeyboardEvent){
let code=e.keyCode;
//左37上38右39下40,后面是检测是否掉头查看键盘值
if (code>36&&code<41&&Math.abs(code-this.preDirection)!=2){
this.direction=code;
this.preDirection=this.direction;
}
}
//蛇移动事件
move(){
//获取当前蛇的位置
let x=this.snake.x;
let y=this.snake.y;
//根据按键方向修改x,u值
switch(this.direction){
//左移
case 37:
x=x-this.food.foodWidth;
break;
//上移
case 38:
y=y-this.food.foodWidth;
break;
//右移
case 39:
x=x+this.food.foodWidth;
break;
//下移
case 40:
y=y+this.food.foodWidth;
break;
}
//重新赋值位置
try {//异常捕获
this.snake.action(x,y);
console.log(`当前位置x轴:${x},y轴:${y}`);
//吃到食物
//
if(this.isEatFood(x,y)){
console.log("吃到食物了")
//重新随机生成食物
this.food.change();
//增加分数
this.scorePanel.addScore();
//增加蛇的身体
this.snake.addBody();
}
} catch (error) {
console.log("游戏结束")
alert("游戏结束")
this.isLive=false;
}
this.isLive&&setTimeout(this.move.bind(this), 300 - (this.scorePanel.level-1)*30);
}
监测吃到食物事件isEatFood()
isEatFood(x:number,y:number){
return x===this.food.x&&y===this.food.y;
}
控制器详细代码
import Food from "./Food";
import ScorePanel from "./ScorePanel";
import Snake from "./Snake"
//定义游戏控制器
class GameController {
food:Food;
scorePanel:ScorePanel;
snake:Snake;
//定义游戏运行状态
isLive:boolean;
//定义移动位置
direction:number;
preDirection:number;
constructor(){
this.food=new Food();
this.scorePanel=new ScorePanel();
this.snake=new Snake();
this.direction=39;
this.preDirection=this.direction;
this.isLive=true;
this.init();
}
//初始化事件
init(){
//绑定点击键盘事件,this.keyDownHandle.bind(this)是为了将方向信息一直保存
document.addEventListener('keydown',this.keyDownHandle.bind(this),false);
//调用移动事件
this.move();
}
//获取键盘值
keyDownHandle(e:KeyboardEvent){
let code=e.keyCode;
//左37上38右39下40,后面是检测是否掉头查看键盘值
if (code>36&&code<41&&Math.abs(code-this.preDirection)!=2){
this.direction=code;
this.preDirection=this.direction;
}
}
//蛇移动事件
move(){
//获取当前蛇的位置
let x=this.snake.x;
let y=this.snake.y;
//根据按键方向修改x,u值
switch(this.direction){
//左移
case 37:
x=x-this.food.foodWidth;
break;
//上移
case 38:
y=y-this.food.foodWidth;
break;
//右移
case 39:
x=x+this.food.foodWidth;
break;
//下移
case 40:
y=y+this.food.foodWidth;
break;
}
//重新赋值位置
try {//异常捕获
this.snake.action(x,y);
console.log(`当前位置x轴:${x},y轴:${y}`);
//吃到食物
//
if(this.isEatFood(x,y)){
console.log("吃到食物了")
//重新随机生成食物
this.food.change();
//增加分数
this.scorePanel.addScore();
//增加蛇的身体
this.snake.addBody();
}
} catch (error) {
console.log("游戏结束")
alert("游戏结束")
this.isLive=false;
}
this.isLive&&setTimeout(this.move.bind(this), 300 - (this.scorePanel.level-1)*30);
}
//监测吃到食物事件
isEatFood(x:number,y:number){
return x===this.food.x&&y===this.food.y;
}
}
export default GameController;
最后在ts入口文件引入,达到最后效果如下
import '../css/scss/index.scss'
import GameController from './modules/GameController'
let gameController=new GameController();