cocos基于Graphics(Canvas)实现简单粒子系统,绘制辉光背景、五芒星以及粒子聚拢等互动效果

起因:自带的粒子系统貌似不能直接控制粒子
因为是游戏的背景,为了提高交互性都增加了互动功能

辉光背景

思路

  1. 粒子系统负责生成粒子,一种特效对应一种粒子,和现有粒子系统通过调参实现整体控制是不同的思路,优点是更简单

粒子系统类

import { IParticleOptions } from '../common/state'
import Particle from './Particle'
import { getRandomNumber } from '../utils/tool'
import { LightParticle } from './LightParticle'

// 粒子系统
export default class ParticleMgr extends Component {
  particles: Particle[] = []
  graphics: Graphics
  timer: number = 0

  private particle: { new (): Particle } // 改为接收一个构造函数
  private options: IParticleOptions = {}

  init(particle, options: IParticleOptions = {}) {
    this.particle = particle
    this.options = options

    this.graphics = this.node.addComponent(Graphics)

    // 两种类型,一种持续生成,一种一次性生成, 没有间隔就是一次性
    if (!this.options.gap) {
      this.generateParticles()
    }

    // 存在大小范围
    if (this.options.maxRange && this.options.minRange) {
      this.options.max = getRandomNumber(this.options.minRange, this.options.maxRange)
    }

    // 有生命周期就在生命结束时清除
    if (this.options.duration) {
      this.scheduleOnce(() => {
        this.clear()
      }, this.options.duration)
    }
  }

  update(dt: number) {
    // 清空画布
    this.graphics.clear()

    // 持续生成
    if (this.options.gap) {
      if (this.timer > this.options.gap) {
        this.addParticles()
        this.timer = 0
      } else {
        this.timer += dt
      }
    }

    this.updateParticles(dt)
  }
  generateParticles() {
    for (let i = 0; i < this.options.max; i++) {
      this.particles.push(new this.particle())
    }
  }
  addParticles() {
    if (this.options.max && this.options.max > this.particles.length) {
      // 清除超出上限的粒子
      this.particles.length = this.options.max
    }
    this.particles.push(new this.particle())
  }
  updateParticles(dt) {
    this.particles.forEach((particle) => {
      particle.update(dt)
      particle.draw(this.graphics)
    })
    // 删除销毁的粒子
    this.particles = this.particles.filter((particle) => !particle.markedForDeletion)
  }

  clear() {
    this.particles.forEach((particle) => (particle.markedForDeletion = true))
  }

  // 粒子聚拢效果
  gather(point: Vec2) {
    const gatherRadius = 200 // 定义聚拢半径
    const gatherSpeed = 100 // 定义聚拢速度

    // 找到触点周围的粒子
    this.particles.forEach((particle) => {
      // 计算粒子与触点的距离
      const distance = Vec2.distance(new Vec2(particle.x, particle.y), point)
      if (distance < gatherRadius) {
        ;(particle as LightParticle).isGather = true
        // 计算方向向量并归一化
        let direction = new Vec2()
        Vec2.subtract(direction, point, new Vec2(particle.x, particle.y))
        direction = direction.normalize()

        // 改变粒子速度,使其朝向触点
        const diffSpeedX =
          (Math.abs(Vec2.distance(new Vec2(particle.x, 0), new Vec2(point.x, 0))) / gatherRadius) * gatherSpeed
        const diffSpeedY =
          (Math.abs(Vec2.distance(new Vec2(0, particle.y), new Vec2(0, point.y))) / gatherRadius) * gatherSpeed
        particle.speedX = direction.clone().multiplyScalar(diffSpeedX).x
        particle.speedY = direction.clone().multiplyScalar(diffSpeedY).y
      }
    })
  }
  offGather() {
    this.particles.forEach((particle) => {
      ;(particle as LightParticle).isGather = false
      // 重置y轴速度
      particle.speedY = Math.random() * 20 + 40
    })
  }
}

粒子基类

继承component是为了使用定时器,不知道会不会因此加重性能损耗

export default class Particle extends Component {
  markedForDeletion: boolean = false
  protected size: number = 0
  speedX: number = 0
  speedY: number = 0
  x: number = 0
  y: number = 0
  protected color: Color = null

  constructor() {
    super()
  }

  update(dt) {
    this.move(dt)
    this.destroyed()
  }
  move(dt) {
    // 粒子的移动
    this.x += this.speedX * dt
    this.y += this.speedY * dt
  }
  draw(graphics: Graphics) {}
  destroyed() {
    // 粒子不断变小
    this.size *= 0.95
    if (this.size < 0.5) this.markedForDeletion = true //基于大小的清除
  }
}

辉光粒子

// 光辉背景
export class LightParticle extends Particle {
  color2: Color
  alpha: number
  flickerDuration: number
  flickerTimer: number
  flickerSpeed: number
  flickerTween: Tween<unknown>
  flickerTween2: Tween<unknown>
  angle: number
  va: number
  curve: number
  borthX: number

