这里的素材更新了一下 点击下载

    动画的原理非常简单,就是在一定的间隔里面切换下一章图片而已。

    只要我们把鱼的身体部位的图片序列放到一个数组里面,我们通过一些变量控制它循环起来就是成了动画。

    当然这里我们需要一个定时器,和具体显示哪一张的索引,因为定时器需要时间,所以这里需要从 game-loop 拿到 deltaTime 变量。

    为了避免一些代码重复我们写了 checkImageIndex 用来检测,具体渲染序列里面的哪张图片 和 checkMove 移动函数,其实还有不少需要优化的,暂时咱先这样。

    游戏逻辑就是,大鱼吃果实,吃了果实就有能量,然后喂给小鱼,当一定时间里面,小鱼没有得到能量就会游戏失败。

    小鱼在没有得到喂食的时候,身体会慢慢变透明。大鱼本身是透明的,当吃了果实之后就会变得有颜色。

    修改我们的 fish.ts,取余的作用是为了让它一直在某个区间里面。

    1. import { ctx_one, cvs_height, cvs_width, mouse_x, mouse_y, fish_mother } from "./init";
    2. import { deltaTime } from "./game-loop";
    3. import utils from "./utils";
    4. // 鱼妈妈
    5. class FishMother{
    6. x: number = cvs_width / 2; // 坐标轴 x
    7. y: number = cvs_height / 2 ; // 坐标轴 y
    8. bigEye : Array<HTMLImageElement> = []; // 眼睛
    9. bigBody : Array<HTMLImageElement> = []; // 身体
    10. BigTail : Array<HTMLImageElement> = []; // 尾巴
    11. angle: number = 0; // 鱼的角度
    12. EyeIndex = 0; // 需要渲染哪个眼睛的索引值
    13. BodyIndex = 0;
    14. TailIndex = 0;
    15. EyeTimer = 0; // 计算眼睛时间
    16. BodyTimer = 0;
    17. TailTimer = 0;
    18. EyeInterval = 300; // 通过这个变量动态的设置眨眼睛的间隔
    19. BodyInterval = 300;
    20. TailInterval = 50;
    21. constructor(){
    22. for (let i = 0; i < 2; ++i) {
    23. this.bigEye[i] = new Image();
    24. this.bigEye[i].src = `assets/img/bigEye${i}.png`;
    25. }
    26. for (let i = 0; i < 8; ++i) {
    27. this.bigBody[i] = new Image();
    28. this.bigBody[i].src = `assets/img/bigSwim${i}.png`;
    29. }
    30. for (let i = 0; i < 8; ++i) {
    31. this.BigTail[i] = new Image();
    32. this.BigTail[i].src = `assets/img/bigTail${i}.png`;
    33. }
    34. }
    35. checkImageIndex(){
    36. this.EyeTimer += deltaTime;
    37. this.TailTimer += deltaTime;
    38. this.BodyTimer += deltaTime;
    39. // 当计时器大于某个值的时候才进行修改
    40. if(this.EyeTimer > this.EyeInterval) {
    41. this.EyeIndex = (this.EyeIndex + 1) % 2;
    42. // 重置一下定时器
    43. this.EyeTimer %= this.EyeInterval;
    44. // 判断是眨眼的哪个过程
    45. if(this.EyeIndex === 0) {
    46. this.EyeInterval = Math.random() * 1500 + 2000 // 设置下一次眨眼的间隔长一点
    47. }else{
    48. // 先闭眼后睁眼,这个过程应该非常短
    49. this.EyeInterval = 300;
    50. }
    51. }
    52. if(this.TailTimer > this.TailInterval) {
    53. this.TailIndex = (this.TailIndex + 1) % 8;
    54. this.TailTimer %= this.TailInterval
    55. }
    56. if(this.BodyTimer > this.BodyInterval) {
    57. this.BodyIndex = this.BodyIndex + 1;
    58. this.BodyTimer %= this.BodyInterval
    59. if(this.BodyIndex > 7) {
    60. this.BodyIndex = 7
    61. }
    62. }
    63. }
    64. checkMove(){
    65. this.x = utils.lerpDistance(mouse_x, this.x , .95)
    66. this.y = utils.lerpDistance(mouse_y, this.y , .95)
    67. let instance_X = mouse_x - this.x; // 边 a
    68. let instance_Y = mouse_y - this.y; // 边 b
    69. let ag = Math.atan2(instance_Y, instance_X) + Math.PI // [-PI, PI]
    70. this.angle = utils.lerpAngle(ag, this.angle, .9)
    71. }
    72. draw(){
    73. this.checkMove()
    74. this.checkImageIndex()
    75. ctx_one.save();
    76. ctx_one.translate(this.x, this.y); // 定义相对定位的坐标中心点
    77. ctx_one.rotate(this.angle);
    78. ctx_one.scale(.8, .8);
    79. ctx_one.drawImage(this.BigTail[this.TailIndex], -this.BigTail[this.TailIndex].width / 2 + 30, -this.BigTail[this.TailIndex].height / 2); // 这里的尾巴,往右移动30像素,让它在身体的后面。
    80. ctx_one.drawImage(this.bigBody[this.BodyIndex], -this.bigBody[this.BodyIndex].width / 2, -this.bigBody[this.BodyIndex].height / 2);
    81. ctx_one.drawImage(this.bigEye[this.EyeIndex], -this.bigEye[this.EyeIndex].width / 2, -this.bigEye[this.EyeIndex].height / 2); // 居中,所以向左移动宽度的一半,向上移动宽度的一半
    82. ctx_one.restore();
    83. }
    84. }
    85. // 鱼宝宝
    86. class FishBaby extends FishMother {
    87. x: number = cvs_width / 2 + 50; // 坐标轴 x
    88. y: number = cvs_height / 2 + 50; // 坐标轴 y
    89. constructor() {
    90. super()
    91. for (let i = 0; i < 2; ++i) {
    92. this.bigEye[i] = new Image();
    93. this.bigEye[i].src = `assets/img/babyEye${i}.png`;
    94. }
    95. for (let i = 0; i < 20; ++i) {
    96. this.bigBody[i] = new Image();
    97. this.bigBody[i].src = `assets/img/babyFade${i}.png`;
    98. }
    99. for (let i = 0; i < 8; ++i) {
    100. this.BigTail[i] = new Image();
    101. this.BigTail[i].src = `assets/img/babyTail${i}.png`;
    102. }
    103. }
    104. checkImageIndex(){
    105. this.EyeTimer += deltaTime;
    106. this.TailTimer += deltaTime;
    107. this.BodyTimer += deltaTime;
    108. // 当计时器大于某个值的时候才进行修改
    109. if(this.EyeTimer > this.EyeInterval) {
    110. this.EyeIndex = (this.EyeIndex + 1) % 2;
    111. // 重置一下定时器
    112. this.EyeTimer %= this.EyeInterval;
    113. // 判断是眨眼的哪个过程
    114. if(this.EyeIndex === 0) {
    115. this.EyeInterval = Math.random() * 1500 + 2000 // 设置下一次眨眼的间隔长一点
    116. }else{
    117. // 先闭眼后睁眼,这个过程应该非常短
    118. this.EyeInterval = 300;
    119. }
    120. }
    121. if(this.TailTimer > this.TailInterval) {
    122. this.TailIndex = (this.TailIndex + 1) % 8;
    123. this.TailTimer %= this.TailInterval
    124. }
    125. if(this.BodyTimer > this.BodyInterval) {
    126. this.BodyIndex = this.BodyIndex + 1;
    127. this.BodyTimer %= this.BodyInterval
    128. if(this.BodyIndex > 19) {
    129. this.BodyIndex = 19
    130. console.log('game over');
    131. }
    132. }
    133. }
    134. // 重置身体的图片,也就是得到能量满血复活
    135. recover(){
    136. this.BodyIndex = 0;
    137. }
    138. checkMove(){
    139. this.x = utils.lerpDistance(fish_mother.x, this.x , .98)
    140. this.y = utils.lerpDistance(fish_mother.y, this.y , .98)
    141. let instance_X = fish_mother.x - this.x; // 边 a
    142. let instance_Y = fish_mother.y - this.y; // 边 b
    143. let ag = Math.atan2(instance_Y, instance_X) + Math.PI // [-PI, PI]
    144. this.angle = utils.lerpAngle(ag, this.angle, .7)
    145. }
    146. draw(){
    147. this.checkMove()
    148. this.checkImageIndex()
    149. ctx_one.save();
    150. ctx_one.translate(this.x, this.y); // 定义相对定位的坐标中心点
    151. ctx_one.rotate(this.angle);
    152. ctx_one.scale(.8, .8);
    153. ctx_one.drawImage(this.BigTail[this.TailIndex], -this.BigTail[this.TailIndex].width / 2 + 24, -this.BigTail[this.TailIndex].height / 2); // 这里的尾巴,往右移动30像素,让它在身体的后面。
    154. ctx_one.drawImage(this.bigBody[this.BodyIndex], -this.bigBody[this.BodyIndex].width / 2, -this.bigBody[this.BodyIndex].height / 2);
    155. ctx_one.drawImage(this.bigEye[this.EyeIndex], -this.bigEye[this.EyeIndex].width / 2, -this.bigEye[this.EyeIndex].height / 2); // 居中,所以向左移动宽度的一半,向上移动宽度的一半
    156. ctx_one.restore();
    157. }
    158. }
    159. export {
    160. FishMother,
    161. FishBaby
    162. }

    然后我们再添加一下小鱼与大鱼碰撞的逻辑,在我们的 game-loop.ts 里面

    1. import { bgPic, cvs_width , cvs_height, ctx_two, ctx_one, anemones, fruits, fish_mother, fish_baby } from "./init";
    2. import utils from "./utils";
    3. let lastTime: number = Date.now(), // 记录上一次绘制的时间
    4. deltaTime: number = 0; // requestAnimationFrame 执行完成所用的时间 = 当前时间 - 上一次绘制的世界
    5. /**
    6. * 鱼妈妈与果实的碰撞检测
    7. */
    8. function fishAndFruitsCollision() {
    9. for (let i = fruits.num; i >= 0; i--) {
    10. // 假如或者就计算鱼儿与果实的距离
    11. if(fruits.alive[i]) {
    12. // 得到距离的平方根
    13. const distance = utils.getDistance(
    14. {x: fruits.x[i], y: fruits.y[i]},
    15. {x: fish_mother.x, y: fish_mother.y}
    16. );
    17. // 假如距离小于 500 让它死亡
    18. if(distance < 500) {
    19. fruits.dead(i)
    20. }
    21. }
    22. }
    23. }
    24. /**
    25. * 鱼妈妈与鱼宝宝的碰撞检测
    26. */
    27. function fishMotherAndBabyCollision() {
    28. // 得到距离的平方根
    29. const distance = utils.getDistance(
    30. {x: fish_baby.x, y: fish_baby.y},
    31. {x: fish_mother.x, y: fish_mother.y}
    32. );
    33. // 假如距离小于 900 就喂食给 baby
    34. if(distance < 900) {
    35. fish_baby.recover();
    36. }
    37. }
    38. function gameLoop() {
    39. const now = Date.now()
    40. deltaTime = now - lastTime;
    41. lastTime = now;
    42. // 给 deltaTime 设置上线
    43. if(deltaTime > 40) deltaTime = 40;
    44. // console.log(deltaTime);
    45. drawBackbround() // 画背景图片
    46. anemones.draw() // 海葵绘制
    47. fruits.draw() // 果实绘制
    48. fruits.monitor() // 监视果实,让死去的果实得到新生
    49. ctx_one.clearRect(0, 0, cvs_width, cvs_width); // 清除掉所有,再进行绘制,要不然的话会多次绘制而进行重叠。
    50. fish_mother.draw() // 绘制鱼妈妈
    51. fish_baby.draw() // 绘制鱼宝宝
    52. fishAndFruitsCollision() // 每一帧都进行碰撞检测
    53. fishMotherAndBabyCollision()
    54. requestAnimationFrame(gameLoop); // 不断的循环 gameLoop,且流畅性提升
    55. }
    56. function drawBackbround() {
    57. ctx_two.drawImage(bgPic, 0, 0, cvs_width, cvs_height)
    58. }
    59. export { deltaTime }
    60. export default gameLoop;