初学面向对象比较难,基本每行都有注释,大家要耐心,细心。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇</title>
<style>
.map {
width: 400px;
height: 400px;
background-color: black;
position: relative;
}
</style>
</head>
<body>
<h2 id="grade">0</h2>
<!-- 记录分数 得起到事件监听的作用 -->
<div class="map"></div>
<script>
// 根本性的 流程 先有数据准备数据 更新数据 再把数据渲染到视图上(数据驱动地图)
{
class Map { //Map地图类 rect 定义一个当前每个小格的宽。
constructor(el, rect = 10) { //接收实例化对象gameMap的内容
this.el = el;
this.rect = rect;
this.data = [
// {x:5,y:8,color:"red"}, //x y 只是测试数据(蛇出现的位置)
// {x:6,y:8,color:"#fff"},
// {x:7,y:8,color:"red"},
];
// 去找行和列
//行列数 = 总宽高 / 行列的大小 获取
this.rows = Math.ceil(Map.getStyle(el, "height") / rect);
this.cells = Math.ceil(Map.getStyle(el, "width") / rect);
//向上取正 行数可以多 少了不行
//通过行列数设置总宽高
Map.setStyle(el, "height", this.rows * rect);
Map.setStyle(el, "width", this.cells * rect);
//上面获取,一遍 再设置一遍 ,为的是 地图的大与小在 传参的时候可以控制他
// 如果不设置 他默认的宽高,以及配套的行列数 如果设置地图的大小 会随着行列数的改变
}
static getStyle(el, attr) { //静态方法
return parseFloat(getComputedStyle(el)[attr]) //传入一个元素返回指定的属性值
}
// 因为取整之后,会跟总的宽高不一样 ,需要重新设置一下
static setStyle(el, attr, val) {
el.style[attr] = val + "px"
}
//设置数据 newData接收 concat给一个数组添加数据(添加进来的可以是数组) 添加完有返回值 去前面接收
setData(newData) { //push 不能添加数组
this.data = this.data.concat(newData)
}
// 清除数据
clearData() {
this.data.length = 0;
}
// 判断指定位置,是否包含数据
// x y 形参谁调用谁给他传参数
include({ x, y }) { //some判断数组里的元素是满足指定条件(就是碰撞检测蛇有没有碰到食物)
return this.data.some(item => (item.x == x && item.y == y));
//这里写大括号 是代码段函数
// 不写或小括号 是返回
}
// 通过数据把(蛇)渲染到页面元素
render() {
//map() 遍历data数组的每一项
this.el.innerHTML = this.data.map(item => { //item*rect 第几行*每个小格的宽/高 = 得出位置信息
return `<span style ="position:absolute;left:${item.x * this.rect}px;top:${item.y * this.rect}px; width:${this.rect}px;height:${this.rect}px;background:${item.color};"></span>`
}).join("") //join() 指定拼接 这里数据会处理的更好 //蛇 宽高各占一小格
}
}
// 第二部分食物类
class Food { //默认值,也可写外面
constructor(cells,rows, color = ["red", 'yellow', "pink"]) { //地图实例方法已经在Food当中 需要直接调用
this.cells =cells;//保存行
this.rows = rows;
this.data = null; // 储存食物
this.color = color
this.create(); //初始化食物(早不到x 报错) 因为游戏还没开始 刷新应该有初始蛇 初始食物,初始地图三样
// 游戏开始 再去操控他修改 和删除 这三样东西
}
//创建食物
create() {
//create this > 指向实例化
//静态 create this 指向方法
let x = Math.floor(Math.random() * this.cells);
let y = Math.floor(Math.random() * this.rows);
//食物颜色 // 0-数组长度随机
let color = this.color[parseInt(Math.random() * this.color.length)]
this.data = { x, y, color } //
// 把自身传进去判断 看一下是否和蛇的位置重叠了
// if (this.map.include(this.data)) { //include 传进去一个数据 返回true
// this.create(); // 如果是true 调用自己方法重新走一遍再判断
// //否则 结束
// }
// //地图类设置数据 //食物类的
// this.map.setData(this.data)
}
}
// 第三部分蛇类
class Snake { ///创建一空的蛇类
constructor() { //食物和地图添加进来
this.data = [
{ x: 6, y: 4, color: "red" },
{ x: 5, y: 4, color: "blue" },
{ x: 4, y: 4, color: "blue" },
{ x: 3, y: 4, color: "blue" }
];
// this.map = map;
// this.food = food;
this.direction = "right";
this.lastData = {}
}
//蛇的移动
move() { //for循环每一格都往前添加
let i = this.data.length - 1
this.lastData = {
x: this.data[i].x, //吃到的食物 应该天机到最后一位this.data.length - 1
y: this.data[i].y,
color: this.data[i].color
}
for (i; i > 0; i--) { //这里 是从后往前倒着
//假如i =4 这里[]中就是3 也就是说把后面的数据不断往前添加
this.data[i].x = this.data[i - 1].x;
this.data[i].y = this.data[i - 1].y;
}
//根据方向,来移动蛇头
switch (this.direction) {
case "left": //向左 --
this.data[0].x--;
break;
case "right": //右 ++
this.data[0].x++;
break;
case "top":
this.data[0].y--;
break;
case "bottom":
this.data[0].y++;
break;
}
}
//根据蛇头移动的方向,来禁止相同的方向
changeDir(dir) {
// 如果蛇本身当前是在左右移动,我们只能修改让蛇上下移动
// 上下移动时,同样只能让其左右移动
// 如果当前的方向是左或者右
if (this.direction === "left" || this.direction === "right") {
// 传入的新方向如果是左 或者 右
if (dir === "left" || dir === "right") {
return false; //不能修改 结束的是整段
}
} else {
// 否则当前的方向是上 或者 下
// 如果传入的新方向是上 或者 下
if (dir === "top" || dir === "bottom") {
return false; //不能修改
}
}
this.direction = dir;
return true;
}
//吃到了食物,蛇会变大
eatFood() {
this.data.push(this.lastData); // 变大的是this.lastData
}
}//蛇类
// 第四游戏类 总控制
class Game {
constructor(el, rect, toControl = null,toGrade =null) {
//着三个类原来在外面放在这里隐藏起来 方便游戏类进行控制
this.map = new Map(el, rect) // 地图
this.food = new Food(this.map.cells,this.map.rows);// 食物类 直接把地图的列,和地图的行
this.snake = new Snake(this.map); //蛇
this.map.setData(this.snake.data) //蛇放到地图中
this.createFood(); //检测食物
this.render(); // 绘制初始地图 就是刷新页面时候 有东西在 蛇已经在地图里就蛇就被带进去了
this.timer = 0; // 声明一下 说明他的存在
this.ineterval = 200;
this.toControl = toControl //控制器
this.keyDown = this.keyDown.bind(this) ;//保存当前this这个保存具体某个值 前用那个保存方法更全面
this.grade = 0; //记录分数
this.toGrade = toGrade //改变分数 事件监听
this.control();// 调用控制器
}
// 将食物 和地图产生关系 食物渲染到地图中
createFood(){
this.food.create(); //创建食物
// 把自身传进去判断 看一下是否和蛇的位置重叠了
if (this.map.include(this.food.data)) { //include 传进去一个数据(把食物传进地图) 返回true
this.createFood(); // 如果是true 调用自己方法重新走一遍再判断
//否则 结束
}
//地图类设置数据 //食物类的
this.map.setData(this.food.data)
}
//开始游戏
start() {
this.move(); // 开始移动
}
//暂停游戏
stop() {
clearInterval(this.timer) //清除定时器
}
// 给地图渲染数据
render(){
this.map.clearData(); // 每次渲染首先清除数据 为了不让旧数据(已经渲染)新数据冲突,重叠,叠加
this.map.setData(this.snake.data); //蛇数据放进来 这里只控制结果 具体过程在具体的蛇类里
this.map.setData(this.food.data); //食物的类
// console.log(this.map.data) // 有个问题 找不到x 输出地图 食物为空没有数据 那就去食物类里找(没有初始食物)
this.map.render(); //绘制地图
}
//控制移动
move() {
this.timer = setInterval(() => {
this.snake.move();//蛇开始移动
// this.map.clearData(); // 清除移动后重影的数据
if(this.isEat()){ //清除数据后判断是否吃到食物
this.grade++ //吃到食物 分数++
console.log(this.grade)
this.snake.eatFood(); //吃到的话 蛇变大
this.createFood(); // 被吃掉之后 新食物出现
this.changeGrade(this.grade) //上面grade值发生改变 传到下面changeGrade函数里 函数接收到 看一下 外面有没有方法
// r如果有 就去调用把参数传进去
this.ineterval *=0.9 // 吃掉食物后 速度变快
this.stop(); // ineterval的原因定时器的运行速度更新了 不再是原来的速度
this.start(); // 需要重新开启 更新速度 不重启的话ineterval虽然改变了 但定时器已经走起来 影响不了定时器
if(this.grade >= 3){ // 分数增长重启定时器 就去判断分数 大于3分
this.over(1); // 传一个状态码 调用游戏状态码 再去判断是胜利 还是 失败结束
}
}
if(this.isOver()){ //每次移动 吃完 调用
this.over(); //如果碰到 游戏结束
return;
}
// this.map.setData(this.snake.data); //蛇数据放进来 这里只控制结果 具体过程在具体的蛇类里
// this.map.setData(this.food.data); //食物的类
// console.log(this.map.data) // 有个问题 找不到x 输出地图 食物为空没有数据 那就去食物类里找(没有初始食物)
this.render(); //上面的三行集中写完外面,这里调用了
//以上这些都是数据的操作
}, this.ineterval) //速度写成属性(变量) 吃一个速度加快一点
}
// 判断是否吃到食物 去判断蛇头的x,y 值与食物的x,y值位置 当相等时说明碰撞在一起
isEat(){
return (this.snake.data[0].x == this.food.data.x) &&
(this.snake.data[0].y == this.food.data.y)
}
// 改变分数 事件监听
changeGrade(grade){ //grade 传一个参数 给外面留接口 不写死
this.toGrade && this.toGrade(grade); //首先看看有没没有用户自身调用这个方法 如果有 就去调用这个方法
//实参grade 会传到调用他的地方
}
//判断是否结束
isOver() {
// 根据蛇头的位置 去判断蛇头 是否出了地图
let i = this.snake.data[0];
//左 右
if(i.x < 0 || i.x >= this.map.cells
|| i.y < 0 || i.y >= this.map.rows
){
return true;
}
//判断蛇撞到了自己的肉体
for(let i=1; i<this.snake.data.length; i++){
if(this.snake.data[0].x == this.snake.data[i].x && // 判断蛇头 和每一节蛇身体是否相等
this.snake.data[0].y == this.snake.data[i].y
){
return true;
}
} return false
}
// overState 0 中间停止,碰壁,吃自己
// 1 胜利了 游戏 结束
//游戏结束
over(overState = 0) {
if(overState){
this.toWin && this.toWin(); // 如果方法存在调用
}else{
this.toOver && this.toOver(); //游戏失败结束
}
this.stop();
}
//键盘事件 37 38 39 40 左 上 右 下
keyDown({ keyCode }) {
let isDir; //输出当前方向,是否修改成功
// console.log(keyCode);
switch (keyCode) {
case 37:
isDir = this.snake.changeDir("left"); //蛇里面控制方向的方法 给他传一个新方向
//新方向通过一系列类的方法测试 如果通过测试 才修改当前方向
// 然后会有一个布尔返回值
break;
case 38:
isDir = this.snake.changeDir("top");
break;
case 39:
isDir = this.snake.changeDir("right");
break;
case 40:
isDir = this.snake.changeDir("bottom");
break;
}
console.log(isDir) //是否修改成功 都会有一个返回值 可以拿这个返回值做,一个用户提示
}
//控制器 更换键位作用
control(){
//判断用户是否添加了控制器 通过控制器上下左右可以改成 W A S D
if(this.toControl){ //如果存在
this.toControl(); //调用方法
return; //结束
}
window.addEventListener("keydown",this.keyDown) //上 如果没写 给window添加一个键盘按下事件
}
addControl(fn){ //添加控制器
fn.call(this); //调用(继承) fn 自己使用
window.removeEventListener("keydown",this.keyDown); //下 this发生改变 上面得保存
} //这几行代码 当我有新的控制器添加进来的时候 借用一下把 上面的添加进来 把下面的删除掉
} //游戏类
{
let map = document.querySelector(".map")// 获取html元素
// let gameMap = new Map(map, 10) // 地图类获取后传到实例 这里实例化对象是入口
// 地图类、食物类、蛇类 都放到这是为了 测试 注释了放到游戏类的costructor里
// let gameFood = new Food(gameMap); //食物类
// let gameSnake = new Snake(gameMap, gameFood); //实例化一下 ,需要穿两个参数进去 地图和食物
let game = new Game(map, 10); //游戏类实例 元素 大小 10后面也可以在传分数的参数
let gradel = document.querySelector("#grade"); //获取分数
game.toWin = function(){
alert("您胜利了,真棒")
}
game.toOver = function(){
alert("游戏结束");
}
game.toGrade = function(){ // 写个函数调用
// console.log(grade)
gradel.innerHTML = game.grade; // 设置分数 然后添加事件监听
}
//w: 87 上
//d: 68 右
//s: 83 下
//a: 65 左 //
game.addControl(function(){ // 这里新定义上下左右键 通过上面代码 现在使用把键位替换wasd
window.addEventListener("keydown",({keyCode})=>{
switch (keyCode) {
case 65:
this.snake.changeDir("left");
break;
case 87:
this.snake.changeDir("top");
break;
case 68:
this.snake.changeDir("right");
break;
case 83:
this.snake.changeDir("bottom");
break;
}
})
})
document.onclick = function(){
game.start(); //点击空白处加载游戏
}
//
}
}
</script>
</body>
</html>