1、将之前写好的package.json、webpack.config.js、tsconfig.json 复制到当前目录下
2、使用 npm i
初始化环境
3、在src目录下新建 index.html 和 index.ts
4、安装工具:npm i -D less less-loader css-loader style-loader
5、修改相关配置文件:
(1)webpack.config.js:
// 指定webpack打包时要使用的模块
module:{
// 指定要加载(loader)的规则
rules:[
......
// 设置less文件的处理
{
test:/\.less$/,
use:[
"style-loader",
"css-loader",
"less-loader"
]
}
]
},
为了使css样式能够兼容各个浏览器,还要使用 postcss:npm i -D postcss postcss-loader postcss-preset-env
// 设置less文件的处理
{
test:/\.less$/,
use:[
"style-loader",
"css-loader",
{
loader:"postcss-loader",
options:{
postOptions:{
plugins:[
[
"postcss-preset-env",
{
browsers:'last 2 versions'
}
]
]
}
}
},
"less-loader"
]
}
这样会自动把样式处理成各个浏览器可兼容的样式
1、index.html
<body>
<div id="main">
<div id="stage">
<div id="snake">
<div>div>
div>
<div id="food">
<div>div>
<div>div>
<div>div>
<div>div>
div>
div>
<div id="score-pannel">
<div>
SCORE:<span id="score">0span>
div>
<div>
LEVEL:<span id="level">1span>
div>
div>
div>
<script src="./index.js">script>
body>
2、index.less
// 设置变量
@bg-color:#b7d4a8;
// 清除默认样式
*{
margin: 0;
padding: 0;
// 改变盒子模型的计算方式
box-sizing: border-box;
}
body{
font: bold 20px "Courier";
}
// 设置主窗口的样式
#main{
width: 360px;
height: 420px;
// 背景颜色
background-color: @bg-color;
// 居中
margin: 100px auto;
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: #000;
// 使身体各部分间产生间距
border: 1px solid @bg-color;
// 蛇的位置会改变,因此开启绝对定位.相对于游戏窗口定位,所以为stage开启相对定位
position: absolute;
}
}
// 设置食物
#food{
width: 10px;
height: 10px;
position: absolute;
left: 40px;
top: 100px;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-content: space-between;
&>div{
width: 4px;
height: 4px;
background-color: #000;
transform: rotate(45deg);
}
}
}
// 游戏记分牌
#score-pannel{
width: 300px;
display: flex;
// 主轴的对齐方式
justify-content: space-between;
}
}
1、 index.ts
//引入样式
import './style/index.less'
//引入模块化文件
import Food from './modules/Food'
import ScorePanel from './modules/ScorePanel'
2、modules/Food.ts : 定义食物类(Food)
class Food{
// 定义一个属性表示食物所对应的元素(div)
element:HTMLElement;
constructor(){
//不加叹号会报错,提示这个值可能为空。在我们的场景存在food,所以这个值不可能为空,加上!表示这个值不可能为空
this.element = document.getElementById('food')!;
}
// 获取食物当前的x坐标
get X(){
return this.element.offsetLeft;
}
// 获取食物当前的y坐标
get Y(){
return this.element.offsetTop;
}
// 修改食物的位置
change(){
// 生成随机位置
// 食物的位置(左偏移量和上偏移量)最小是0,最大是290
// 因为蛇的大小10px,每次只能移动1格10px.所以食物的位置必须是整10的,蛇才能吃到
// Math.round(Math.random()*290) //Math.random()*290 表示 [0,290).Math.round()四舍五入,可以表示[0,290].但是这种方式并不能去除掉整10的
let top = Math.round(Math.random()*29) * 10 // Math.random()*29 [0,29); Math.round(Math.random()*29) [0,29]. 这时整体乘10即可
// 或者 Math.floor(Math.random()*30) * 10
let left = Math.round(Math.random()*29) * 10
this.element.style.left = left + 'px'
this.element.style.top = top + 'px'
}
}
3、modules/ScorePanel.ts : 定义记分牌的类(ScorePanel)
class ScorePanel{
// score,level记录分数和等级
score = 0;
level = 1;
// 分数和等级所在元素,在构造函数中初始化
scoreEle:HTMLElement;
LevelEle:HTMLElement;
// 设置变量限制等级
maxLevel:number;
// 设置变量表示多少分时升级
upScore:number;
constructor(maxLevel: number = 10, upScore:number = 10){ //给maxLevel一个默认值
this.scoreEle = document.getElementById('score')! ;
this.LevelEle = document.getElementById('level')! ;
this.maxLevel = maxLevel
this.upScore = upScore
}
// 加分的方法
addScore(){
// 使分数自增
this.scoreEle.innerHTML = ++this.score + '';
// 判断分数是多少.当达到一定分数后,升一级
if(this.score % this.upScore === 0){ //每十分升一级
this.levelUp();
}
}
// 提升等级的方法
levelUp(){
if(this.level < this.maxLevel){
this.LevelEle.innerHTML = ++this.level + '';
}
}
}
export default ScorePanel
注:在使用到常量的地方尽量将其设置成一个变量,方便后续修改
4、modules/snake.ts : 定义 蛇类(snake)(初步代码,有待完善)
class snake{
// 表示蛇头的元素
head : HTMLElement;
// 表示蛇的身体(包括蛇头)
bodies:HTMLCollection;
// 表示蛇的容器
element:HTMLElement;
constructor(){
// 获取snake中的第一个div,就是蛇头
this.head = document.querySelector('#snake > div')! as HTMLElement ;
// 获取蛇的身体
this.bodies = document.getElementById('snake')!.getElementsByTagName('div');
// 获取蛇的容器
this.element = document.getElementById('snake')!;
}
// 获取蛇的坐标(蛇头的坐标)
get X(){
return this.head.offsetLeft;
}
get Y(){
return this.head.offsetTop;
}
// 设置蛇头的坐标
set X(value){
this.head.style.left = value + 'px';
}
set Y(value){
this.head.style.top = value + 'px';
}
// 蛇吃到食物后增加身体
addBody(){
// 向element中添加一个div
this.element.insertAdjacentHTML("beforeend","") //把加到element结束标签的前面
}
}
export default snake
5、整合
现在各个类是相互独立的,需要新建一个类将这些类整合到一起。
在modules下新建 GameControl.ts (游戏控制器,控制其他所有类)
初始代码:
// 引入其他类
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 游戏控制器,控制其他所有类
class GameControl{
// 定义三个属性
// 蛇
snake:Snake;
// 食物
food:Food;
// 记分牌
scorePanel:ScorePanel;
// 创建一个属性来存储蛇的移动方向(也就是按键的方向)
direction:string = '';
// 创建一个属性来记录游戏是否结束。(蛇撞墙)
isLive = true;
constructor(){
this.snake = new Snake();
this. food = new Food();
this.scorePanel = new ScorePanel();
// 开始游戏
this.init()
}
// 游戏初始化方法,调用后游戏即开始
init(){
// 绑定键盘的按下事件
// 如果不绑定的话,this是指document。使用bind后this指的是GameControl
document.addEventListener('keydown',this.keydownHandler.bind(this))
// 调用run方法
this.run()
}
// 创建键盘按下的响应函数
keydownHandler(event: KeyboardEvent){
// 获取当前用户按键的名字
/*
event.key 返回的是字符串
ArrowUp 方向键 上
ArrowDown 下
ArrowRight 左
ArrowLeft 右
但在IE中是: Up, Down, Left, Right
*/
//检查用户是否按了方向键
// if()
this.direction = event.key;
}
// 创建一个控制蛇移动的方法
run(){
// 根据方向(this.direction)修改蛇的位置
// 向上:top减少; 向下:top增加; 向左:left减少;向右:left增加
// 获取蛇当前的坐标
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 "ArrowLeft":
case "Left":
// 向左移动
X -= 10;
break;
case "ArrowRight":
case "Right":
// 向右移动
X += 10;
break;
}
// 修改蛇的位置
this.snake.X = X
this.snake.Y = Y
// 开启定时调用 run方法
this.isLive && setTimeout(this.run.bind(this),300 - (this.scorePanel.level - 1) * 30)
}
}
export default GameControl
6、继续完善各功能:
(1)限制蛇的位置,不能超过主窗口。修改 snake.ts:
// 设置蛇头的坐标
set X(value){
// 如果新值和旧值相同,则直接返回不再修改
if(this.X === value) return;
// 是否撞墙(X的范围是否在 0-290之间)
if(value < 0 || value > 290){
// 进入判断说明蛇撞墙,抛出一个异常。此时游戏会终止
// 在GameControl中 使用try catch来捕获异常
throw new Error('蛇撞墙了')
}
this.head.style.left = value + 'px';
}
set Y(value){
if(this.Y === value) return;
if(value < 0 || value > 290){
// 进入判断说明蛇撞墙
throw new Error('蛇撞墙了')
}
this.head.style.top = value + 'px';
}
(2)完善GameControl.ts
// 游戏控制器,控制其他所有类
class GameControl{
......
// 创建一个控制蛇移动的方法
run(){
......
// 检查蛇是否吃到食物
this.checkEat(X,Y)
try {
// 修改蛇的位置
this.snake.X = X
this.snake.Y = Y
} catch (error) {
// 进入catch说明出现了异常,即弹出提示信息
alert((error as any).message + 'Game Over!')
// 将isLive设为false,终止游戏。否则会一直弹窗
this.isLive = false
}
// 开启定时调用 run方法
this.isLive && setTimeout(this.run.bind(this),300 - (this.scorePanel.level - 1) * 30)
}
// 定义一个检查蛇是否吃到食物的方法
checkEat(X:number, Y:number){
if (X === this.food.X && Y === this.food.Y){
// 吃到食物后,食物的位置要重置
this.food.change()
// 分数增加
this.scorePanel.addScore()
// 蛇增加一节
this.snake.addBody()
}
}
}
export default GameControl
7、身体的移动
(1)直至上一步,蛇新增加的身体还是静止不动的。
(2)蛇是可以掉头的。游戏中不应该掉头(即向上走时不能向下走…)
(3)蛇可以撞自己身体。游戏中应该不能
解决: snake.ts
class snake{
......
// 设置蛇头的坐标
set X(value){
// 如果新值和旧值相同,则直接返回不再修改
if(this.X === value) return;
// 是否撞墙(X的范围是否在 0-290之间)
if(value < 0 || value > 290){
// 进入判断说明蛇撞墙,抛出一个异常。此时游戏会终止
// 在GameControl中 使用try catch来捕获异常
throw new Error('蛇撞墙了')
}
// 修改x时是在修改水平坐标,蛇在左右移动.蛇向左时不能向右掉头,反之亦然
// 蛇头的坐标和第二节身体的坐标一致,则表明掉头。刚开始只有一节身体,所以要先判断是否有第二节
if(this.bodies[1] && (this.bodies[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 > 290){
// 进入判断说明蛇撞墙
throw new Error('蛇撞墙了')
}
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value){
// 如果发生了掉头,让蛇向反方向移动
if(value > this.Y){
// 如果新值 value 大于 X, 则说明蛇在向右走,此时发生掉头,应该让蛇继续向左走
value = this.Y - 10
}else{
value = this.Y + 10
}
}
// 移动身体
this.moveBody();
this.head.style.top = value + 'px';
// 检查有没有撞到自己
this.checkHeadBody();
}
// 蛇吃到食物后增加身体
addBody(){
// 向element中添加一个div
this.element.insertAdjacentHTML("beforeend","") //把加到element结束标签的前面
}
// 添加蛇身体移动的方法,在 set中调用
moveBody(){
// 蛇是一节一节的向前移。最好的方式是将后边身体的位置 设置为 前边身体的位置
// 第4节 = 第三节位置
// 遍历获取所有的身体(从后往前改)
for(let i = this.bodies.length-1; i > 0; i--){
// 获取前边身体的位置
let X = (this.bodies[i-1] as HTMLElement).offsetLeft; //不加的话 .offset会报错
let Y = (this.bodies[i-1] as HTMLElement).offsetTop;
// 将值设置到当前身体上
(this.bodies[i] as HTMLElement).style.left = X + 'px';
(this.bodies[i] as HTMLElement).style.top = Y + 'px';
}
}
// 检查蛇头与身体是否相撞
checkHeadBody(){
// 获取所有的身体,检查是否和蛇头的坐标发生重叠
for(let i = 1; i < this.bodies.length; i++){
let bd = this.bodies[i] as HTMLElement
if(this.X === bd.offsetLeft && this.Y === bd.offsetTop){
// 进入判断说明蛇头撞到身体
throw new Error('撞到自己')
}
}
}
}
export default snake