  isGather: boolean = false
  constructor() {
    super()
    this.size = getRandomNumber(3, 5)
    // 粒子在宽度上散布
    this.borthX = getRandomNumber(0, mapW)
    this.x = this.borthX
    this.y = this.y

    this.speedY = Math.random() * 20 + 40 //40-60  --这是向上的

    this.color = new Color(255, 250, 101, 200) // 中金色
    // 背景光环
    this.color2 = new Color(255, 247, 153, 100) // 淡金色

    // 创建一个循环的 tween 来改变 alpha 值,包含闪烁完成后的延时
    const delay = Math.random() * 0.5 + 1
    this.flickerTween = tween(this.color)
      .to(0.5, { a: 0 }, { easing: 'sineInOut' })
      .to(0.5, { a: 200 }, { easing: 'sineInOut' })
      .delay(delay) // 闪烁完成后,延时1秒
      // 确保to按顺序执行
      .union()
      .repeatForever()
      .start()
    this.flickerTween2 = tween(this.color2)
      .to(0.5, { a: 0 }, { easing: 'sineInOut' })
      .to(0.5, { a: 100 }, { easing: 'sineInOut' })
      .delay(delay) // 闪烁完成后,延时1秒
      // 确保to按顺序执行
      .union()
      .repeatForever()
      .start()
      
		const swayAmount = 100 // 摆动幅度,根据实际情况调整
      this.speedX = (Math.random() - 0.5) * swayAmount
      // 无规律移动
      this.schedule(() => {
        // 是否收到牵引决定移动方式
        if (this.isGather) return
        tween(this)
          .to(1, { speedX: (Math.random() - 0.5) * swayAmount })
          .start()
      }, 1)
  }
  update(dt) {
    super.update(dt)
  }
  move(dt) {
    // 粒子的移动
    this.x += this.speedX * dt
    this.y += this.speedY * dt
  }
  draw(graphics: Graphics) {
    graphics.fillColor = this.color2
    graphics.rect(this.x + 2, this.y + 2, this.size, this.size)
    graphics.fill()
    graphics.fillColor = this.color
    graphics.rect(this.x, this.y, this.size, this.size)
    graphics.fill()
  }
  destroyed() {
    if (this.y > mapH) {
      this.markedForDeletion = true
      this.flickerTween.stop() // 在对象被销毁时,停止并清理 tween
      this.flickerTween2.stop() // 在对象被销毁时,停止并清理 tween
    }
  }
}

具体使用

在场景管理器类中使用即可

onLoad() {
    this.particleMgr = this.canvas.addComponent(ParticleMgr)
    this.particleMgr.init(LightParticle, {
      gap: 0.5,
      // max: 1,
    })

    // this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this)
    this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this)
    this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this)
  }
  onDestroy() {
    this.particleMgr.clear()
    // this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this)
    this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this)
    this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this)
  }

  onTouchMove(event: EventTouch) {
    // 辉光聚拢效果
    const touch = event.touch
    this.particleMgr.gather(touch.getLocation())
  }
  onTouchEnd() {
    this.particleMgr.offGather()
  }

五芒星

用canvas和用图片的区别就是可以有个绘制的过程动画

@ccclass('FiveStarBg')
export class FiveStarBg extends Component {
  graphics: Graphics
  graphics2: Graphics

  lastPoint: Vec2
  childNode: Node

