game.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. import * as PIXI from "pixi.js";
  2. import "pixi-plugin-bump";
  3. const { Application, Sprite, Container } = PIXI;
  4. const { TextureCache } = PIXI.utils;
  5. import { loaderRes } from "./util";
  6. import createGroundData from "./groudData";
  7. const GAME_RESULT_STATE = {
  8. ON: "游戏开始",
  9. OVER: "游戏失败",
  10. SUCC: "通关完成"
  11. };
  12. const ROLESTATE = {
  13. READY: "READY",
  14. RUN: "RUN",
  15. JUMP: "JUMP",
  16. DOWN: "DOWN"
  17. };
  18. class Game {
  19. constructor(wrapper) {
  20. this.wrapper = wrapper; //外层element
  21. this.stageWidth = 750;
  22. this.stageHeight = 1334;
  23. this.app = null;
  24. this.bump = null; //碰撞检测实例
  25. this.gameState = GAME_RESULT_STATE.ON; //游戏状态
  26. this._roleState = ROLESTATE.READY; //人物状态
  27. this.roleReadyStep = 0; //人物准备状态的当前帧数
  28. this.roleReadyTimer = null; //人物准备动画的计时器
  29. this.roleRunStep = 0; //人物跑动状态的当前帧数
  30. this.roleRunTimer = null; // 人物跑动动画的计时器
  31. this.fallingDown = false; //下坠状态
  32. //地面
  33. this.groundData = null; //地面构造数据
  34. this.groundArr = []; //地面元素数组
  35. this.totalWidth = 0; //总宽度,主要用于布局
  36. //倒计时
  37. this.uiCountdownTimes = 3; //倒计时次数 3 2 1 开始
  38. this.uiCountdownTimer = null; //倒计时计时器
  39. //ui
  40. this.uiRole = null; //人物sprite
  41. this.uiuiGroundContainer = null; //地面总容器
  42. this.uiGameTopTip = null; //游戏顶部提示
  43. this.uiCountdown = null; //游戏开始倒计时
  44. this.uiFireworksContanier = null; //烟花容器
  45. this.fireworkOpts = {
  46. green: {
  47. delay: 0,
  48. pos: {
  49. x: 588,
  50. y: 230
  51. }
  52. },
  53. orange: {
  54. delay: 300,
  55. pos: {
  56. x: 290,
  57. y: 375
  58. }
  59. },
  60. purple: {
  61. delay: 500,
  62. pos: {
  63. x: 400,
  64. y: 200
  65. }
  66. },
  67. red: {
  68. delay: 600,
  69. pos: {
  70. x: 450,
  71. y: 370
  72. }
  73. }
  74. };
  75. this.decibelArr = []; //声贝队列
  76. this.decibel = 0; //声贝峰值
  77. this.init();
  78. this.initProxy();
  79. }
  80. //底部小游戏
  81. init() {
  82. this.app = new Application({
  83. width: this.stageWidth,
  84. height: this.stageHeight,
  85. transparent: true
  86. });
  87. this.wrapper.appendChild(this.app.view);
  88. this.bump = new PIXI.extras.Bump();
  89. loaderRes(["/scene.json"], this.loadedSource, this);
  90. }
  91. //roleState代理
  92. initProxy() {
  93. Object.defineProperty(this, "roleState", {
  94. configurable: true,
  95. enumerable: true,
  96. get: () => {
  97. return this._roleState;
  98. },
  99. set: value => {
  100. if (this._roleState === value) return;
  101. this._roleState = value;
  102. this.clearRoleStateEffect();
  103. switch (value) {
  104. case ROLESTATE.READY:
  105. this.roleReadyStep = 0;
  106. this.roleReadyLoop();
  107. break;
  108. case ROLESTATE.RUN:
  109. this.roleRunStep = 0;
  110. this.roleRunLoop();
  111. break;
  112. case ROLESTATE.JUMP:
  113. this.uiRole.texture = TextureCache[`p_jump_0.png`];
  114. break;
  115. case ROLESTATE.DOWN:
  116. this.uiRole.texture = TextureCache[`p_jump_1.png`];
  117. break;
  118. }
  119. }
  120. });
  121. }
  122. clearRoleStateEffect() {
  123. clearTimeout(this.roleReadyTimer);
  124. clearTimeout(this.roleRunTimer);
  125. }
  126. roleReadyLoop() {
  127. this.roleReadyTimer = setTimeout(() => {
  128. this.uiRole.texture =
  129. TextureCache[`p_stand_${this.roleReadyStep % 2}.png`];
  130. this.roleReadyStep++;
  131. this.roleReadyLoop();
  132. }, 500);
  133. }
  134. roleRunLoop() {
  135. this.roleRunTimer = setTimeout(() => {
  136. this.uiRole.texture =
  137. TextureCache[`p_run_${this.roleRunStep % 2}.png`];
  138. this.roleRunStep++;
  139. this.roleRunLoop();
  140. }, 100);
  141. }
  142. loadedSource() {
  143. this.initGameReady();
  144. this.initGround();
  145. this.initRole();
  146. this.initFireworks();
  147. this.readyStart();
  148. }
  149. //开始倒计时
  150. initGameReady() {
  151. this.uiGameTopTip = Sprite.from("word_tips.png");
  152. this.uiGameTopTip.anchor.x = 0.5;
  153. this.uiGameTopTip.x = 375;
  154. this.uiGameTopTip.y = 20;
  155. this.app.stage.addChild(this.uiGameTopTip);
  156. this.uiCountdown = Sprite.from("word_3.png");
  157. this.uiCountdown.anchor.x = 0.5;
  158. this.uiCountdown.anchor.y = 0.5;
  159. this.uiCountdown.x = 375;
  160. this.uiCountdown.y = 400;
  161. this.app.stage.addChild(this.uiCountdown);
  162. }
  163. // 创建地面容器
  164. initGround() {
  165. this.groundData = createGroundData();
  166. this.groundArr = [];
  167. this.totalWidth = 0;
  168. this.uiGroundContainer = new Container();
  169. this.uiGroundContainer._vx = 2.5;
  170. this.app.stage.addChild(this.uiGroundContainer);
  171. this.groundData.forEach(this.createGround.bind(this));
  172. this.totalWidth = this.uiGroundContainer.width;
  173. }
  174. //生成地面迭代器函数
  175. createGround(groundData, index, arr) {
  176. let { height, blockNum, gap } = groundData;
  177. this.totalWidth += gap;
  178. const groundUnitContainer = new Container();
  179. groundData.x = groundUnitContainer.x = this.totalWidth;
  180. groundData.y = groundUnitContainer.y = this.stageHeight - height + 24; //草地高度除外
  181. groundData.width = blockNum * 100;
  182. groundUnitContainer.cacheAsBitmap = true;
  183. let totalX = 0;
  184. for (let i = 0; i < blockNum; i++) {
  185. const ground = Sprite.fromFrame(
  186. `bg_${i === 0 ? "l" : i === blockNum - 1 ? "r" : "m"}.png`
  187. );
  188. ground.x = totalX;
  189. ground.y = -24; //负值,刚好贴着地面。方便碰撞检测
  190. groundUnitContainer.addChild(ground);
  191. totalX += ground.width;
  192. }
  193. this.totalWidth += groundData.width;
  194. this.groundArr.push(groundUnitContainer);
  195. this.uiGroundContainer.addChild(groundUnitContainer);
  196. //最后一个,需要添加通关门UI
  197. if (index === arr.length - 1) {
  198. let uiCastle = Sprite.from("castle.png");
  199. uiCastle.anchor.x = 0.7;
  200. uiCastle.anchor.y = 1;
  201. uiCastle.x = totalX;
  202. groundUnitContainer.addChild(uiCastle);
  203. }
  204. }
  205. //创建角色
  206. initRole() {
  207. this.uiRole = Sprite.fromFrame("people.png");
  208. this.uiRole.x = this.stageWidth / 2;
  209. this.uiRole.y = this.stageHeight - this.groundData[0].height + 24; //初始化位置
  210. this.uiRole._vy = 400 / 60;
  211. this.uiRole.anchor.x = 0.5;
  212. this.uiRole.anchor.y = 1;
  213. this.app.stage.addChild(this.uiRole);
  214. this.roleState = ROLESTATE.READY;
  215. }
  216. //生成烟花
  217. initFireworks() {
  218. this.uiFireworksContanier = new Container();
  219. Object.keys(this.fireworkOpts).forEach(key => {
  220. let firework = this.fireworkOpts[key];
  221. firework.frameFirework = 0; //下标
  222. firework.timer = null; //计时器
  223. let uiFirework = Sprite.from(
  224. `fireworks_${key}_${firework.frameFirework}.png`
  225. ); //ui
  226. uiFirework.anchor.x = 0.5;
  227. uiFirework.anchor.y = 0.5;
  228. uiFirework.x = firework.pos.x;
  229. uiFirework.y = firework.pos.y;
  230. firework.uiFirework = uiFirework;
  231. this.uiFireworksContanier.addChild(uiFirework);
  232. });
  233. this.uiFireworksContanier.visible = false;
  234. this.app.stage.addChild(this.uiFireworksContanier);
  235. }
  236. //开始倒计时
  237. readyStart() {
  238. this.uiCountdownTimes = 3;
  239. this.uiCountdown.texture = TextureCache[`word_3.png`];
  240. this.uiCountdown.visible = true;
  241. this.countDownLoop();
  242. }
  243. countDownLoop() {
  244. this.uiCountdownTimer = setTimeout(() => {
  245. this.uiCountdownTimes--;
  246. if (this.uiCountdownTimes === 0) {
  247. this.uiCountdown.texture = TextureCache[`word_start.png`];
  248. this.gameStart();
  249. } else if (this.uiCountdownTimes < 0) {
  250. clearTimeout(this.uiCountdownTimer);
  251. this.uiCountdown.visible = false;
  252. } else {
  253. this.uiCountdown.texture =
  254. TextureCache[`word_${this.uiCountdownTimes}.png`];
  255. }
  256. this.countDownLoop();
  257. }, 1000);
  258. }
  259. gameStart() {
  260. this.roleState = ROLESTATE.RUN;
  261. this.groundWalk();
  262. }
  263. restartGame() {
  264. console.log("restartGame");
  265. this.gameState = GAME_RESULT_STATE.ON;
  266. this.uiGroundContainer.x = 0;
  267. this.uiRole.x = 375;
  268. this.uiRole.y = this.groundData[0].y;
  269. this.gameResultContainer.visible = false;
  270. this.decibel = 0;
  271. this.roleState = ROLESTATE.RUN;
  272. this.app.ticker.start();
  273. }
  274. //每帧多要去判断人物状态
  275. walkLoop() {
  276. this.uiGroundContainer.x -= this.uiGroundContainer._vx;
  277. let rolePosX = Math.abs(this.uiGroundContainer.x - 375); //人物所在位置
  278. let rolePosY = this.uiRole.y;
  279. if (rolePosX >= this.totalWidth - 375) {
  280. console.log("完成游戏");
  281. this.gameSucc();
  282. return;
  283. } else if (rolePosY >= this.stageHeight) {
  284. console.log("掉坑里挂了");
  285. this.gameOver();
  286. return;
  287. }
  288. //寻找人物是否在地面上方。如果不在,就寻找下一个可能会碰撞的地面
  289. let index = this.groundData.findIndex(
  290. ground =>
  291. rolePosX >= ground.x && rolePosX <= ground.x + ground.width
  292. );
  293. if (index === -1) {
  294. /**
  295. * 掉坑里了
  296. * 如果不是跳起状态,默认下坠
  297. * */
  298. console.log("在坑里的范围内");
  299. if ((this.roleState !== ROLESTATE.JUMP && this.roleState !== ROLESTATE.DOWN) && !this.fallingDown) {
  300. this.fallDown();
  301. }
  302. //寻找下一个地面作为碰撞检测
  303. index = this.groundData.findIndex(ground => rolePosX <= ground.x);
  304. }
  305. if (index >= 0) {
  306. /**
  307. * 人物下方的方块下标
  308. * 判断是否和方块有碰撞
  309. */
  310. let ground = this.groundArr[index];
  311. if (ground.y <= rolePosY) {
  312. let collision = this.bump.hit(
  313. this.uiRole,
  314. ground,
  315. true,
  316. true,
  317. true
  318. );
  319. console.log("collision", collision);
  320. if (collision) {
  321. switch (collision) {
  322. case "bottom":
  323. console.log("站上了");
  324. this.standGround();
  325. break;
  326. default:
  327. console.log("撞上了");
  328. this.gameOver();
  329. break;
  330. }
  331. }
  332. }
  333. }
  334. }
  335. //站在地面上
  336. standGround() {
  337. this.decibel = 0;
  338. this.decibelArr = [];
  339. this.roleState = ROLESTATE.RUN;
  340. this.fallingDown = false;
  341. this.app.ticker.remove(this.downLoop, this);
  342. }
  343. //最后即将通关时候,人物开始走动,而不是背景移动。
  344. roleWalk() {
  345. this.app.ticker.add(this.roleWalkLoop, this);
  346. }
  347. //人物停止走动
  348. roleStop() {
  349. this.app.ticker.remove(this.roleWalkLoop, this);
  350. }
  351. //人物走动帧函数
  352. roleWalkLoop() {
  353. this.uiRole.x += 2.5;
  354. if (this.uiRole.x >= 750 + this.uiRole.width / 2) {
  355. console.log("人物通关");
  356. this.roleStop();
  357. this.startFireworks();
  358. }
  359. }
  360. //背景移动,人物不动。
  361. groundWalk() {
  362. this.app.ticker.add(this.walkLoop, this);
  363. }
  364. //背景停止移动
  365. groundStop() {
  366. this.app.ticker.remove(this.walkLoop, this);
  367. }
  368. /**
  369. *
  370. * @param {number} decibel 声贝差值
  371. * 取声贝峰值算法
  372. * 1.如果声贝大于设置值,一旦呈下降趋势,则去当前峰值。
  373. * 2.如果声贝一直增长,取第30帧的值。
  374. */
  375. jump(decibel) {
  376. if (this.gameState === GAME_RESULT_STATE.OVER) return;
  377. if (
  378. this.roleState === ROLESTATE.JUMP ||
  379. this.roleState === ROLESTATE.DOWN ||
  380. this.roleState === ROLESTATE.READY
  381. ) {
  382. return;
  383. }
  384. console.log(this.decibelArr);
  385. if (this.decibelArr.length >= 1) {
  386. if (this.decibelArr.length >= 30) {
  387. this.decibel = decibel * 10;
  388. this.startJump();
  389. } else {
  390. let isAscend = this.decibelArr.every(item => decibel >= item);
  391. if (isAscend) {
  392. this.decibelArr.push(decibel);
  393. } else {
  394. this.decibel = this.decibelArr.pop() * 10;
  395. this.startJump();
  396. }
  397. }
  398. } else {
  399. this.decibelArr.push(decibel);
  400. }
  401. }
  402. startJump() {
  403. console.log("startJump");
  404. this.app.ticker.add(this.jumpLoop, this);
  405. this.fallingDown = false;
  406. this.app.ticker.remove(this.fallDownLoop, this);
  407. this.app.ticker.remove(this.downLoop, this);
  408. }
  409. jumpLoop() {
  410. if (this.decibel > 0) {
  411. let distance = Math.min(this.uiRole._vy, this.decibel);
  412. this.uiRole.y -= distance;
  413. this.decibel -= distance;
  414. console.log("distance,decibel", distance, this.decibel);
  415. this.roleState = ROLESTATE.JUMP;
  416. if (this.decibel === 0) {
  417. this.app.ticker.remove(this.jumpLoop, this);
  418. this.app.ticker.add(this.downLoop, this);
  419. }
  420. }
  421. }
  422. downLoop() {
  423. this.roleState = ROLESTATE.DOWN;
  424. this.uiRole.y += this.uiRole._vy;
  425. }
  426. fallDownLoop() {
  427. this.uiRole.y += this.uiRole._vy;
  428. }
  429. fallDown() {
  430. this.fallingDown = true;
  431. this.roleState = ROLESTATE.DOWN;
  432. this.app.ticker.add(this.fallDownLoop, this);
  433. }
  434. startFireworks() {
  435. this.uiFireworksContanier.visible = true;
  436. Object.keys(this.fireworkOpts).forEach(key => {
  437. let firework = this.fireworkOpts[key];
  438. firework.frameFirework = 0;
  439. clearInterval(firework.timer);
  440. setTimeout(() => {
  441. firework.timer = setInterval(() => {
  442. firework.uiFirework.visible = firework.frameFirework <= 6;
  443. if (firework.frameFirework <= 6) {
  444. firework.uiFirework.texture =
  445. TextureCache[
  446. `fireworks_${key}_${firework.frameFirework}.png`
  447. ];
  448. }
  449. firework.frameFirework = (firework.frameFirework + 1) % 12;
  450. }, 100);
  451. }, firework.delay);
  452. });
  453. }
  454. gameOver() {
  455. this.roleState = ROLESTATE.READY;
  456. this.fallingDown = false;
  457. this.app.ticker.remove(this.fallDownLoop, this);
  458. this.gameState = GAME_RESULT_STATE.OVER;
  459. this.app.ticker.stop();
  460. }
  461. gameSucc() {
  462. this.groundStop();
  463. this.roleWalk();
  464. }
  465. }
  466. export default Game;