用ES6的类实现面向对象编程
用ES6的类实现面向对象编程:制作一个简单的小游戏
在这篇文章中,我将分享如何使用ES6的类来实现面向对象的编程,并通过开发一个简单的游戏来实践这些概念。我将逐步讲解游戏中的各个模块以及我如何通过类来组织和管理这些模块。
1. 面向对象编程与ES6类的引入
面向对象编程(OOP)是一种将数据和操作这些数据的代码封装在一起的编程范式。ES6引入了类(Class)的概念,使得JavaScript的面向对象编程更加简洁和直观。在这部分,我们将回顾ES6类的基本概念,并了解它们如何在游戏开发中帮助我们更好地组织代码。
2. 游戏设计概述
这款游戏的核心是一只小鸟,在一个充满障碍的世界中飞行。玩家需要控制小鸟跳跃,并尽可能避免碰撞。游戏中包含多个模块,分别控制不同的游戏元素。我设计了以下几个主要类来分别控制这些模块:
- 天空类(Sky):控制背景天空的移动。
- 大地类(Land):控制背景大地的移动。
- 小鸟类(Bird):控制小鸟的跳跃和自由落体。
- 管道类(PipePairGroup):控制管道的生成和销毁,并使它们与大地的速度同步。
- 积分榜类(Rank):显示玩家的游戏积分。
- 游戏类(Game):整合所有的模块,控制游戏的逻辑流程,包括碰撞检测、游戏循环、开始和结束。
3. 如何使用类来管理游戏逻辑
接下来,我将详细讲解每个类的设计与实现,并阐明如何利用面向对象的思想来管理游戏的各个方面。
矩形类(Rectangle)
矩形类是一个抽象类,用来提取其它类中的公共代码,具体:x坐标、y坐标、元素宽度、元素高度、横向速度、纵向速度、dom元素、move方法实现等。
/**
* 矩形抽象类
* @class Rectangle
* @constructor
* @param {Number} top 矩形左上角x坐标, 单位:像素
* @param {Number} left 矩形左上角y坐标, 单位:像素
* @param {Number} width 矩形宽度, 单位:像素
* @param {Number} height 矩形高度, 单位:像素
* @param {Number} xSpeed 矩形x方向速度, 单位:像素/秒
* @param {Number} ySpeed 矩形y方向速度, 单位:像素/秒
* @param {Element} dom 矩形对应的DOM元素
*/
class Rectangle {
constructor(top, left, width, height, xSpeed, ySpeed, dom) {
this.top = top;
this.left = left;
this.width = width;
this.height = height;
this.xSpeed = xSpeed;
this.ySpeed = ySpeed;
this.dom = dom;
}
// 矩形移动
move(duration){
this.top += this.ySpeed * duration;
this.left += this.xSpeed * duration;
this.top = parseInt(this.top);
this.left = parseInt(this.left);
// 矩形移动完成后触发事件
if(this.onmove) {
this.onmove();
}
this.render();
}
// 渲染矩形
render(){
this.dom.style.top = this.top + 'px';
this.dom.style.left = this.left + 'px';
this.dom.style.width = this.width + 'px';
this.dom.style.height = this.height + 'px';
}
}
天空类(Sky)
天空类负责背景的移动。通过定义一个move()
方法,可以让天空不断向左移动,模拟小鸟飞行的感觉。这个类非常简单,但它为游戏提供了重要的视觉效果。
class Sky extends Rectangle {
constructor(xSpeed) {
super(0, 0, skyWidth, skyHeight, xSpeed, 0, sky);
}
onmove(){
if(this.left <= -gameWidth){
this.left = -267 + this.left + gameWidth;
}
}
reset(){
this.left = 0;
this.render();
}
}
大地类(Land)
大地类与天空类类似,用于控制地面背景的滚动。地面与天空的移动速度保持一致,确保游戏的视觉效果流畅。
class Land extends Rectangle {
constructor(xSpeed) {
super(skyHeight, 0, landWidth, landHeight, xSpeed, landYSpeed, land);
}
onmove(){
if(this.left <= -gameWidth){
this.left = this.left + gameWidth;
}
}
reset(){
this.left = 0;
this.render();
}
}
小鸟类(Bird)
小鸟类是游戏的核心,它控制小鸟的跳跃和自由落体。为了让跳跃更加平滑,我根据小鸟的下落速度动态调整上升速度。
class Bird extends Rectangle {
constructor(ySpeed) {
super(100, 200, birdWidth, birdHeight, birdXSpeed, ySpeed, bird);
this.acceleration = acceleration;
this.timer = null;
this.swingIdx = 0;
this.dom.className = 'bird swing' + 1;
this.upAcceleration = 250;
this.keydownHandle = (e)=>{
e.preventDefault();
if (e.code !== "Space") {
return;
}
this.ySpeed -= this.upAcceleration
}
}
taggleSwing() {
this.swingIdx++;
this.swingIdx = this.swingIdx % 3 + 1;
this.dom.className = 'bird swing' + this.swingIdx;
}
startSwing(duration) {
if (this.timer) return;
this.timer = setInterval(() => {
this.taggleSwing();
}, duration);
}
stopSwing() {
if(this.timer){
clearInterval(this.timer);
}
this.timer = null;
}
rgEvent() {
window.addEventListener('keydown', this.keydownHandle)
}
unRgEvent(){
window.removeEventListener('keydown', this.keydownHandle)
}
move(duration) {
super.move(duration);
this.ySpeed += this.acceleration * duration;
if (this.ySpeed > 0) {
this.upAcceleration = this.upAcceleration = this.ySpeed + 250;
} else {
this.upAcceleration = 250;
}
}
reset(){
this.stopSwing();
this.unRgEvent();
this.left = 200;
this.top = 100;
this.swingIdx = 0;
this.dom.className = 'bird swing' + 1;
this.ySpeed = 0;
this.render();
}
}
管道类(PipePairGroup)
管道类负责生成和管理障碍物。它们会随机生成,并随着大地的滚动向左移动。当管道移出屏幕时,它们会被销毁,以释放内存。
class PipePair {
constructor(xSpeed) {
this.tick = 150; // 管道对的空间间隔
const minHeigth = 50;
const maxHeigth = gameHeight - landHeight - minHeigth - this.tick;
const upHeight = createHeight(minHeigth, maxHeigth);
const downHeight = gameHeight - landHeight - upHeight - this.tick;
this.upPipe = new Pipe(0, upHeight, xSpeed, createPipe('up'));
this.downPipe = new Pipe(upHeight + this.tick, downHeight, xSpeed, createPipe('down'));
}
removeSelf() {
game.removeChild(this.upPipe.dom);
game.removeChild(this.downPipe.dom);
}
move(duration) {
this.upPipe.move(duration);
this.downPipe.move(duration);
if (this.upPipe.left <= -this.upPipe.width) {
// 移除管道
this.removeSelf();
}
}
}
class PipePairGroup {
constructor(xSpeed) {
this.pipePairs = [];
this.xSpeed = xSpeed;
this.timer = null;
}
reset(){
for(let pair of this.pipePairs){
pair.removeSelf();
}
this.pipePairs = [];
}
/**
* 开始生成管道对
* @param {*} duration 单位:毫秒
*/
startPipeProduct(duration) {
if (this.timer) {
return;
}
this.timer = setInterval(() => {
this.pipePairs.push(new PipePair(this.xSpeed));
}, duration);
}
/**
* 停止生成管道对
* 该方法清除当前的定时器,并将timer属性重置为null,从而停止管道对的生成
*/
stopPipeProduct() {
// 清除当前的定时器
clearInterval(this.timer);
// 将timer属性重置为null
this.timer = null;
}
clearFreePipe() {
this.pipePairs = this.pipePairs
.filter(pair => pair.upPipe.left > -pair.upPipe.width);
}
move(duration) {
this.pipePairs.forEach(pair => pair.move(duration));
this.clearFreePipe();
}
}
游戏积分榜类(Rank)
积分榜类用于管理和显示玩家的积分。我使用Object.defineProperty
创建了一个计算属性,在数据变化时重新渲染UI。
class Rank{
constructor(){
this.rankDom = rank;
this._rankData = [];
this.storeDom = store;
this._curStore = 0;
}
addStore(val){
this.curStore += val;
}
subStore(val){
this.curStore -= val;
}
updateStoreText(){
this.storeDom.innerText = `当前积分:${
this.curStore}`;
}
runStore(){
this.addStore(1);
this.updateStoreText();
}
addStoreToRank(){
this.rankData = this.curStore;
this.curStore = 0;
}
rankHtml(){
return this.rankData.map(item=>{
return `<li>
${
item} 分
</li>`;
}).join("");
}
renderRank(){
this.rankDom.innerHTML = this.rankHtml();
}
}
游戏类(Game)
游戏类是游戏的主控制类,负责整体的游戏循环、碰撞检测以及游戏的开始、结束和重置。它使用setInterval
来定期执行游戏逻辑。
class Game {
constructor() {
this.rankControler = new Rank();
Object.defineProperty(this.rankControler, 'rankData', {
set(val){
this._rankData.unshift(val);
this.renderRank();
},
get(){
return this._rankData;
}
})
Object.defineProperty(this.rankControler, 'curStore', {
set(val){
this._curStore = val;
this.updateStoreText();
},
get(){
return this._curStore;
}
})
this.timer = null;
// 初始化游戏内容
this.sky = new Sky(skyXSpeed);
this.land = new Land(landXSpeed);
this.bird = new Bird(birdYSpeed);
this.pipePairGroup = new PipePairGroup(landXSpeed);
this.isGameStart = false;
this.isGameOver = false;
this.keydownHandler = (e) => {
if (e.code === "Enter") {
if (this.isGameOver) {
// 重新开始游戏
this.reset();
return;
}
if (this.isGameStart) {
// 游戏已经开始了,暂停游戏
this.isGameStart = false;
this.stopGame();
} else {
this.isGameStart = true;
this.bird.rgEvent();
this.startGame(gameDuration);
}
}
}
}
reset() {
this.bird.reset();
this.land.reset();
this.sky.reset();
this.pipePairGroup.reset();
this.isGameOver = false;
this.isGameStart = false;
}
/**
* 矩形碰撞检测
* @param {*} lx 第一个矩形的x轴中间坐标
* @param {*} ly 第一个矩形的y轴中间坐标
* @param {*} rx 第二个矩形的x轴中间坐标
* @param {*} ry 第二个矩形的y轴中间坐标
* @param {*} w 两个矩形的半宽的和
* @param {*} h 两个矩形的半高的和
*/
collision(lx, ly, rx, ry, w, h) {
return Math.abs(lx - rx) <= w && Math.abs(ly - ry) <= h;
}
collisionPipe() {
// 检测小鸟是否碰到了管道
const birdRight = this.bird.left + this.bird.width; // 小鸟的右边界
const birdUp = this.bird.top; // 小鸟的上边界
const birdDown = this.bird.top + this.bird.height; // 小鸟的下边界;
let isCollide = false;
this.pipePairGroup.pipePairs.forEach(pair => {
const birdXmid = this.bird.left + this.bird.width / 2; // 小鸟的x轴中间位置
const birdYmid = this.bird.top + this.bird.height / 2; // 小鸟的y轴中间位置
const pipeUpXmid = pair.upPipe.left + pair.upPipe.width / 2; // 上管道的x轴中间位置
const pipeDownXmid = pair.downPipe.left + pair.downPipe.width / 2; // 下管道的x轴中间位置
const pipeUpYmid = pair.upPipe.top + pair.upPipe.height / 2; // 上管道的y轴中间位置
const pipeDownYmid = pair.downPipe.top + pair.downPipe.height / 2; // 下管道的y轴中间位置
const halfWidthSum = this.bird.width / 2 + pair.upPipe.width / 2; // 两个矩形的半宽的和
if (this.collision(birdXmid, birdYmid, pipeUpXmid, pipeUpYmid, halfWidthSum, this.bird.height / 2 + pair.upPipe.height / 2) ||
this.collision(birdXmid, birdYmid, pipeDownXmid, pipeDownYmid, halfWidthSum, this.bird.height / 2 + pair.downPipe.height / 2)) {
// 小鸟碰到了管道
isCollide = true;
}
});
return isCollide;
}
collisionDetection() {
if (this.bird.top <= 0 || this.bird.top >= this.land.top - this.bird.height) {
// 小鸟掉地上了,游戏结束
return true;
} else if (this.collisionPipe()) {
// 小鸟碰到了管道
return true;
}
return false;
}
judgeStatus(){
if(this.collisionDetection()){
this.stopGame();
this.rankControler.addStoreToRank();
this.isGameOver = true;
}
}
startGame(duration) {
if (this.timer) {
return;
}
// 游戏进程开始
this.bird.startSwing(); // 小鸟开始煽动翅膀
this.pipePairGroup.startPipeProduct(1000);
const secs = duration / 1000;
this.timer = setInterval(() => {
this.rankControler.runStore();
this.sky.move(secs);
this.land.move(secs);
this.bird.move(secs);
this.pipePairGroup.move(secs);
this.judgeStatus();
}, duration);
}
stopGame() {
// 游戏进程结束
this.bird.stopSwing(); // 小鸟停止煽动翅膀
this.bird.unRgEvent();
this.pipePairGroup.stopPipeProduct();
clearInterval(this.timer);
this.timer = null;
}
rgEvent() {
window.addEventListener('keydown', this.keydownHandler);
}
unRgEvent() {
window.removeEventListener('keydown', this.keydownHandler);
}
}
4. 动态UI与交互优化
为了提高游戏的交互体验,我使用了动态UI。通过Object.defineProperty
,当数据发生变化时,UI会自动更新,这大大简化了我的代码并提升了性能。此外,我还根据小鸟的自由落体速度动态调整跳跃的高度,使跳跃更加自然和流畅。
5. UI设计
尽管我对UI设计没有太多经验,但通过与ChatGPT的互动,我获得了一个简洁大方的UI设计方案,并通过HTML和CSS实现了它。UI的设计重点是简洁、清晰,避免了不必要的复杂性,以便玩家能够专注于游戏本身。
6. 总结
通过这个小项目,我加深了对面向对象编程的理解,并用它们构建一个简单的游戏。游戏开发不仅能帮助我提升编程能力,还锻炼了我的逻辑思维和问题解决能力。