目录
- 效果展示:
- 功能简介:
- 1.通关方法:
- 2.操作方法:
- 3.小蛇运动规则:
- 制作思路
- 代码特点
- 代码书写
- 0.结构搭建
- 1.游戏场景搭建:先绘制每次都不变的样式(index.html,index.css)
- ①html
- ②CSS
- 2.创建蛇对象(snake.js)
- ①在构造器里添加蛇对象的属性:
- ②在原型链上添加蛇对象的方法:
- 为什么要在原型链上添加方法?
- 3.创建食物对象(food.js)
- ①在构造器里添加食物对象的属性:
- ②在原型链上添加食物对象的方法:
- 4.设计游戏的玩法(index.html)
- ①创建游戏对象的构造函数
- ②在游戏对象的构造函数里实例化蛇对象和食物对象
- ③添加游戏对象的各种属性
- ④将游戏的各种方法放入构造函数内
- 开始游戏的方法
- 暂停游戏的方法
- 结束游戏的方法
- 吃到食物后加速的方法&运动方向改变的方法
- 长按方向键快速移动的方法
- 绘制map地图和地图中的格子
- 4.玩家操作游戏的代码
- 细节优化
- 出现问题1:
- 出现问题2:
- 出现问题3:
- 总体回顾
- 面向对象特点:
效果展示:
屏幕录制 2024-04-26 012419
功能简介:
1.通关方法:
- 小蛇吃到一个小球得五分,且运动速度会加快(最快不超过100毫秒)。当分数达到100分时,通关游戏。
- 可在未达到100分时手动结束游戏。
2.操作方法:
- 上下左右键操控方向;
- 按空格键暂停;
- 按回车键继续游戏;
- 长按方向键加速运动。
3.小蛇运动规则:
可:
可撞到自己身体;
可长按方向键加速运动。
不可:
向右走的时候不能直接向左走,也就是说不能直接回头;
不可撞墙,会死亡。
制作思路
- 绘制地图(蛇移动的区域);
- 绘制小蛇,并实现蛇对象相关的方法;
- 绘制食物,使其在随机位置出现;
- 设置游戏的玩法(开始,结束,暂停,死亡,通关,得分机制,游戏内容,难度变化,各种情况的提示框等);
- 绑定各个按钮、键盘按键的事件监听,实现人机交互。
代码特点
- 可在一个页面集中修改、自定义地图的宽高,蛇的初始长度,初始速度,加速百分比,吃到一个食物的得分,最大速度,便于管理。
实现方法
:
通过给构造器传入实参的形式把死的参数写活。
- 在index.html页面里集中对参数进行赋值,并在实例化的时候传入实参。
- 在构造函数里用“this.形参名=形参”接收实参的值。
- 使用对象的方法时,因为“this.形参名”已有实参的值,所以无需再传入参数,直接使用“this.形参名”。
function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
//传入可自定义的小蛇初始速度speed,吃到食物后速度增加的百分比percent,
//吃到一个食物的得分scoreAdd,最大速度maxSpeed,地图的宽和高mapWidth、mapHight
//用“this.形参名=形参”接收,“this.形参名”有了传入的实参的值
this.scoreAdd = scoreAdd
this.speed = speed
this.cunt = 0
this.over = 0
this.timer = 0 //定时器id
this.percent = percent
this.maxSpeed = maxSpeed
this.mapWidth=mapWidth
this.mapHight=mapHight
//对象的方法无需再传入参数
this.speedUp = function () {
//直接使用this.percent
this.speed = this.speed * (this.percent / 100)
clearInterval(this.timer)
this.start(this.speed, this.percent, this.scoreAdd, this.maxSpeed)
}
})
- 在index.html页面里集中对参数进行赋值
//可以修改参数的值的地方
const bodyLength=4
const mapWidth=800 /* 需要是20的整数倍 */
const mapHight=500
const speed=400
const percent=90
const scoreAdd=5
const maxSpeed=100
//实例化时传入参数
const game = new Game(".map", ".score span", ".over", ".over div >span",".time span",speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight)
代码书写
0.结构搭建
1.游戏场景搭建:先绘制每次都不变的样式(index.html,index.css)
①html
<div class="over">
<span></span>
<div>得分: <span>0</span></div>
</div>
<div class="btnbox">
<div>贪吃蛇</div>
<button id="start">开始</button>
<button id="pause">暂停</button>
<button id="game-over">结束</button>
<div class="score">得分: <span>0</span></div>
</div>
<div class="tips">
<p>tips:</p>
<p>按空格暂停</p><p>按回车开始</p>
<p>上下左右控制方向</p>
<p>通关条件:</p>
<p>得分达到100分</p>
</div>
<div class="map">
//这里还要通过js绘制一千个格子作为map的背景,
//并且实现管理者可以自行修改map地图的大小和格子数
</div>
<script src="./food.js"></script>
<script src="./snake.js"></script>
<script src="./game.js"></script>
②CSS
* {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
background-color: #157c93;
}
.btnbox {
position: fixed;
width: 150px;
height: 400px;
/* background-color: wheat; */
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.btnbox div {
color: #e1e779;
font-size: 30px;
text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;
}
button {
width: 100px;
height: 50px;
font-size: 23px;
background: #2f3640;
color: #F3F0E7;
box-shadow: 0 0 5px #2f3640;
text-shadow: 0px 0px 4px #fff;
}
.btnbox .score{
color:#fff;
font-size: 28px;
text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;
}
.map {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
background-color: #3B5666;
width: 800px;
height: 500px;
border: #EDECE8 8px solid;
/* border-radius: 10px; */
box-shadow: 0 0 8px #F3F0E7 ;
}
.map>div {
width: 20px;
height: 20px;
position: absolute;
border-radius: 50%;
box-shadow: 0px 0px 10px #96dfe7;
}
div.body {
background: repeating-radial-gradient(#F3F0E7, #93d5dc);
}
div.head {
left: 20px;
background: repeating-radial-gradient(#93d5dc, #6bc1ca);
}
div.food {
background: repeating-radial-gradient(#4293d5,#2775b6);
animation: footAnimation 1s linear infinite;
}
@keyframes footAnimation {
0%{
box-shadow: 0 0 10px #8db8dc;
}
50%{
box-shadow: 0 0 80px #206399;
}
100%{
box-shadow: 0 0 10px #8db8dc;
}
}
.over{
display: none;
width: 400px;
height: 300px;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;/* 这几行能实现居中 */
background-color: #EDECE8;
color: #e1e779;
font-size: 70px;
z-index: 100;
line-height: 220px;
text-align: center;
text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;
box-shadow: 0 0 10px #F3F0E7 ;
border-radius: 8px;
}
.over div{
position: absolute;
left: 50%;
transform: translate(-50%,-10%);
bottom: 0;
line-height: 150px;
color: #e1e779;
text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;
font-size:35px;
}
.li{
float: left;
list-style: none;
width: 20px;
height: 20px;
box-sizing: border-box;
border: 1px rgba(5,27,32,.5) solid;
}
.tips{
position: absolute;
bottom: 10%;
left: .3%;
display: flex;
flex-direction: column;
align-items: center;
width: 142px;
color:#e1e779;
border: 2px #e1e779 dashed;
border-radius: 8% 0%;
}
2.创建蛇对象(snake.js)
①在构造器里添加蛇对象的属性:
function Snake(select,bodyLength) {
//传入一个地图元素,并捕获
this.map = document.querySelector(select)
//蛇运动的方向
this.direction = "right"
//蛇的数组(把蛇的头和身体都会存储到数组当中,头从数组的第0位开始)
this.snakeList = []
this.head = null
this.pos = {}//坐标
//接收传入的参数
this.bodyLength=bodyLength
//调用创建蛇的方法
this.createSnake()
}
②在原型链上添加蛇对象的方法:
包含:1. 创建蛇头的方法
2. 创建蛇的方法
3. 蛇移动的方法
4. 判断是否吃到食物的方法
5. 判断是否撞墙的方法
Snake.prototype.createHead = function () {
this.head = this.snakeList[0]
//创建的新的小球的坐标
this.pos = {
x: 0,
y: 0
}
if (this.head) {
//如果有蛇头那么创建新的小球放到原先蛇头的后面坐标位置上,作为新的蛇头,之前的蛇头变为身体
switch (this.direction) {
case "left":
this.pos.x = this.head.offsetLeft - 20;
this.pos.y = this.head.offsetTop;
break;
case "right":
this.pos.x = this.head.offsetLeft + 20;
this.pos.y = this.head.offsetTop;
break;
case "top":
this.pos.x = this.head.offsetLeft;
this.pos.y = this.head.offsetTop - 20;
break;
case "bottom":
this.pos.x = this.head.offsetLeft;
this.pos.y = this.head.offsetTop + 20;
break;
default:
break;
}
//把上一个创建的球的样式从蛇头改为蛇的身体样式
this.head.className = "body"
}
const div = document.createElement("div") //创建蛇头
this.map.appendChild(div)
div.className = "head"
this.snakeList.unshift(div)
div.style.left = this.pos.x + "px"
div.style.top = this.pos.y + "px"
}
//新增蛇的方法
Snake.prototype.createSnake = function () {
for (let i = 0; i < this.bodyLength; i++) {
this.createHead()
}
}
//蛇移动的方法
Snake.prototype.move = function () {
const body = this.snakeList.pop() //从数组中移除并返回最后一个元素
body.remove() //从页面删除
this.createHead()
}
//判断蛇有没有吃到食物
Snake.prototype.isEat = function (foodX, foodY) {
const head = this.snakeList[0]
const headX = head.offsetLeft
const headY = head.offsetTop
if (foodX === headX && foodY === headY) {
return true
}
return false
}
//是否撞墙
Snake.prototype.isDie = function () {
const head = this.snakeList[0]
const headX = head.offsetLeft
const headY = head.offsetTop
if (headX < 0 || headY < 0 || headX >= this.map.clientWidth || headY >= this.map.clientHeight) {
return true
}
return false
}
为什么要在原型链上添加方法?
- 将方法写在原型链上,可以使得所有由Snake 构造函数创建的实例都可使用写在函数原型上的方法(如:move方法),而不需要每个实例都有自己的 move 方法。
- 同时这样能使创建对象的时候不创建方法,而是调用的时候才创建方法。
3.创建食物对象(food.js)
①在构造器里添加食物对象的属性:
function Food(select) {
//***小栗子 */
/* this指的是实例化的对象,指向的是那个实例*/
//传入一个地图元素,并捕获
this.map = document.querySelector(select)
//创建食物元素
this.food = document.createElement("div")
//这样子就能把食物元素放到地图元素中
this.map.appendChild(this.food)
//定义样式
this.food.className = "food"
this.x = 0
this.y = 0
this.foodPos()
}
②在原型链上添加食物对象的方法:
随机在某一位置生成食物的方法:
Food.prototype.foodPos=function() {
//1.将长和宽划分为...份//为什么要除以20再乘以20是为了让他出现在格子上而不是两个格子中间
const w = this.map.clientWidth / 20
const h = this.map.clientHeight / 20
//2.随机生成数字
let n1 = Math.floor(Math.random() * w) //相当于总的份数乘一个百分比,就可以得到0-w范围内的数字
let n2 = Math.floor(Math.random() * h)
//3.将坐标转化为长度(因为食物的位置是要具体的px)
this.x = n1 * 20
this.y = n2 * 20
//4定位
this.food.style.left = this.x + "px"
this.food.style.top = this.y + "px"
}
4.设计游戏的玩法(index.html)
因为游戏只有一个,所以游戏对象的方法没有写在原型链上
①创建游戏对象的构造函数
function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
//创建游戏对象的构造函数
}
②在游戏对象的构造函数里实例化蛇对象和食物对象
目的:这样就可以通过food实例和snake实例使用对象里的属性和方法。
function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
this.food = new Food(select)
this.snake = new Snake(select,bodyLength)
})
③添加游戏对象的各种属性
function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
//实例化
this.food = new Food(select)
this.snake = new Snake(select,bodyLength)
//捕获元素
this.map = document.querySelector(select)
this.gameover = document.querySelector(gameover)
this.score = document.querySelector(score)
this.time = document.querySelector(time)
this.finalScore = document.querySelector(finalScore)
this.startBtn = document.querySelector("#start")
this.ending = document.querySelector(".over span")
this.map = document.querySelector(".map")
this.ul = document.createElement("ul")
this.cunt = 0
this.over = 0
this.timer = 0 //定时器id
//接收传入的参数
this.scoreAdd = scoreAdd
this.speed = speed
this.percent = percent
this.maxSpeed = maxSpeed
this.mapWidth=mapWidth
this.mapHight=mapHight
})
④将游戏的各种方法放入构造函数内
包含:
- 开始游戏的方法
- 暂停游戏的方法
- 结束游戏的方法
- 吃到食物后加速的方法
- 运动方向改变的方法
- 长按方向键快速移动的方法
- 绘制map地图和地图中的格子
开始游戏的方法
this.start = function () {
//(细节优化)禁用开始、结束按钮
this.startBtn.disabled = true
//(细节优化)添加方向键的事件监听,使得可以通过方向键操控运动方向
document.addEventListener("keydown", keyDownDirection)
//(细节优化)如果出现游戏结束的提示框,则重新加载页面
if (this.gameover.style.display === "block") {
window.location.reload() //***重新加载了会停止执行代码,后面的代码不会执行
}
this.timer = setInterval(() => {
this.snake.move()
if (this.snake.isEat(this.food.x, this.food.y)) {
//吃到食物要变长,调用增加蛇头的方法id
this.snake.createHead()
this.food.foodPos() //更新食物位置
//更改得分情况
this.cunt += this.scoreAdd
this.score.innerText = this.cunt
if (this.speed >= maxSpeed) {
this.speedUp()
}
}
//得分满100游戏结束
if (this.cunt === 100) {
this.gameOver()
}
if (this.snake.isDie()) {
clearInterval(this.timer)
this.gameOver()
}
}, `${ this.speed}`);
}
暂停游戏的方法
this.pause = function () {
clearInterval(this.timer)
//(细节优化)不禁用开始、结束按钮
this.startBtn.disabled = false
//(细节优化)添加方向键的事件监听,使得可以通过方向键操控运动方向
document.removeEventListener("keydown", keyDownDirection)
}
结束游戏的方法
this.gameOver = function () {
//出现提示框
this.gameover.style.display = "block"
clearInterval(this.timer)
//写入得分
this.finalScore.innerText = this.cunt
//(细节优化)不禁用开始、结束按钮
this.startBtn.disabled = false
console.log(this.speed)
document.removeEventListener("keydown", keyDownDirection)
//三种不同的情况显示不同的文字,分别为通关,死亡,玩家主动点击结束按钮(默认)
//1(默认)
this.ending.innerText = "游戏结束"
//2
if (this.cunt === 100) {
this.ending.innerText = "恭喜通关"
}
//3
if (this.snake.isDie()) {
this.ending.innerText = "游戏失败"
}
}
吃到食物后加速的方法&运动方向改变的方法
this.speedUp = function () {
this.speed = this.speed * (this.percent / 100)
clearInterval(this.timer)
this.start(this.speed, this.percent, this.scoreAdd, this.maxSpeed)
}
this.change = function (type) {
this.snake.direction = type
}
长按方向键快速移动的方法
思路:
- 一开始的时候一直在想改变定时器时间的事情,后面发现可以通过在按方向键的时候调用一个移动的函数。
- 这个函数里必须包括吃到小球后得分,变长,死亡,游戏通关等游戏的基本规则的代码,否则小蛇吃到小球后不得分,也不变长,撞墙也不会死亡。
这样也能够使快速交替按左键和上键的时候,小蛇运动更灵活,不会像原本一样受定时器的限制。(因为定时器有时间500毫秒,如果快速按两次时,间隔时间小于500,则会忽略前一次。但修改后不会,只要按方向就一定会改变,小蛇运动更灵活,体验感会更好。)
this.quicklyMove = function () {
this.snake.move()
if (this.snake.isEat(this.food.x, this.food.y)) {
//吃到食物要变长,调用增加蛇头的方法id
this.snake.createHead()
this.food.foodPos() //更新食物位置
this.cunt += this.scoreAdd
this.score.innerText = this.cunt
if (this.speed >= this.maxSpeed) {
this.speedUp()
}
}
if (this.cunt === 100) {
this.gameOver()
}
if (this.snake.isDie()) {
clearInterval(this.timer)
this.gameOver()
}
}
绘制map地图和地图中的格子
this.createCell = function () {
const cellNumber=(this.mapWidth/20)*(this.mapHight/20)
this.map.style.width=this.mapWidth+"px"
this.map.style.hight=this.mapHight+"px"
for (let i = 0; i < cellNumber; i++) {
let li = document.createElement("li")
li.className = "li"
this.ul.appendChild(li)
}
this.map.appendChild(this.ul)
}
4.玩家操作游戏的代码
- 写在index.html页面中
- 在这里面实例化游戏对象
<script>
//管理者可在这里统一修改参数
const bodyLength=4
const mapWidth=800 /* 需要是20的整数倍 */
const mapHight=500
const speed=400
const percent=90
const scoreAdd=5
const maxSpeed=100
const game = new Game(".map", ".score span", ".over", ".over div span",".time span",speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight)
const start = document.querySelector("#start")
const pause = document.querySelector("#pause")
const gameOver = document.querySelector("#game-over")
//调用绘制地图和格子的函数
game.createCell()
//按钮的事件监听
start.onclick = function () {
game.start()//传入初始速度,时间变快的百分比
this.blur()//(细节优化:点击按钮后按钮失去焦点)
//(如果不写,按空格会再点击一次按钮)
}
pause.onclick = function () {
game.pause()
this.blur()
}
gameOver.onclick = function () {
game.gameOver()
this.blur()
}
//方向键的事件监听
//键盘监听左37上38右39下40
function keyDownDirection(e) {
switch (e.keyCode) { //获取键对应的数字
case 37:
if (game.snake.direction !== "right") {
game.change("left")
game.quicklyMove()
}
break;
case 38:
if (game.snake.direction !== "bottom") { //不能直接用snake.direction,因为这个页面没有实例化snake实例化了game,
//需要通过game引入,全部都在game里面实例化了
game.change("top")
game.quicklyMove()
}
break;
case 39:
if (game.snake.direction !== "left") {
game.change("right")
game.quicklyMove()
}
break;
case 40:
if (game.snake.direction !== "top") {
game.change("bottom")
game.quicklyMove()
}
break;
default:
break;
}
}
//开始、结束键的事件监听
function keyDownStartPause(e) {
switch (e.keyCode) { //获取键对应的数字
case 13:
if (!game.startBtn.disabled) {
game.start()
}
break;
case 32:
game.pause()
break;
default:
break;
}
}
//(细节优化)在这里绑定开始结束键的事件监听,却不绑定方向键的事件监听,(而是在游戏开始的函数里才绑定),是为了防止游戏为开始时按动方向键,开始后会直接往那个方向走
document.addEventListener("keydown", keyDownStartPause)
</script>
细节优化
本文前面写的代码为已优化的代码,在优化的地方均已添加//细节优化
的注释,可配合查看,方便理解。
出现问题1:
点击开始之后,若再次点击开始,会出现定时器叠加的问题。
解决方法
:
- 在开始方法里解除开始按钮的事件监听;
- 在暂停和结束方法里,加上开始按钮的事件监听。
出现问题2:
暂停游戏后,若再按方向键,开始后会直接往那个方向运动。
解决方法
:
- 在暂停游戏的方法里解除方向键的事件监听;
- 在暂停和结束方法里,加上方向键的事件监听。
出现问题3:
页面加载完成但是没有点击开始前,若按动方向键,点击开始后小蛇会直接朝该方向运动。
解决方法
:
不在人机交互页面(也就是index HTML页面,添加方向键的事件监听),而是在开始函数里添加。
总体回顾
面向对象特点:
1.可以通过类(类和构造器是一个东西)将一个对象的属性和方法进行封装。
2.在一个页面中实例化一个对象,就可以通过这个对象调用他的方法。
3.在对象的页面里再实例化,就可以通过第一个对象调用第二个对象再调用它的方法。(如game.snake.direction !== "top"
)【本文的代码是在HTML页面实例化游戏类,游戏类中实例化蛇和食物。】