lover-fish
  • Introduction
  • 搭建环境
  • 绘制背景
  • 绘制海葵
  • 绘制果实
  • 果实动画
  • 控制果实的数量
  • 增加果实的类型
  • 绘制鱼
  • 让鱼动起来
  • 让鱼跟着鼠标动起来
  • 吃果实之碰撞检测
  • 创建鱼宝宝
  • 给鱼添加动画
  • 加入计分模块
  • 大鱼身体特效
  • 游戏结束逻辑
  • 增加吃果实特效
  • 海葵动画
  • 漂浮物
Powered by GitBook
On this page

给鱼添加动画

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

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

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

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

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

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

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

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

import { ctx_one, cvs_height, cvs_width, mouse_x, mouse_y, fish_mother } from "./init";
import { deltaTime } from "./game-loop";

import utils from "./utils";


// 鱼妈妈
class FishMother{
  x: number = cvs_width / 2; // 坐标轴 x
  y: number = cvs_height / 2 ; // 坐标轴 y
  bigEye : Array<HTMLImageElement> = []; // 眼睛
  bigBody : Array<HTMLImageElement> = []; // 身体
  BigTail : Array<HTMLImageElement> = []; // 尾巴
  angle: number = 0; // 鱼的角度
  EyeIndex = 0; // 需要渲染哪个眼睛的索引值
  BodyIndex = 0;
  TailIndex = 0;
  EyeTimer = 0; // 计算眼睛时间
  BodyTimer = 0;
  TailTimer = 0;
  EyeInterval = 300; // 通过这个变量动态的设置眨眼睛的间隔
  BodyInterval = 300;
  TailInterval = 50;
  constructor(){

    for (let i = 0; i < 2; ++i) {
      this.bigEye[i] = new Image();
      this.bigEye[i].src = `assets/img/bigEye${i}.png`;
    }
    for (let i = 0; i < 8; ++i) {
      this.bigBody[i] = new Image();
      this.bigBody[i].src = `assets/img/bigSwim${i}.png`;
    }

    for (let i = 0; i < 8; ++i) {
      this.BigTail[i] = new Image();
      this.BigTail[i].src = `assets/img/bigTail${i}.png`;
    }
  }

  checkImageIndex(){
    this.EyeTimer += deltaTime;
    this.TailTimer += deltaTime;
    this.BodyTimer += deltaTime;

    // 当计时器大于某个值的时候才进行修改
    if(this.EyeTimer > this.EyeInterval) {
      this.EyeIndex = (this.EyeIndex + 1) % 2;

      // 重置一下定时器
      this.EyeTimer %= this.EyeInterval;

      // 判断是眨眼的哪个过程
      if(this.EyeIndex === 0) {
        this.EyeInterval = Math.random() * 1500 + 2000 // 设置下一次眨眼的间隔长一点
      }else{
        // 先闭眼后睁眼,这个过程应该非常短
        this.EyeInterval = 300;
      }
    }

    if(this.TailTimer > this.TailInterval) {
      this.TailIndex = (this.TailIndex + 1) % 8;
      this.TailTimer %= this.TailInterval

    }

    if(this.BodyTimer > this.BodyInterval) {
      this.BodyIndex = this.BodyIndex + 1;
      this.BodyTimer %= this.BodyInterval
      if(this.BodyIndex > 7) {
        this.BodyIndex = 7
      }
    }
  }

  checkMove(){
    this.x = utils.lerpDistance(mouse_x, this.x , .95)
    this.y = utils.lerpDistance(mouse_y, this.y , .95)


    let instance_X = mouse_x - this.x; // 边 a
    let instance_Y = mouse_y - this.y; // 边 b

    let ag = Math.atan2(instance_Y, instance_X) + Math.PI // [-PI, PI]

    this.angle = utils.lerpAngle(ag, this.angle, .9)
  }

  draw(){

    this.checkMove()
    this.checkImageIndex()

    ctx_one.save();
    ctx_one.translate(this.x, this.y); // 定义相对定位的坐标中心点
    ctx_one.rotate(this.angle);
    ctx_one.scale(.8, .8);
    ctx_one.drawImage(this.BigTail[this.TailIndex], -this.BigTail[this.TailIndex].width / 2 + 30, -this.BigTail[this.TailIndex].height / 2); // 这里的尾巴,往右移动30像素,让它在身体的后面。
    ctx_one.drawImage(this.bigBody[this.BodyIndex], -this.bigBody[this.BodyIndex].width / 2, -this.bigBody[this.BodyIndex].height / 2);
    ctx_one.drawImage(this.bigEye[this.EyeIndex], -this.bigEye[this.EyeIndex].width / 2, -this.bigEye[this.EyeIndex].height / 2); // 居中,所以向左移动宽度的一半,向上移动宽度的一半

    ctx_one.restore();

  }
}

// 鱼宝宝
class FishBaby extends FishMother {
  x: number = cvs_width / 2 + 50; // 坐标轴 x
  y: number = cvs_height / 2 + 50; // 坐标轴 y
  constructor() {
    super()

    for (let i = 0; i < 2; ++i) {
      this.bigEye[i] = new Image();
      this.bigEye[i].src = `assets/img/babyEye${i}.png`;
    }
    for (let i = 0; i < 20; ++i) {
      this.bigBody[i] = new Image();
      this.bigBody[i].src = `assets/img/babyFade${i}.png`;
    }

    for (let i = 0; i < 8; ++i) {
      this.BigTail[i] = new Image();
      this.BigTail[i].src = `assets/img/babyTail${i}.png`;
    }
  }


