起因:自带的粒子系统貌似不能直接控制粒子
因为是游戏的背景,为了提高交互性都增加了互动功能
辉光背景
思路
- 粒子系统负责生成粒子,一种特效对应一种粒子,和现有粒子系统通过调参实现整体控制是不同的思路,优点是更简单
粒子系统类
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) {}
}