Initial commit
This commit is contained in:
89
js/base/animation.js
Normal file
89
js/base/animation.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import Sprite from './sprite';
|
||||
|
||||
const __ = {
|
||||
timer: Symbol('timer'),
|
||||
};
|
||||
|
||||
/**
|
||||
* 简易的帧动画类实现
|
||||
*/
|
||||
export default class Animation extends Sprite {
|
||||
constructor(imgSrc, width, height) {
|
||||
super(imgSrc, width, height);
|
||||
|
||||
this.isPlaying = false; // 当前动画是否播放中
|
||||
this.loop = false; // 动画是否需要循环播放
|
||||
this.interval = 1000 / 60; // 每一帧的时间间隔
|
||||
this[__.timer] = null; // 帧定时器
|
||||
this.index = -1; // 当前播放的帧
|
||||
this.count = 0; // 总帧数
|
||||
this.imgList = []; // 帧图片集合
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化帧动画的所有帧
|
||||
* @param {Array} imgList - 帧图片的路径数组
|
||||
*/
|
||||
initFrames(imgList) {
|
||||
this.imgList = imgList.map((src) => {
|
||||
const img = wx.createImage();
|
||||
img.src = src;
|
||||
return img;
|
||||
});
|
||||
|
||||
this.count = imgList.length;
|
||||
|
||||
// 推入到全局动画池,便于全局绘图的时候遍历和绘制当前动画帧
|
||||
GameGlobal.databus.animations.push(this);
|
||||
}
|
||||
|
||||
// 将播放中的帧绘制到canvas上
|
||||
aniRender(ctx) {
|
||||
if (this.index >= 0 && this.index < this.count) {
|
||||
ctx.drawImage(
|
||||
this.imgList[this.index],
|
||||
this.x,
|
||||
this.y,
|
||||
this.width * 1.2,
|
||||
this.height * 1.2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 播放预定的帧动画
|
||||
playAnimation(index = 0, loop = false) {
|
||||
this.visible = false; // 动画播放时隐藏精灵图
|
||||
this.isPlaying = true;
|
||||
this.loop = loop;
|
||||
this.index = index;
|
||||
|
||||
if (this.interval > 0 && this.count) {
|
||||
this[__.timer] = setInterval(this.frameLoop.bind(this), this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止帧动画播放
|
||||
stopAnimation() {
|
||||
this.isPlaying = false;
|
||||
this.index = -1;
|
||||
if (this[__.timer]) {
|
||||
clearInterval(this[__.timer]);
|
||||
this[__.timer] = null; // 清空定时器引用
|
||||
this.emit('stopAnimation');
|
||||
}
|
||||
}
|
||||
|
||||
// 帧遍历
|
||||
frameLoop() {
|
||||
this.index++;
|
||||
|
||||
if (this.index >= this.count) {
|
||||
if (this.loop) {
|
||||
this.index = 0; // 循环播放
|
||||
} else {
|
||||
this.index = this.count - 1; // 保持在最后一帧
|
||||
this.stopAnimation(); // 停止播放
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
js/base/pool.js
Normal file
43
js/base/pool.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const __ = {
|
||||
poolDic: Symbol('poolDic'),
|
||||
};
|
||||
|
||||
/**
|
||||
* 简易的对象池实现
|
||||
* 用于对象的存贮和重复使用
|
||||
* 可以有效减少对象创建开销和避免频繁的垃圾回收
|
||||
* 提高游戏性能
|
||||
*/
|
||||
export default class Pool {
|
||||
constructor() {
|
||||
this[__.poolDic] = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对象标识符
|
||||
* 获取对应的对象池
|
||||
*/
|
||||
getPoolBySign(name) {
|
||||
return this[__.poolDic][name] || (this[__.poolDic][name] = []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据传入的对象标识符,查询对象池
|
||||
* 对象池为空创建新的类,否则从对象池中取
|
||||
*/
|
||||
getItemByClass(name, className) {
|
||||
const pool = this.getPoolBySign(name);
|
||||
|
||||
const result = pool.length ? pool.shift() : new className();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象回收到对象池
|
||||
* 方便后续继续使用
|
||||
*/
|
||||
recover(name, instance) {
|
||||
this.getPoolBySign(name).push(instance);
|
||||
}
|
||||
}
|
||||
55
js/base/sprite.js
Normal file
55
js/base/sprite.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import Emitter from '../libs/tinyemitter';
|
||||
|
||||
/**
|
||||
* 游戏基础的精灵类
|
||||
*/
|
||||
export default class Sprite extends Emitter {
|
||||
visible = true; // 是否可见
|
||||
isActive = true; // 是否可碰撞
|
||||
|
||||
constructor(imgSrc = '', width = 0, height = 0, x = 0, y = 0) {
|
||||
super();
|
||||
|
||||
this.img = wx.createImage();
|
||||
this.img.src = imgSrc;
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将精灵图绘制在canvas上
|
||||
*/
|
||||
render(ctx) {
|
||||
if (!this.visible) return;
|
||||
|
||||
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的碰撞检测定义:
|
||||
* 另一个精灵的中心点处于本精灵所在的矩形内即可
|
||||
* @param{Sprite} sp: Sptite的实例
|
||||
*/
|
||||
isCollideWith(sp) {
|
||||
const spX = sp.x + sp.width / 2;
|
||||
const spY = sp.y + sp.height / 2;
|
||||
|
||||
// 不可见则不检测
|
||||
if (!this.visible || !sp.visible) return false;
|
||||
// 不可碰撞则不检测
|
||||
if (!this.isActive || !sp.isActive) return false;
|
||||
|
||||
return !!(
|
||||
spX >= this.x &&
|
||||
spX <= this.x + this.width &&
|
||||
spY >= this.y &&
|
||||
spY <= this.y + this.height
|
||||
);
|
||||
}
|
||||
}
|
||||
64
js/databus.js
Normal file
64
js/databus.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import Pool from './base/pool';
|
||||
|
||||
let instance;
|
||||
|
||||
/**
|
||||
* 全局状态管理器
|
||||
* 负责管理游戏的状态,包括帧数、分数、子弹、敌人和动画等
|
||||
*/
|
||||
export default class DataBus {
|
||||
// 直接在类中定义实例属性
|
||||
enemys = []; // 存储敌人
|
||||
bullets = []; // 存储子弹
|
||||
animations = []; // 存储动画
|
||||
frame = 0; // 当前帧数
|
||||
score = 0; // 当前分数
|
||||
isGameOver = false; // 游戏是否结束
|
||||
pool = new Pool(); // 初始化对象池
|
||||
|
||||
constructor() {
|
||||
// 确保单例模式
|
||||
if (instance) return instance;
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
// 重置游戏状态
|
||||
reset() {
|
||||
this.frame = 0; // 当前帧数
|
||||
this.score = 0; // 当前分数
|
||||
this.bullets = []; // 存储子弹
|
||||
this.enemys = []; // 存储敌人
|
||||
this.animations = []; // 存储动画
|
||||
this.isGameOver = false; // 游戏是否结束
|
||||
}
|
||||
|
||||
// 游戏结束
|
||||
gameOver() {
|
||||
this.isGameOver = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收敌人,进入对象池
|
||||
* 此后不进入帧循环
|
||||
* @param {Object} enemy - 要回收的敌人对象
|
||||
*/
|
||||
removeEnemy(enemy) {
|
||||
const temp = this.enemys.splice(this.enemys.indexOf(enemy), 1);
|
||||
if (temp) {
|
||||
this.pool.recover('new_enemy', enemy); // 回收敌人到对象池
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收子弹,进入对象池
|
||||
* 此后不进入帧循环
|
||||
* @param {Object} bullet - 要回收的子弹对象
|
||||
*/
|
||||
removeBullets(bullet) {
|
||||
const temp = this.bullets.splice(this.bullets.indexOf(bullet), 1);
|
||||
if (temp) {
|
||||
this.pool.recover('whip_bullet', bullet); // 回收子弹到对象池
|
||||
}
|
||||
}
|
||||
}
|
||||
1
js/libs/tinyemitter.js
Normal file
1
js/libs/tinyemitter.js
Normal file
@@ -0,0 +1 @@
|
||||
(function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var n;if(typeof window!=="undefined"){n=window}else if(typeof global!=="undefined"){n=global}else if(typeof self!=="undefined"){n=self}else{n=this}n.TinyEmitter=e()}})(function(){var e,n,t;return function r(e,n,t){function i(o,u){if(!n[o]){if(!e[o]){var s=typeof require=="function"&&require;if(!u&&s)return s(o,!0);if(f)return f(o,!0);var a=new Error("Cannot find module '"+o+"'");throw a.code="MODULE_NOT_FOUND",a}var l=n[o]={exports:{}};e[o][0].call(l.exports,function(n){var t=e[o][1][n];return i(t?t:n)},l,l.exports,r,e,n,t)}return n[o].exports}var f=typeof require=="function"&&require;for(var o=0;o<t.length;o++)i(t[o]);return i}({1:[function(e,n,t){function r(){}r.prototype={on:function(e,n,t){var r=this.e||(this.e={});(r[e]||(r[e]=[])).push({fn:n,ctx:t});return this},once:function(e,n,t){var r=this;function i(){r.off(e,i);n.apply(t,arguments)}i._=n;return this.on(e,i,t)},emit:function(e){var n=[].slice.call(arguments,1);var t=((this.e||(this.e={}))[e]||[]).slice();var r=0;var i=t.length;for(r;r<i;r++){t[r].fn.apply(t[r].ctx,n)}return this},off:function(e,n){var t=this.e||(this.e={});var r=t[e];var i=[];if(r&&n){for(var f=0,o=r.length;f<o;f++){if(r[f].fn!==n&&r[f].fn._!==n)i.push(r[f])}}i.length?t[e]=i:delete t[e];return this}};n.exports=r;n.exports.TinyEmitter=r},{}]},{},[1])(1)});
|
||||
141
js/main.js
Normal file
141
js/main.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import './render'; // 初始化Canvas
|
||||
import Player from './player/index'; // 导入玩家类
|
||||
import Enemy from './npc/enemy'; // 导入敌机类
|
||||
import BackGround from './runtime/background'; // 导入背景类
|
||||
import GameInfo from './runtime/gameinfo'; // 导入游戏UI类
|
||||
import Music from './runtime/music'; // 导入音乐类
|
||||
import DataBus from './databus'; // 导入数据类,用于管理游戏状态和数据
|
||||
|
||||
const ENEMY_GENERATE_INTERVAL = 30;
|
||||
const ctx = canvas.getContext('2d'); // 获取canvas的2D绘图上下文;
|
||||
|
||||
GameGlobal.databus = new DataBus(); // 全局数据管理,用于管理游戏状态和数据
|
||||
GameGlobal.musicManager = new Music(); // 全局音乐管理实例
|
||||
|
||||
/**
|
||||
* 游戏主函数
|
||||
*/
|
||||
export default class Main {
|
||||
aniId = 0; // 用于存储动画帧的ID
|
||||
bg = new BackGround(); // 创建背景
|
||||
player = new Player(); // 创建玩家
|
||||
gameInfo = new GameInfo(); // 创建游戏UI显示
|
||||
|
||||
constructor() {
|
||||
// 当开始游戏被点击时,重新开始游戏
|
||||
this.gameInfo.on('restart', this.start.bind(this));
|
||||
|
||||
// 开始游戏
|
||||
this.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始或重启游戏
|
||||
*/
|
||||
start() {
|
||||
GameGlobal.databus.reset(); // 重置数据
|
||||
this.player.init(); // 重置玩家状态
|
||||
cancelAnimationFrame(this.aniId); // 清除上一局的动画
|
||||
this.aniId = requestAnimationFrame(this.loop.bind(this)); // 开始新的动画循环
|
||||
}
|
||||
|
||||
/**
|
||||
* 随着帧数变化的敌机生成逻辑
|
||||
* 帧数取模定义成生成的频率
|
||||
*/
|
||||
enemyGenerate() {
|
||||
// 随着分数增加,生成频率增加 (最小间隔 10 帧)
|
||||
const score = GameGlobal.databus.score;
|
||||
const interval = Math.max(10, 30 - Math.floor(score / 5));
|
||||
|
||||
if (GameGlobal.databus.frame % interval === 0) {
|
||||
const enemy = GameGlobal.databus.pool.getItemByClass('new_enemy', Enemy); // 从对象池获取敌机实例
|
||||
|
||||
// 随着分数增加,敌人速度增加
|
||||
const speed = Math.random() * 2 + 1;
|
||||
enemy.init(speed); // 初始化敌机
|
||||
|
||||
GameGlobal.databus.enemys.push(enemy); // 将敌机添加到敌机数组中
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局碰撞检测
|
||||
*/
|
||||
collisionDetection() {
|
||||
// 检测子弹与敌机的碰撞
|
||||
GameGlobal.databus.bullets.forEach((bullet) => {
|
||||
for (let i = 0, il = GameGlobal.databus.enemys.length; i < il; i++) {
|
||||
const enemy = GameGlobal.databus.enemys[i];
|
||||
|
||||
// 如果敌机存活并且发生了发生碰撞
|
||||
if (enemy.isCollideWith(bullet)) {
|
||||
enemy.destroy(); // 销毁敌机
|
||||
// bullet.destroy(); // 鞭子模式下子弹不销毁
|
||||
GameGlobal.databus.score += 1; // 增加分数
|
||||
break; // 退出循环
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 检测玩家与敌机的碰撞
|
||||
for (let i = 0, il = GameGlobal.databus.enemys.length; i < il; i++) {
|
||||
const enemy = GameGlobal.databus.enemys[i];
|
||||
|
||||
// 如果玩家与敌机发生碰撞
|
||||
if (this.player.isCollideWith(enemy)) {
|
||||
this.player.destroy(); // 销毁玩家飞机
|
||||
GameGlobal.databus.gameOver(); // 游戏结束
|
||||
|
||||
break; // 退出循环
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* canvas重绘函数
|
||||
* 每一帧重新绘制所有的需要展示的元素
|
||||
*/
|
||||
render() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布
|
||||
|
||||
this.bg.render(ctx); // 绘制背景
|
||||
this.player.render(ctx); // 绘制玩家飞机
|
||||
GameGlobal.databus.bullets.forEach((item) => item.render(ctx)); // 绘制所有子弹
|
||||
GameGlobal.databus.enemys.forEach((item) => item.render(ctx)); // 绘制所有敌机
|
||||
this.gameInfo.render(ctx); // 绘制游戏UI
|
||||
GameGlobal.databus.animations.forEach((ani) => {
|
||||
if (ani.isPlaying) {
|
||||
ani.aniRender(ctx); // 渲染动画
|
||||
}
|
||||
}); // 绘制所有动画
|
||||
}
|
||||
|
||||
// 游戏逻辑更新主函数
|
||||
update() {
|
||||
GameGlobal.databus.frame++; // 增加帧数
|
||||
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bg.update(); // 更新背景
|
||||
this.player.update(); // 更新玩家
|
||||
// 更新所有子弹
|
||||
GameGlobal.databus.bullets.forEach((item) => item.update());
|
||||
// 更新所有敌机
|
||||
GameGlobal.databus.enemys.forEach((item) => item.update());
|
||||
|
||||
this.enemyGenerate(); // 生成敌机
|
||||
this.collisionDetection(); // 检测碰撞
|
||||
}
|
||||
|
||||
// 实现游戏帧循环
|
||||
loop() {
|
||||
this.update(); // 更新游戏逻辑
|
||||
this.render(); // 渲染游戏画面
|
||||
|
||||
// 请求下一帧动画
|
||||
this.aniId = requestAnimationFrame(this.loop.bind(this));
|
||||
}
|
||||
}
|
||||
100
js/npc/enemy.js
Normal file
100
js/npc/enemy.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import Animation from '../base/animation';
|
||||
import { SCREEN_WIDTH, SCREEN_HEIGHT } from '../render';
|
||||
|
||||
const ENEMY_IMG_SRC = 'images/enemy.png';
|
||||
const ENEMY_WIDTH = 60;
|
||||
const ENEMY_HEIGHT = 60;
|
||||
const EXPLO_IMG_PREFIX = 'images/explosion';
|
||||
|
||||
export default class Enemy extends Animation {
|
||||
speed = Math.random() * 2 + 1; // 飞行速度
|
||||
|
||||
constructor() {
|
||||
super(ENEMY_IMG_SRC, ENEMY_WIDTH, ENEMY_HEIGHT);
|
||||
}
|
||||
|
||||
init(speed) {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
|
||||
// 随机选择一个边生成 (0:上, 1:右, 2:下, 3:左)
|
||||
const side = Math.floor(Math.random() * 4);
|
||||
|
||||
switch (side) {
|
||||
case 0: // Top
|
||||
this.x = Math.random() * (SCREEN_WIDTH - this.width);
|
||||
this.y = -this.height;
|
||||
break;
|
||||
case 1: // Right
|
||||
this.x = SCREEN_WIDTH;
|
||||
this.y = Math.random() * (SCREEN_HEIGHT - this.height);
|
||||
break;
|
||||
case 2: // Bottom
|
||||
this.x = Math.random() * (SCREEN_WIDTH - this.width);
|
||||
this.y = SCREEN_HEIGHT;
|
||||
break;
|
||||
case 3: // Left
|
||||
this.x = -this.width;
|
||||
this.y = Math.random() * (SCREEN_HEIGHT - this.height);
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算朝向中心的速度向量
|
||||
const targetX = SCREEN_WIDTH / 2 - this.width / 2;
|
||||
const targetY = SCREEN_HEIGHT / 2 - this.height / 2;
|
||||
|
||||
const dx = targetX - this.x;
|
||||
const dy = targetY - this.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
this.speed = speed || (Math.random() * 2 + 2); // 默认速度
|
||||
|
||||
this.vx = (dx / distance) * this.speed;
|
||||
this.vy = (dy / distance) * this.speed;
|
||||
|
||||
this.isActive = true;
|
||||
this.visible = true;
|
||||
// 设置爆炸动画
|
||||
this.initExplosionAnimation();
|
||||
}
|
||||
|
||||
// 预定义爆炸的帧动画
|
||||
initExplosionAnimation() {
|
||||
const EXPLO_FRAME_COUNT = 19;
|
||||
const frames = Array.from(
|
||||
{ length: EXPLO_FRAME_COUNT },
|
||||
(_, i) => `${EXPLO_IMG_PREFIX}${i + 1}.png`
|
||||
);
|
||||
this.initFrames(frames);
|
||||
}
|
||||
|
||||
// 每一帧更新敌人位置
|
||||
update() {
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
// 如果到达中心(或者非常接近),可以视为撞击玩家(碰撞检测会处理)
|
||||
// 这里不需要额外的边界回收逻辑,因为它们最终会撞上玩家或被消灭
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isActive = false;
|
||||
// 播放销毁动画后移除
|
||||
this.playAnimation();
|
||||
GameGlobal.musicManager.playExplosion(); // 播放爆炸音效
|
||||
wx.vibrateShort({
|
||||
type: 'light'
|
||||
}); // 轻微震动
|
||||
this.on('stopAnimation', () => this.remove.bind(this));
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.isActive = false;
|
||||
this.visible = false;
|
||||
GameGlobal.databus.removeEnemy(this);
|
||||
}
|
||||
}
|
||||
48
js/player/bullet.js
Normal file
48
js/player/bullet.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import Sprite from '../base/sprite';
|
||||
|
||||
const BULLET_IMG_SRC = 'images/bullet.png';
|
||||
const BULLET_WIDTH = 16;
|
||||
const BULLET_HEIGHT = 30;
|
||||
|
||||
export default class Bullet extends Sprite {
|
||||
constructor() {
|
||||
super(BULLET_IMG_SRC, BULLET_WIDTH, BULLET_HEIGHT);
|
||||
}
|
||||
|
||||
init(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.isActive = true;
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
setPos(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// 每一帧更新子弹位置
|
||||
update() {
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 子弹位置由外部控制(Player控制)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isActive = false;
|
||||
// 子弹没有销毁动画,直接移除
|
||||
this.remove();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.isActive = false;
|
||||
this.visible = false;
|
||||
// 回收子弹对象
|
||||
GameGlobal.databus.removeBullets(this);
|
||||
// 注意:DataBus.removeBullets 内部调用 pool.recover('bullet', bullet)
|
||||
// 我们需要修改 DataBus 或者在这里手动回收
|
||||
// 由于 DataBus 是通用的,我们最好修改 DataBus 或者在 DataBus 中传入 key
|
||||
}
|
||||
}
|
||||
161
js/player/index.js
Normal file
161
js/player/index.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import Animation from '../base/animation';
|
||||
import { SCREEN_WIDTH, SCREEN_HEIGHT } from '../render';
|
||||
import Bullet from './bullet';
|
||||
|
||||
// 玩家相关常量设置
|
||||
const PLAYER_IMG_SRC = 'images/turtle5.png';
|
||||
const PLAYER_WIDTH = 80;
|
||||
const PLAYER_HEIGHT = 80;
|
||||
const EXPLO_IMG_PREFIX = 'images/explosion';
|
||||
const PLAYER_SHOOT_INTERVAL = 20;
|
||||
|
||||
export default class Player extends Animation {
|
||||
constructor() {
|
||||
super(PLAYER_IMG_SRC, PLAYER_WIDTH, PLAYER_HEIGHT);
|
||||
|
||||
// 初始化坐标
|
||||
this.init();
|
||||
|
||||
// 初始化事件监听
|
||||
this.initEvent();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 玩家固定在屏幕中心
|
||||
this.x = SCREEN_WIDTH / 2 - this.width / 2;
|
||||
this.y = SCREEN_HEIGHT / 2 - this.height / 2;
|
||||
|
||||
// 用于在手指移动的时候标识手指是否已经在飞机上了
|
||||
this.touched = false;
|
||||
|
||||
this.isActive = true;
|
||||
this.visible = true;
|
||||
|
||||
// 设置爆炸动画
|
||||
this.initExplosionAnimation();
|
||||
|
||||
// 初始化子弹(鞭子)
|
||||
this.angle = 0;
|
||||
this.radius = PLAYER_WIDTH * 1.5; // 旋转半径
|
||||
this.bullets = [];
|
||||
|
||||
// 创建两个子弹
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const bullet = GameGlobal.databus.pool.getItemByClass('whip_bullet', Bullet);
|
||||
bullet.init(this.x, this.y);
|
||||
this.bullets.push(bullet);
|
||||
GameGlobal.databus.bullets.push(bullet);
|
||||
}
|
||||
}
|
||||
|
||||
// 预定义爆炸的帧动画
|
||||
initExplosionAnimation() {
|
||||
const EXPLO_FRAME_COUNT = 19;
|
||||
const frames = Array.from(
|
||||
{ length: EXPLO_FRAME_COUNT },
|
||||
(_, i) => `${EXPLO_IMG_PREFIX}${i + 1}.png`
|
||||
);
|
||||
this.initFrames(frames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断手指是否在飞机上
|
||||
* @param {Number} x: 手指的X轴坐标
|
||||
* @param {Number} y: 手指的Y轴坐标
|
||||
* @return {Boolean}: 用于标识手指是否在飞机上的布尔值
|
||||
*/
|
||||
checkIsFingerOnAir(x, y) {
|
||||
const deviation = 30;
|
||||
return (
|
||||
x >= this.x - deviation &&
|
||||
y >= this.y - deviation &&
|
||||
x <= this.x + this.width + deviation &&
|
||||
y <= this.y + this.height + deviation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手指的位置设置飞机的位置
|
||||
* 保证手指处于飞机中间
|
||||
* 同时限定飞机的活动范围限制在屏幕中
|
||||
*/
|
||||
setAirPosAcrossFingerPosZ(x, y) {
|
||||
const disX = Math.max(
|
||||
0,
|
||||
Math.min(x - this.width / 2, SCREEN_WIDTH - this.width)
|
||||
);
|
||||
const disY = Math.max(
|
||||
0,
|
||||
Math.min(y - this.height / 2, SCREEN_HEIGHT - this.height)
|
||||
);
|
||||
|
||||
this.x = disX;
|
||||
this.y = disY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家响应手指的触摸事件
|
||||
* 改变战机的位置
|
||||
*/
|
||||
initEvent() {
|
||||
wx.onTouchStart((e) => {
|
||||
const { clientX: x, clientY: y } = e.touches[0];
|
||||
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
return;
|
||||
}
|
||||
if (this.checkIsFingerOnAir(x, y)) {
|
||||
this.touched = true;
|
||||
this.setAirPosAcrossFingerPosZ(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
wx.onTouchMove((e) => {
|
||||
const { clientX: x, clientY: y } = e.touches[0];
|
||||
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
return;
|
||||
}
|
||||
if (this.touched) {
|
||||
this.setAirPosAcrossFingerPosZ(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
wx.onTouchEnd((e) => {
|
||||
this.touched = false;
|
||||
});
|
||||
|
||||
wx.onTouchCancel((e) => {
|
||||
this.touched = false;
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新旋转角度
|
||||
this.angle += 0.1; // 旋转速度
|
||||
|
||||
// 更新子弹位置
|
||||
this.bullets.forEach((bullet, index) => {
|
||||
// 两个子弹相差 180 度 (PI)
|
||||
const currentAngle = this.angle + index * Math.PI;
|
||||
|
||||
const bulletX = this.x + this.width / 2 + Math.cos(currentAngle) * this.radius - bullet.width / 2;
|
||||
const bulletY = this.y + this.height / 2 + Math.sin(currentAngle) * this.radius - bullet.height / 2;
|
||||
|
||||
bullet.setPos(bulletX, bulletY);
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isActive = false;
|
||||
this.playAnimation();
|
||||
GameGlobal.musicManager.playExplosion(); // 播放爆炸音效
|
||||
wx.vibrateShort({
|
||||
type: 'medium'
|
||||
}); // 震动
|
||||
}
|
||||
}
|
||||
9
js/render.js
Normal file
9
js/render.js
Normal file
@@ -0,0 +1,9 @@
|
||||
GameGlobal.canvas = wx.createCanvas();
|
||||
|
||||
const windowInfo = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync();
|
||||
|
||||
canvas.width = windowInfo.screenWidth;
|
||||
canvas.height = windowInfo.screenHeight;
|
||||
|
||||
export const SCREEN_WIDTH = windowInfo.screenWidth;
|
||||
export const SCREEN_HEIGHT = windowInfo.screenHeight;
|
||||
52
js/runtime/background.js
Normal file
52
js/runtime/background.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import Sprite from '../base/sprite';
|
||||
import { SCREEN_WIDTH, SCREEN_HEIGHT } from '../render';
|
||||
|
||||
const BACKGROUND_IMAGE_SRC = 'images/bg.jpg';
|
||||
const BACKGROUND_WIDTH = 512;
|
||||
const BACKGROUND_HEIGHT = 512;
|
||||
const BACKGROUND_SPEED = 2;
|
||||
|
||||
/**
|
||||
* 游戏背景类
|
||||
* 提供 update 和 render 函数实现无限滚动的背景功能
|
||||
*/
|
||||
export default class BackGround extends Sprite {
|
||||
constructor() {
|
||||
super(BACKGROUND_IMAGE_SRC, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
}
|
||||
|
||||
update() {
|
||||
// 背景不再滚动
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景图重绘函数
|
||||
* 绘制一张静态图片
|
||||
*/
|
||||
render(ctx) {
|
||||
ctx.drawImage(
|
||||
this.img,
|
||||
0,
|
||||
0,
|
||||
this.width,
|
||||
this.height,
|
||||
0,
|
||||
0,
|
||||
SCREEN_WIDTH,
|
||||
SCREEN_HEIGHT
|
||||
);
|
||||
|
||||
// 绘制第二张背景
|
||||
ctx.drawImage(
|
||||
this.img,
|
||||
0,
|
||||
0,
|
||||
this.width,
|
||||
this.height,
|
||||
0,
|
||||
BACKGROUND_HEIGHT,
|
||||
SCREEN_WIDTH,
|
||||
SCREEN_HEIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
111
js/runtime/gameinfo.js
Normal file
111
js/runtime/gameinfo.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import Emitter from '../libs/tinyemitter';
|
||||
import { SCREEN_WIDTH, SCREEN_HEIGHT } from '../render';
|
||||
|
||||
const atlas = wx.createImage();
|
||||
atlas.src = 'images/Common.png';
|
||||
|
||||
export default class GameInfo extends Emitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.btnArea = {
|
||||
startX: SCREEN_WIDTH / 2 - 40,
|
||||
startY: SCREEN_HEIGHT / 2 - 100 + 180,
|
||||
endX: SCREEN_WIDTH / 2 + 50,
|
||||
endY: SCREEN_HEIGHT / 2 - 100 + 255,
|
||||
};
|
||||
|
||||
// 绑定触摸事件
|
||||
wx.onTouchStart(this.touchEventHandler.bind(this))
|
||||
}
|
||||
|
||||
setFont(ctx) {
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '20px Arial';
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
this.renderGameScore(ctx, GameGlobal.databus.score); // 绘制当前分数
|
||||
|
||||
// 游戏结束时停止帧循环并显示游戏结束画面
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
this.renderGameOver(ctx, GameGlobal.databus.score); // 绘制游戏结束画面
|
||||
}
|
||||
}
|
||||
|
||||
renderGameScore(ctx, score) {
|
||||
this.setFont(ctx);
|
||||
ctx.fillText(score, 10, 30);
|
||||
}
|
||||
|
||||
renderGameOver(ctx, score) {
|
||||
this.drawGameOverImage(ctx);
|
||||
this.drawGameOverText(ctx, score);
|
||||
this.drawRestartButton(ctx);
|
||||
}
|
||||
|
||||
drawGameOverImage(ctx) {
|
||||
ctx.drawImage(
|
||||
atlas,
|
||||
0,
|
||||
0,
|
||||
119,
|
||||
108,
|
||||
SCREEN_WIDTH / 2 - 150,
|
||||
SCREEN_HEIGHT / 2 - 100,
|
||||
300,
|
||||
300
|
||||
);
|
||||
}
|
||||
|
||||
drawGameOverText(ctx, score) {
|
||||
this.setFont(ctx);
|
||||
ctx.fillText(
|
||||
'游戏结束',
|
||||
SCREEN_WIDTH / 2 - 40,
|
||||
SCREEN_HEIGHT / 2 - 100 + 50
|
||||
);
|
||||
ctx.fillText(
|
||||
`得分: ${score}`,
|
||||
SCREEN_WIDTH / 2 - 40,
|
||||
SCREEN_HEIGHT / 2 - 100 + 130
|
||||
);
|
||||
}
|
||||
|
||||
drawRestartButton(ctx) {
|
||||
ctx.drawImage(
|
||||
atlas,
|
||||
120,
|
||||
6,
|
||||
39,
|
||||
24,
|
||||
SCREEN_WIDTH / 2 - 60,
|
||||
SCREEN_HEIGHT / 2 - 100 + 180,
|
||||
120,
|
||||
40
|
||||
);
|
||||
ctx.fillText(
|
||||
'重新开始',
|
||||
SCREEN_WIDTH / 2 - 40,
|
||||
SCREEN_HEIGHT / 2 - 100 + 205
|
||||
);
|
||||
}
|
||||
|
||||
touchEventHandler(event) {
|
||||
const { clientX, clientY } = event.touches[0]; // 获取触摸点的坐标
|
||||
|
||||
// 当前只有游戏结束时展示了UI,所以只处理游戏结束时的状态
|
||||
if (GameGlobal.databus.isGameOver) {
|
||||
// 检查触摸是否在按钮区域内
|
||||
if (
|
||||
clientX >= this.btnArea.startX &&
|
||||
clientX <= this.btnArea.endX &&
|
||||
clientY >= this.btnArea.startY &&
|
||||
clientY <= this.btnArea.endY
|
||||
) {
|
||||
// 调用重启游戏的回调函数
|
||||
this.emit('restart');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
js/runtime/music.js
Normal file
32
js/runtime/music.js
Normal file
@@ -0,0 +1,32 @@
|
||||
let instance;
|
||||
|
||||
/**
|
||||
* 统一的音效管理器
|
||||
*/
|
||||
export default class Music {
|
||||
bgmAudio = wx.createInnerAudioContext();
|
||||
shootAudio = wx.createInnerAudioContext();
|
||||
boomAudio = wx.createInnerAudioContext();
|
||||
|
||||
constructor() {
|
||||
if (instance) return instance;
|
||||
|
||||
instance = this;
|
||||
|
||||
this.bgmAudio.loop = true; // 背景音乐循环播放
|
||||
this.bgmAudio.autoplay = true; // 背景音乐自动播放
|
||||
this.bgmAudio.src = 'audio/bgm.mp3';
|
||||
this.shootAudio.src = 'audio/bullet.mp3';
|
||||
this.boomAudio.src = 'audio/boom.mp3';
|
||||
}
|
||||
|
||||
playShoot() {
|
||||
this.shootAudio.currentTime = 0;
|
||||
this.shootAudio.play();
|
||||
}
|
||||
|
||||
playExplosion() {
|
||||
this.boomAudio.currentTime = 0;
|
||||
this.boomAudio.play();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user