Initial commit
31
.eslintrc.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Eslint config file
|
||||||
|
* Documentation: https://eslint.org/docs/user-guide/configuring/
|
||||||
|
* Install the Eslint extension before using this feature.
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
ecmaFeatures: {
|
||||||
|
modules: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
wx: true,
|
||||||
|
App: true,
|
||||||
|
Page: true,
|
||||||
|
getCurrentPages: true,
|
||||||
|
getApp: true,
|
||||||
|
Component: true,
|
||||||
|
requirePlugin: true,
|
||||||
|
requireMiniProgram: true,
|
||||||
|
},
|
||||||
|
// extends: 'eslint:recommended',
|
||||||
|
rules: {},
|
||||||
|
}
|
||||||
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node Modules
|
||||||
|
node_modules/
|
||||||
|
miniprogram_npm/
|
||||||
|
|
||||||
|
# WeChat Mini Program/Game
|
||||||
|
project.private.config.json
|
||||||
|
project.local.config.json
|
||||||
|
/unpackage/
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
34
README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 示例游戏
|
||||||
|
|
||||||
|
示例相关说明查阅[新手教程](https://developers.weixin.qq.com/minigame/dev/guide/develop/start.html)
|
||||||
|
|
||||||
|
## 源码目录介绍
|
||||||
|
|
||||||
|
```
|
||||||
|
├── audio // 音频资源
|
||||||
|
├── images // 图片资源
|
||||||
|
├── js
|
||||||
|
│ ├── base
|
||||||
|
│ │ ├── animatoin.js // 帧动画的简易实现
|
||||||
|
│ │ ├── pool.js // 对象池的简易实现
|
||||||
|
│ │ └── sprite.js // 游戏基本元素精灵类
|
||||||
|
│ ├── libs
|
||||||
|
│ │ └── tinyemitter.js // 事件监听和触发
|
||||||
|
│ ├── npc
|
||||||
|
│ │ └── enemy.js // 敌机类
|
||||||
|
│ ├── player
|
||||||
|
│ │ ├── bullet.js // 子弹类
|
||||||
|
│ │ └── index.js // 玩家类
|
||||||
|
│ ├── runtime
|
||||||
|
│ │ ├── background.js // 背景类
|
||||||
|
│ │ ├── gameinfo.js // 用于展示分数和结算界面
|
||||||
|
│ │ └── music.js // 全局音效管理器
|
||||||
|
│ ├── databus.js // 管控游戏状态
|
||||||
|
│ ├── main.js // 游戏入口主函数
|
||||||
|
│ └── render.js // 基础渲染信息
|
||||||
|
├── .eslintrc.js // 代码规范
|
||||||
|
├── game.js // 游戏逻辑主入口
|
||||||
|
├── game.json // 游戏运行时配置
|
||||||
|
├── project.config.json // 项目配置
|
||||||
|
└── project.private.config.json // 项目个人配置
|
||||||
|
```
|
||||||
BIN
audio/bgm.mp3
Normal file
BIN
audio/boom.mp3
Normal file
BIN
audio/bullet.mp3
Normal file
BIN
images/Common.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
images/bg.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
images/bullet.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
images/enemy.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
images/explosion1.png
Normal file
|
After Width: | Height: | Size: 525 B |
BIN
images/explosion10.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
images/explosion11.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
images/explosion12.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
images/explosion13.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
images/explosion14.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
images/explosion15.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
images/explosion16.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
images/explosion17.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
images/explosion18.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
images/explosion19.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
images/explosion2.png
Normal file
|
After Width: | Height: | Size: 1017 B |
BIN
images/explosion3.png
Normal file
|
After Width: | Height: | Size: 995 B |
BIN
images/explosion4.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
images/explosion5.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/explosion6.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/explosion7.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
images/explosion8.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
images/explosion9.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
images/hero.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
images/turtle.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
images/turtle2.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
images/turtle3.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
images/turtle5.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
project.config.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"description": "项目配置文件",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": false,
|
||||||
|
"es6": true,
|
||||||
|
"postcss": true,
|
||||||
|
"minified": true,
|
||||||
|
"newFeature": true,
|
||||||
|
"compileWorklet": false,
|
||||||
|
"uglifyFileName": false,
|
||||||
|
"uploadWithSourceMap": true,
|
||||||
|
"enhance": false,
|
||||||
|
"packNpmManually": false,
|
||||||
|
"packNpmRelationList": [],
|
||||||
|
"minifyWXSS": true,
|
||||||
|
"minifyWXML": true,
|
||||||
|
"localPlugins": false,
|
||||||
|
"condition": false,
|
||||||
|
"swc": false,
|
||||||
|
"disableSWC": true,
|
||||||
|
"babelSetting": {
|
||||||
|
"ignore": [],
|
||||||
|
"disablePlugins": [],
|
||||||
|
"outputPath": ""
|
||||||
|
},
|
||||||
|
"disableUseStrict": false,
|
||||||
|
"useCompilerPlugins": false
|
||||||
|
},
|
||||||
|
"compileType": "game",
|
||||||
|
"libVersion": "latest",
|
||||||
|
"appid": "wxbbe1578da04083a1",
|
||||||
|
"projectname": "quickstart",
|
||||||
|
"condition": {
|
||||||
|
"search": {
|
||||||
|
"current": -1,
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"current": -1,
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"currentL": -1,
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"miniprogram": {
|
||||||
|
"current": -1,
|
||||||
|
"list": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simulatorPluginLibVersion": {},
|
||||||
|
"packOptions": {
|
||||||
|
"ignore": [],
|
||||||
|
"include": []
|
||||||
|
},
|
||||||
|
"isGameTourist": false,
|
||||||
|
"editorSetting": {}
|
||||||
|
}
|
||||||