  checkImageIndex(){
    this.EyeTimer += deltaTime;
    this.TailTimer += deltaTime;
    this.BodyTimer += deltaTime;

    // 当计时器大于某个值的时候才进行修改
    if(this.EyeTimer > this.EyeInterval) {
      this.EyeIndex = (this.EyeIndex + 1) % 2;

      // 重置一下定时器
      this.EyeTimer %= this.EyeInterval;

      // 判断是眨眼的哪个过程
      if(this.EyeIndex === 0) {
        this.EyeInterval = Math.random() * 1500 + 2000 // 设置下一次眨眼的间隔长一点
      }else{
        // 先闭眼后睁眼,这个过程应该非常短
        this.EyeInterval = 300;
      }
    }

    if(this.TailTimer > this.TailInterval) {
      this.TailIndex = (this.TailIndex + 1) % 8;
      this.TailTimer %= this.TailInterval

    }

    if(this.BodyTimer > this.BodyInterval) {
      this.BodyIndex = this.BodyIndex + 1;
      this.BodyTimer %= this.BodyInterval
      if(this.BodyIndex > 19) {
        this.BodyIndex = 19
        console.log('game over');
      }
    }
  }

  // 重置身体的图片,也就是得到能量满血复活
  recover(){
    this.BodyIndex = 0;
  }

  checkMove(){
    this.x = utils.lerpDistance(fish_mother.x, this.x , .98)
    this.y = utils.lerpDistance(fish_mother.y, this.y , .98)

    let instance_X = fish_mother.x - this.x; // 边 a
    let instance_Y = fish_mother.y - this.y; // 边 b

    let ag = Math.atan2(instance_Y, instance_X) + Math.PI // [-PI, PI]

    this.angle = utils.lerpAngle(ag, this.angle, .7)
  }

  draw(){
    this.checkMove()
    this.checkImageIndex()

    ctx_one.save();
    ctx_one.translate(this.x, this.y); // 定义相对定位的坐标中心点
    ctx_one.rotate(this.angle);
    ctx_one.scale(.8, .8);
    ctx_one.drawImage(this.BigTail[this.TailIndex], -this.BigTail[this.TailIndex].width / 2 + 24, -this.BigTail[this.TailIndex].height / 2); // 这里的尾巴,往右移动30像素,让它在身体的后面。
    ctx_one.drawImage(this.bigBody[this.BodyIndex], -this.bigBody[this.BodyIndex].width / 2, -this.bigBody[this.BodyIndex].height / 2);
    ctx_one.drawImage(this.bigEye[this.EyeIndex], -this.bigEye[this.EyeIndex].width / 2, -this.bigEye[this.EyeIndex].height / 2); // 居中,所以向左移动宽度的一半,向上移动宽度的一半
    ctx_one.restore();
  }
}

export {
  FishMother,
  FishBaby
}

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

import { bgPic, cvs_width , cvs_height, ctx_two, ctx_one, anemones, fruits, fish_mother, fish_baby } from "./init";
import utils from "./utils";


let lastTime: number = Date.now(), // 记录上一次绘制的时间
    deltaTime: number = 0; // requestAnimationFrame 执行完成所用的时间 = 当前时间 - 上一次绘制的世界

/**
 * 鱼妈妈与果实的碰撞检测
 */
function fishAndFruitsCollision() {
  for (let i = fruits.num; i >= 0; i--) {
    // 假如或者就计算鱼儿与果实的距离
    if(fruits.alive[i]) {
      // 得到距离的平方根
      const distance = utils.getDistance(
        {x: fruits.x[i], y: fruits.y[i]},
        {x: fish_mother.x, y: fish_mother.y}
      );

      // 假如距离小于 500 让它死亡
      if(distance < 500) {
        fruits.dead(i)
      }
    }
  }
}

/**
 * 鱼妈妈与鱼宝宝的碰撞检测
 */
function fishMotherAndBabyCollision() {

    // 得到距离的平方根
    const distance = utils.getDistance(
      {x: fish_baby.x, y: fish_baby.y},
      {x: fish_mother.x, y: fish_mother.y}
    );

    // 假如距离小于 900 就喂食给 baby
    if(distance < 900) {
      fish_baby.recover();
    }

}

function gameLoop() {
  const now = Date.now()
  deltaTime = now - lastTime;
  lastTime = now;

  //  给 deltaTime 设置上线
  if(deltaTime > 40) deltaTime = 40;

  // console.log(deltaTime);

  drawBackbround()  // 画背景图片

  anemones.draw()  // 海葵绘制
  fruits.draw()  // 果实绘制
  fruits.monitor() // 监视果实,让死去的果实得到新生

  ctx_one.clearRect(0, 0, cvs_width, cvs_width); // 清除掉所有,再进行绘制,要不然的话会多次绘制而进行重叠。
  fish_mother.draw() // 绘制鱼妈妈
  fish_baby.draw() // 绘制鱼宝宝
  fishAndFruitsCollision() // 每一帧都进行碰撞检测
  fishMotherAndBabyCollision()
  requestAnimationFrame(gameLoop); // 不断的循环 gameLoop,且流畅性提升
}


function drawBackbround() {
  ctx_two.drawImage(bgPic, 0, 0, cvs_width, cvs_height)
}

export { deltaTime }

export default gameLoop;
Previous创建鱼宝宝Next加入计分模块

Last updated 7 years ago