TypeScript:项目实战—贪吃蛇
2022-03-23 本文已影响0人
生命里那束光
项目实战—贪吃蛇
配置文件 => 画界面 => 写样式 => 写交互 =>最后打包
先讲一下大概思路再上详细代码:
- 1.进行项目的搭建,安装项目中需要的包,完成项目前的准备
- 2.进行项目界面的制作,就是画界面,把大概样子画出来,此时没有任何交互。
- 3.进行类的定义,定义贪吃蛇里面的各类元素:
- 食物(Food类): 获取食物的坐标、修改食物的位置(随机生成)
- 记分牌(ScorePanel类): 记录分数和等级、实现加分功能、实现升级功能
- 蛇(Snake类): 获取和设置蛇头的坐标、蛇身体变长、蛇不能掉头、蛇身体移动、检查蛇头是否撞到身体
- 游戏控制器(GameControl类): 键盘事件、使蛇移动、蛇撞墙、吃食检测
- 4.进行项目的归类,通过导出和引用,优化文件结构和代码,实现index.ts直接使用。
先展示一下最后效果:
1.项目搭建
- 准备好之前的webpack.config.js、tsconfig.json、package.json、package-lock.json四个文件,然后执行npm i安装依赖
- 安装其他依赖:npm i -D less less-loader css-loader style-loader(四个包,因为要使用到less)如果有其他web资源的话则还需引入web资源的加载器,引入方法类似
- 修改webpack配置文件—在rules中添加
//设置less文件的处理
{
test: /\.less$/,
use:[
"style-loader",
"css-loader",
"less-loader"
]
}
这样就能在项目中使用less了。执行npm run build
并打开dist中的index.html即可看到效果。
- 安装postcss来处理css的浏览器兼容性问题:
npm i -D postcss postcss-loader postcss-preset-env
,并在webpack中引入
//设置less文件的处理
{
test: /\.less$/,
use:[
"style-loader",
"css-loader",
//引入postcss
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
browsers: 'last 2 versions'
}
]
]
}
}
},
"less-loader"
]
}
这样就可以看到在打包后的js文件中,有些css属性会加上浏览器前缀。
2.项目界面
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>贪吃蛇</title>
</head>
<body>
<!-- 创建游戏的主容器 -->
<div id="main">
<!-- 设置游戏的舞台 -->
<div id="stage">
<!-- 设置蛇 -->
<div id="snake">
<!-- snake内部的div 表示蛇的各部分 -->
<div></div>
</div>
<!-- 设置食物 -->
<div id="food">
<!-- 添加4个小div 来设置食物的样式 -->
<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>
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;
// 开启绝对定位
position: absolute;
}
}
#food {
// 开启绝对定位
width: 10px;
height: 10px;
position: absolute;
// background-color: red;
// 开启弹性盒子
display: flex;
// 设置横轴为主轴, wrap表示会自动换行
flex-flow: row wrap;
// 设置主轴和侧轴的空白空间分配到元素之间
justify-content: space-between;
align-content: space-between;
left: 40px;
top: 100px;
&>div{
width: 4px;
height: 4px;
background-color: black;
// 使四个div旋转45度
transform: rotate(45deg);
}
}
}
}
// 记分牌
#score-panel {
width: 300px;
display: flex;
// 设置主轴对齐方式
justify-content: space-between;
}
3.定义Food类
Food类为定义食物的类
主要实现
- 获取食物的坐标
- 修改食物的位置(随机生成)
// 定义食物类Food
class Food{
// 定义的一个属性表示食物所对应的元素
element: HTMLElement;
constructor(){
// 获取页面中的food元素并将其赋值给element
this.element = document.getElementById('food')!;
}
// 定义一个获取食物X轴坐标的方法
get X(){
return this.element.offsetLeft;
}
// 定义一个获取食物Y轴坐标的方法
get Y(){
return this.element.offsetTop;
}
// 修改食物位置
change(){
// 生成一个随机的位置
// 食物的位置最小是0, 最大是290
// 蛇移动一次就是一格,一格大小就是10,所以要求食物的坐标必须是整10
// Math.round(Math.random() * 290);//生成一个[0,290]的整数
let top = Math.round(Math.random() * 29) * 10;
let left = Math.round(Math.random() * 29) * 10;
// Math.floor(Math.random() * 30) * 10;//向下取整
this.element.style.left = top + 'px';
this.element.style.top = left + 'px';
}
}
//导出食物模块
export default Food;
4.定义ScorePanel类
ScorePanel类为定义记分牌的类
主要实现
- 记录分数和等级
- 实现加分功能
- 实现升级功能
// 定义表示记分牌的类
class ScorePanel{
// score和level用来记录分数和等级
score = 0;
level = 1;
// 分数和等级所在的元素,在构造函数中进行初始化
scoreEle: HTMLElement;
levelEle: HTMLElement;
// 设置一个变量限制等级
maxLevel: number;
// 设置一个变量表示多少分时升级
upScore: number;
constructor(maxLevel: number = 10, upScore: number = 10){
this.scoreEle = document.getElementById('score')!;//后面加 ! 表示该值一定不为空
this.levelEle = document.getElementById('level')!;
this.maxLevel = maxLevel;
this.upScore = upScore;
}
// 设置加分的方法
addScore(){
// 使分数自增
// this.score++;
// this.scoreEle.innerHTML = this.score + '';
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;
5.定义Snake类
Snake类为定义蛇的类
主要实现
- 获取和设置蛇头的坐标
- 蛇身体变长
- 蛇不能掉头
- 蛇身体移动
- 检查蛇头是否撞到身体
class Snake{
// 表示蛇头的元素
head: HTMLElement;
// 蛇的身体(包括蛇头)
bodies: HTMLCollection;
// 获取蛇的容器
element: HTMLElement;
constructor(){
this.element = document.getElementById('snake')!;
this.head = document.querySelector('#snake > div') as HTMLElement;
// document.querySelectorAll('#snake > div');// nodeList
this.bodies = this.element.getElementsByTagName('div');
}
// 获取蛇的坐标(蛇头坐标)
get X(){
return this.head.offsetLeft;
}
// 获取蛇的Y轴坐标
get Y(){
return this.head.offsetTop
}
// 设置蛇头的坐标
set X(value:number){
// 如果新值和旧值相同,则直接返回不再修改 (加判断只是为了可以减少修改属性的次数,提升性能)
if (this.X === value) {
return;
}
// X值的合法范围0-290之间
if (value <0 || value > 290 ) {
// 进入判断说明蛇撞墙了
throw new Error("蛇撞墙了~~");
}
// 修改x时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
// console.log('水平方向发生了掉头');
// 如果发生了掉头,让蛇向方向继续移动
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: number){
// 如果新值和旧值相同,则直接返回不再修改
if (this.Y === value) {
return;
}
// Y值的合法范围0-290之间
if (value <0 || value > 290 ) {
// 进入判断说明蛇撞墙了
throw new Error("蛇撞墙了~~");
}
// 修改Y时,是在修改水平坐标,蛇在上下移动,蛇在向上移动时,不能向下掉头,反之亦然
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
// console.log('垂直方向发生了掉头');
// 如果发生了掉头,让蛇向方向继续移动
if (value > this.Y) {
// 如果新值value大于旧值Y, 则说明蛇在向下走,此时发生掉头,应该使蛇继续向上走
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", "<div></div>")//添加到结束标签前
}
// 添加一个蛇身体移动的方法
moveBody(){
/*
从后往前改
将后面的身体设置为前面身体的位置
举例子:
第4节 = 第3节的位置
第3节 = 第2节的位置
第2节 = 蛇头的位置
*/
//遍历获取所有的身体
for(let i = this.bodies.length-1; i > 0; i--){
// 获取前面身体的位置
let X = (this.bodies[i-1] as HTMLElement).offsetLeft;
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;
6.定义GameControl类
GameControl类为游戏控制器,来控制其他的所有类。
主要实现
- 键盘事件
- 使蛇移动
- 蛇撞墙
- 吃食检测
// 引入其他的类
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(10, 2);
this.init();
}
// 游戏的初始化方法,调用后游戏即开始
init() {
// 绑定键盘按下的事件
document.addEventListener('keydown', this.keydownHandler.bind(this));
// 涉及到this和bind知识
// 调用run()方法,使蛇移动
this.run();
}
/* 谷歌 ie
ArrowUp Up
ArrowDown Down
ArrowRight Right
ArrowLeft Left
*/
// 创建一个键盘按下的响应函数
keydownHandler(event: KeyboardEvent) {
// console.log(this);
// 需要检查event.key的值是否合法(用户是否按了正确的按键)
// 修改direction属性
this.direction = event.key
// console.log(event.key);
}
// 创建一个控制蛇移动的方法
run() {
/*
根据方向(this.direction)来使蛇的位置改变
向上 top 减少
向下 top 增加
向左 left 减少
向右 left 增加
*/
// 获取蛇现在的坐标
let X = this.snake.X;
let Y = this.snake.Y;
// 根据按键方向修改X值和Y值
switch (this.direction) {
case "ArrowUp":
case "Up":
// 向上移动 top 减少
Y -= 10;
break;
case "ArrowDown":
case "Down":
// 向下移动 top 增加
Y += 10;
break;
case "ArrowLeft":
case "Left":
// 向左移动 left 减少
X -= 10;
break;
case "ArrowRight":
case "Right":
// 向右移动 left 增加
X += 10;
break;
}
// 检查蛇是否吃到了食物
this.checkEat(X, Y);
// if (this.checkEat(X, Y)) {
// console.log('吃到食物了~~');
// // 食物的位置进行重置
// this.food.change();
// // 分数增加
// this.scorePanel.addScore();
// // 蛇要增加一节
// this.snake.addBody();
// }
// 修改蛇的X和Y值
try {
this.snake.X = X;
this.snake.Y = Y;
} catch (e) {
// 进入到catch, 说明出现了异常,游戏结束,弹出一个提示信息
alert(e.message+ 'GAME OVER!');
// 将isLive设置为false
this.isLive = false;
}
// 开启一个定时调用
clearTimeout();
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) {
console.log('吃到食物了~~');
// 食物的位置进行重置
this.food.change();
// 分数增加
this.scorePanel.addScore();
// 蛇要增加一节
this.snake.addBody();
}
}
}
//导出游戏控制器模块
export default GameControl;
7.项目入口文件index.ts
将上述定义的四个类放在文件夹modules中,并将GameControl引入到index.ts中。
// 引入样式
import './style/index.less';
import GameControl from "./modules/GameControl";
const gameControl = new GameControl();
// setInterval(()=>{
// console.log(gameControl.direction);
// }, 1000);