  private tw: Tween<unknown>
  start() {
    this.graphics = this.node.getComponent(Graphics)
    this.drawMagicaCircl()

    this.lastPoint = null // 上一个触摸点
    // 用新节点监听触摸绘画效果
    this.childNode = new Node('ChildNode')
    const tran = this.childNode.addComponent(UITransform)
    tran.setContentSize(this.node.getComponent(UITransform).contentSize)
    tran.setAnchorPoint(0, 0)
    this.childNode.setPosition(-tran.contentSize.width / 2, -tran.contentSize.height / 2) //锚点改了,位置也要改
    this.graphics2 = this.childNode.addComponent(Graphics)
    this.node.addChild(this.childNode)

    this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this)
    this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this)
    this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this)
  }
  onDestroy() {
    this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this)
    this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this)
    this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this)
  }
  
  onTouchStart(event: EventTouch) {
    let touch = event.touch
    this.lastPoint = touch.getLocation()

    this.graphics2.lineCap = Graphics.LineCap.ROUND
    this.graphics2.lineJoin = Graphics.LineJoin.ROUND
    this.graphics2.strokeColor = Color.WHITE // 线条颜色
    this.graphics2.lineWidth = 10

    this.graphics2.moveTo(this.lastPoint.x, this.lastPoint.y)

    // 计算速度或距离,动态调整线宽
    // this.tw = tween(this.graphics2).to(0.3, { lineWidth: 6 }).start()
  }

  onTouchMove(event: EventTouch) {
    let touch = event.touch
    let currentPoint = touch.getLocation()

    this.graphics2.moveTo(this.lastPoint.x, this.lastPoint.y)
    this.graphics2.lineTo(currentPoint.x, currentPoint.y)
    this.graphics2.stroke()

    // 更新上一个触摸点
    this.lastPoint = currentPoint
  }

  onTouchEnd(event: EventTouch) {
    this.lastPoint = null
    // this.offEvent()

    // 防抖
    this.unschedule(this.clearCanvas)
    this.scheduleOnce(this.clearCanvas, 1)
  }
  clearCanvas() {
    this.graphics2.clear()
  }

  drawMagicaCircl() {
    let graphics = this.graphics
    // 五芒星的五个顶点
    let radius = 200 // 增大五芒星半径

    // 绘制外圈圆
    graphics.lineWidth = 10 // 线宽
    graphics.strokeColor = Color.WHITE // 线条颜色
    let outerCircleRadius = radius * 1.1 // 外圈圆半径稍大于五芒星半径
    // graphics.circle(0, 0, outerCircleRadius)
    // graphics.stroke()
    let from = { angle: 0 }
    let to = { angle: 2 * Math.PI }
    tween(from)
      .to(0.5, to, {
        onUpdate: () => {
          // 更新角度
          // graphics.clear() // 清除之前的绘制
          graphics.arc(0, 0, outerCircleRadius, Math.PI / 2, from.angle + Math.PI / 2, true) // 使用from对象的角度
          graphics.stroke()
        },
      })
      .start()

    // 初始化五芒星的顶点数组
    let points = []
    let angle = 90 // 开始角度设置为90度,这样第一个点是朝上的

    // 计算五个顶点的坐标
    for (let i = 0; i < 5; i++) {
      // let angle = (i * 72 * Math.PI) / 180 - Math.PI / 2
      let rad = (Math.PI / 180) * (angle + i * 72) // 将角度转换为弧度
      points.push({
        x: radius * Math.cos(rad),
        y: radius * Math.sin(rad),
      })
    }

    // 绘制五芒星
    graphics.lineWidth = 10 // 线宽
    graphics.strokeColor = Color.WHITE // 线条颜色

    const connectOrder = [0, 2, 4, 1, 3, 0] // 修正后的正确连接顺序
    // 定义一个状态对象来记录绘制进度
    let drawState = { value: 0 }
    // 使用tween逐步改变drawState.value,从0改变到5(因为有5个顶点)
    tween(drawState)
      .delay(0.5)
      .to(
        2,
        { value: 5 },
        {
          onUpdate: () => {
            graphics.lineCap = Graphics.LineCap.ROUND // 设置线条端点样式为圆形
            // 绘制五芒星的黑色轮廓
            graphics.lineWidth = 20 // 线宽
            graphics.strokeColor = Color.BLACK // 线条颜色

            // 根据当前的绘制状态绘制线条
            const currentPoint = Math.floor(drawState.value)

            graphics.moveTo(points[0].x, points[0].y) // 移动到起始顶点

            for (let i = 1; i <= currentPoint && i < connectOrder.length; i++) {
              let pointIndex = connectOrder[i]
              graphics.lineTo(points[pointIndex].x, points[pointIndex].y)
            }

            // 如果不是整数,则绘制一部分的线条
            if (drawState.value % 1 !== 0 && currentPoint < connectOrder.length - 1) {
              let nextPointIndex = connectOrder[currentPoint + 1]
              let lastPointIndex = connectOrder[currentPoint]
              // 计算两点之间的插值
              let partialX =
                points[lastPointIndex].x + (points[nextPointIndex].x - points[lastPointIndex].x) * (drawState.value % 1)
              let partialY =
                points[lastPointIndex].y + (points[nextPointIndex].y - points[lastPointIndex].y) * (drawState.value % 1)
              // 绘制到当前的位置
              graphics.lineTo(partialX, partialY)
            }

            graphics.stroke() // 完成本次绘制

            // 绘制白五芒星
            graphics.lineWidth = 10 // 线宽
            graphics.strokeColor = Color.WHITE // 线条颜色

            graphics.moveTo(points[0].x, points[0].y) // 移动到起始顶点

            // 根据当前的绘制状态绘制线条
            for (let i = 1; i <= currentPoint && i < connectOrder.length; i++) {
              let pointIndex = connectOrder[i]
              graphics.lineTo(points[pointIndex].x, points[pointIndex].y)
            }

            // 如果不是整数,则绘制一部分的线条
            if (drawState.value % 1 !== 0 && currentPoint < connectOrder.length - 1) {
              let nextPointIndex = connectOrder[currentPoint + 1]
              let lastPointIndex = connectOrder[currentPoint]
              // 计算两点之间的插值
              let partialX =
                points[lastPointIndex].x + (points[nextPointIndex].x - points[lastPointIndex].x) * (drawState.value % 1)
              let partialY =
                points[lastPointIndex].y + (points[nextPointIndex].y - points[lastPointIndex].y) * (drawState.value % 1)
              // 绘制到当前的位置
              graphics.lineTo(partialX, partialY)
            }

            graphics.stroke() // 完成本次绘制
          },
          easing: 'linear',
        },
      )
      .start() // 开始过渡动画

    // for (let i = 1; i < connectOrder.length; i++) {
    //   let pointIndex = connectOrder[i]
    //   graphics.lineTo(points[pointIndex].x, points[pointIndex].y)
    // }
    // graphics.stroke()
  }

  update(deltaTime: number) {}
}

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值