mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
Refactor and unify particle system
This commit is contained in:
parent
b248c4beeb
commit
c510cbb834
@ -1,9 +1,7 @@
|
||||
package ocelot.desktop.node
|
||||
|
||||
import ocelot.desktop.OcelotDesktop
|
||||
import ocelot.desktop.{Settings => DesktopSettings}
|
||||
import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings => DesktopSettings}
|
||||
import ocelot.desktop.audio._
|
||||
import ocelot.desktop.geometry.Vector2D
|
||||
import ocelot.desktop.graphics.{Graphics, IconSource}
|
||||
import ocelot.desktop.inventory.SyncedInventory
|
||||
import ocelot.desktop.node.ComputerAwareNode._
|
||||
@ -11,14 +9,13 @@ import ocelot.desktop.node.Node.Size
|
||||
import ocelot.desktop.ui.UiHandler
|
||||
import ocelot.desktop.ui.event.BrainEvent
|
||||
import ocelot.desktop.ui.event.handlers.DiskActivityHandler
|
||||
import ocelot.desktop.ui.widget.ComputerErrorMessageLabel
|
||||
import ocelot.desktop.ui.particle.Particle
|
||||
import ocelot.desktop.util.Messages
|
||||
import totoro.ocelot.brain.Settings
|
||||
import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware}
|
||||
import totoro.ocelot.brain.event._
|
||||
|
||||
import java.util.Calendar
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceAware)
|
||||
extends EntityNode(entity)
|
||||
@ -27,13 +24,6 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
|
||||
with OcelotLogParticleNode
|
||||
with ShiftClickNode {
|
||||
|
||||
// access should be synchronized because messages are added in the update thread
|
||||
private val messages = ArrayBuffer.empty[(Float, ComputerErrorMessageLabel)]
|
||||
|
||||
private def addErrorMessage(message: ComputerErrorMessageLabel): Unit = messages.synchronized {
|
||||
messages += ((0f, message))
|
||||
}
|
||||
|
||||
private lazy val soundCardSounds: (SoundStream, SoundSource) = Audio.newStream(SoundCategory.Records)
|
||||
private def soundCardStream: SoundStream = soundCardSounds._1
|
||||
private def soundCardSource: SoundSource = soundCardSounds._2
|
||||
@ -44,13 +34,11 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
|
||||
case Some(message) =>
|
||||
logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message code ${event.message}: $message")
|
||||
message
|
||||
|
||||
case None =>
|
||||
logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message: ${event.message}")
|
||||
event.message
|
||||
}
|
||||
|
||||
addErrorMessage(new ComputerErrorMessageLabel(this, message))
|
||||
UiHandler.root.workspaceView.particleSystem.add(new ErrorMessageParticle(message))
|
||||
|
||||
case BrainEvent(event: BeepEvent) if !Audio.isDisabled =>
|
||||
BeepGenerator.newBeep(".", event.frequency, event.duration).play()
|
||||
@ -70,28 +58,6 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
|
||||
})
|
||||
}
|
||||
|
||||
override def update(): Unit = {
|
||||
messages.synchronized {
|
||||
messages.mapInPlace { case (t, message) => (t + ErrorMessageMoveSpeed * UiHandler.dt, message) }
|
||||
messages.filterInPlace(_._1 <= 1f)
|
||||
}
|
||||
|
||||
super.update()
|
||||
}
|
||||
|
||||
private def drawMessageParticles(g: Graphics): Unit = messages.synchronized {
|
||||
for ((time, message) <- messages.reverseIterator) {
|
||||
message.position = message.initialPosition + Vector2D(0, -MaxErrorMessageDistance * time)
|
||||
message.alpha = 1 - time
|
||||
message.draw(g)
|
||||
}
|
||||
}
|
||||
|
||||
override def drawParticles(g: Graphics): Unit = {
|
||||
super.drawParticles(g)
|
||||
drawMessageParticles(g)
|
||||
}
|
||||
|
||||
protected def drawOverlay(g: Graphics): Unit = HolidayIcon match {
|
||||
case Some(icon) if DesktopSettings.get.enableFestiveDecorations =>
|
||||
val holidayOverlaySize = Size * 1.6f
|
||||
@ -110,9 +76,22 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
|
||||
|
||||
override def draw(g: Graphics): Unit = {
|
||||
super.draw(g)
|
||||
|
||||
drawOverlay(g)
|
||||
}
|
||||
|
||||
private class ErrorMessageParticle(message: String) extends Particle {
|
||||
private val offsetX = size.width / 2 - message.length * 4
|
||||
private val offsetY = -8
|
||||
override def update(dt: Float): Unit = {
|
||||
time += ErrorMessageMoveSpeed * dt
|
||||
}
|
||||
override def draw(g: Graphics): Unit = {
|
||||
g.setSmallFont()
|
||||
g.foreground = ColorScheme("ErrorMessage").withAlpha(1 - (2 * time - 1).min(1).max(0))
|
||||
g.text(position.x + offsetX, position.y + offsetY - MaxErrorMessageDistance * time, message)
|
||||
g.setNormalFont()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ComputerAwareNode {
|
||||
|
||||
@ -297,8 +297,6 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers
|
||||
drawPortLegend(g)
|
||||
}
|
||||
|
||||
def drawParticles(g: Graphics): Unit = {}
|
||||
|
||||
def drawPorts(g: Graphics): Unit = {
|
||||
for ((port, rects) <- portsBounds) {
|
||||
val color = port.getColor
|
||||
|
||||
@ -8,62 +8,41 @@ import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.node.OcelotLogParticleNode._
|
||||
import ocelot.desktop.ui.UiHandler
|
||||
import ocelot.desktop.ui.event.BrainEvent
|
||||
import ocelot.desktop.ui.particle.Particle
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.util.Random
|
||||
|
||||
trait OcelotLogParticleNode extends Node {
|
||||
private case class LogParticle(
|
||||
var time: Float = -LogParticleGrow,
|
||||
angle: Float = Random.between(0f, 2 * math.Pi.toFloat * LogParticleMaxAngle),
|
||||
)
|
||||
|
||||
// access should be synchronized because log particles are added in the update thread
|
||||
private val logParticles = mutable.ArrayDeque.empty[LogParticle]
|
||||
|
||||
private def addLogParticle(): Unit = logParticles.synchronized {
|
||||
if (logParticles.length < MaxLogParticles) {
|
||||
logParticles += LogParticle()
|
||||
}
|
||||
}
|
||||
|
||||
eventHandlers += {
|
||||
case BrainEvent(OcelotInterface.LogEvent.CardToUser(_, _)) =>
|
||||
addLogParticle()
|
||||
}
|
||||
|
||||
override def update(): Unit = {
|
||||
super.update()
|
||||
|
||||
logParticles.synchronized {
|
||||
logParticles.foreach(particle => particle.time += LogParticleMoveSpeed * UiHandler.dt)
|
||||
logParticles.filterInPlace(_.time <= 1f)
|
||||
val system = UiHandler.root.workspaceView.particleSystem
|
||||
if (system.count[LogParticle](Some(this)) < MaxLogParticles) {
|
||||
system.add(new LogParticle)
|
||||
}
|
||||
}
|
||||
|
||||
private def drawLogParticles(g: Graphics): Unit = logParticles.synchronized {
|
||||
for (particle <- logParticles) {
|
||||
val size = (1 + particle.time / LogParticleGrow).clamp() * LogParticleSize
|
||||
val offset = particle.time.clamp() * LogParticleMoveDistance
|
||||
val alpha = 1 - particle.time.clamp()
|
||||
private class LogParticle extends Particle(-LogParticleGrow, origin = Some(this)) {
|
||||
private val angle: Float = Random.between(0f, 2 * math.Pi.toFloat * LogParticleMaxAngle)
|
||||
override def update(dt: Float): Unit = {
|
||||
time += LogParticleMoveSpeed * dt
|
||||
}
|
||||
override def draw(g: Graphics): Unit = {
|
||||
val size = (1 + time / LogParticleGrow).clamp() * LogParticleSize
|
||||
val offset = time.clamp() * LogParticleMoveDistance
|
||||
val alpha = 1 - time.clamp()
|
||||
|
||||
val r1 = (bounds.w max bounds.h) / math.sqrt(2) + offset + LogParticlePadding
|
||||
val r2 = r1 + size
|
||||
|
||||
for (i <- 0 until LogParticleCount) {
|
||||
val angle = particle.angle + (2 * math.Pi).toFloat * i / LogParticleCount
|
||||
val v = Vector2D.unit(angle)
|
||||
val a = angle + (2 * math.Pi).toFloat * i / LogParticleCount
|
||||
val v = Vector2D.unit(a)
|
||||
val p1 = v * r1 + bounds.center
|
||||
val p2 = v * r2 + bounds.center
|
||||
g.line(p1, p2, 1f, ColorScheme("LogParticle").mapA(_ => alpha))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def drawParticles(g: Graphics): Unit = {
|
||||
super.drawParticles(g)
|
||||
drawLogParticles(g)
|
||||
}
|
||||
}
|
||||
|
||||
object OcelotLogParticleNode {
|
||||
|
||||
@ -7,11 +7,10 @@ import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.node.{EntityNode, LabeledEntityNode}
|
||||
import ocelot.desktop.ui.UiHandler
|
||||
import ocelot.desktop.ui.event.BrainEvent
|
||||
import ocelot.desktop.ui.particle.Particle
|
||||
import totoro.ocelot.brain.entity.traits.{Entity, Environment}
|
||||
import totoro.ocelot.brain.event.NoteBlockTriggerEvent
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
abstract class NoteBlockNodeBase(entity: Entity with Environment) extends EntityNode(entity) with LabeledEntityNode {
|
||||
eventHandlers += {
|
||||
case BrainEvent(event: NoteBlockTriggerEvent) =>
|
||||
@ -22,31 +21,24 @@ abstract class NoteBlockNodeBase(entity: Entity with Environment) extends Entity
|
||||
volume = event.volume.toFloat.min(1f).max(0f),
|
||||
).play()
|
||||
|
||||
addParticle(event.pitch)
|
||||
}
|
||||
|
||||
private val particles = mutable.ArrayBuffer[(Float, Int)]()
|
||||
|
||||
private def addParticle(pitch: Int): Unit = {
|
||||
synchronized {
|
||||
particles += ((0f, pitch))
|
||||
}
|
||||
}
|
||||
|
||||
override def drawParticles(g: Graphics): Unit = synchronized {
|
||||
for ((time, pitch) <- particles.reverseIterator) {
|
||||
val col = ColorScheme("Note" + pitch.min(24).max(0)).withAlpha(1 - (2 * time - 1).min(1).max(0))
|
||||
g.sprite("particles/Note", position + Vector2D(pitch / 24f * 40f + 5, height / 2 - 10 - 100 * time),
|
||||
Size2D(14, 20), col)
|
||||
}
|
||||
particles.mapInPlace { case (t, p) => (t + 1.2f * UiHandler.dt, p) }
|
||||
particles.filterInPlace(_._1 <= 1f)
|
||||
UiHandler.root.workspaceView.particleSystem.add(new NoteParticle(event.pitch))
|
||||
}
|
||||
|
||||
override def update(): Unit = {
|
||||
super.update()
|
||||
|
||||
if (isHovered || isMoving)
|
||||
if (isHovered || isMoving) {
|
||||
root.get.statusBar.addMouseEntry("icons/LMB", "Play sample")
|
||||
}
|
||||
}
|
||||
|
||||
private class NoteParticle(pitch: Int) extends Particle {
|
||||
override def update(dt: Float): Unit = {
|
||||
time += 1.2f * UiHandler.dt
|
||||
}
|
||||
override def draw(g: Graphics): Unit = {
|
||||
val col = ColorScheme("Note" + pitch.min(24).max(0)).withAlpha(1 - (2 * time - 1).min(1).max(0))
|
||||
g.sprite("particles/Note", position + Vector2D(pitch / 24f * 40f + 5, height / 2 - 10 - 100 * time),
|
||||
Size2D(14, 20), col)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/main/scala/ocelot/desktop/ui/particle/Particle.scala
Normal file
25
src/main/scala/ocelot/desktop/ui/particle/Particle.scala
Normal file
@ -0,0 +1,25 @@
|
||||
package ocelot.desktop.ui.particle
|
||||
|
||||
import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.ui.widget.Widget
|
||||
|
||||
/**
|
||||
* Everything a single particle needs to live and thrive.
|
||||
* @param time current lifetime of the particle
|
||||
* @param ttl when the `time` will reach this value - the particle is going to be discarded
|
||||
* @param origin optional, marks the widget that "spawned" this particle
|
||||
*/
|
||||
|
||||
case class Particle(
|
||||
var time: Float = 0.0f,
|
||||
var ttl: Float = 1.0f,
|
||||
origin: Option[Widget] = None,
|
||||
) {
|
||||
def expired: Boolean = time >= ttl
|
||||
|
||||
def update(dt: Float): Unit = {
|
||||
time += dt
|
||||
}
|
||||
|
||||
def draw(g: Graphics): Unit = {}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package ocelot.desktop.ui.particle
|
||||
|
||||
import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.ui.widget.Widget
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
class ParticleSystem {
|
||||
private val particles: ListBuffer[Particle] = ListBuffer.empty
|
||||
|
||||
def add(particle: Particle): Unit = particles.addOne(particle)
|
||||
|
||||
def count[A](origin: Option[Widget]): Int = particles.count(p => p.isInstanceOf[A] && p.origin == origin)
|
||||
|
||||
/**
|
||||
* Calls `update` method on all particles, advances time.
|
||||
* Removes expired particles.
|
||||
* @param dt delta time
|
||||
*/
|
||||
def update(dt: Float): Unit = {
|
||||
particles.foreach(_.update(dt))
|
||||
particles.filterInPlace(!_.expired)
|
||||
}
|
||||
|
||||
def draw(g: Graphics): Unit = {
|
||||
particles.foreach(_.draw(g))
|
||||
}
|
||||
|
||||
def clear(): Unit = {
|
||||
particles.clear()
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
package ocelot.desktop.ui.widget
|
||||
|
||||
import ocelot.desktop.ColorScheme
|
||||
import ocelot.desktop.color.Color
|
||||
import ocelot.desktop.geometry.Vector2D
|
||||
import ocelot.desktop.node.Node
|
||||
|
||||
class ComputerErrorMessageLabel(node: Node, override val text: String) extends Label {
|
||||
override def isSmall: Boolean = true
|
||||
|
||||
var alpha: Float = 1f
|
||||
|
||||
override def color: Color = ColorScheme("ErrorMessage").toRGBANorm.mapA(_ * alpha)
|
||||
|
||||
val initialPosition: Vector2D = {
|
||||
val position = node.position
|
||||
val size = node.size
|
||||
|
||||
position + Vector2D(size.width / 2 - minimumSize.width / 2, -minimumSize.height)
|
||||
}
|
||||
|
||||
position = initialPosition
|
||||
}
|
||||
@ -6,10 +6,12 @@ import ocelot.desktop.graphics.{Graphics, IconSource}
|
||||
import ocelot.desktop.node.Node.Size
|
||||
import ocelot.desktop.node.nodes.{ComputerNode, ScreenNode}
|
||||
import ocelot.desktop.node.{EntityNode, Node, NodePort}
|
||||
import ocelot.desktop.ui.UiHandler
|
||||
import ocelot.desktop.ui.event._
|
||||
import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler}
|
||||
import ocelot.desktop.ui.event.sources.KeyEvents
|
||||
import ocelot.desktop.ui.layout.{CopyLayout, Layout}
|
||||
import ocelot.desktop.ui.particle.ParticleSystem
|
||||
import ocelot.desktop.ui.widget.WorkspaceView.NodeLoadException
|
||||
import ocelot.desktop.ui.widget.window.{NodeSelector, ProfilerWindow, WindowPool}
|
||||
import ocelot.desktop.util.Keybind.{Center, Profiler}
|
||||
@ -34,6 +36,7 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover
|
||||
@volatile
|
||||
var nodes: immutable.Seq[Node] = immutable.ArraySeq[Node]()
|
||||
|
||||
var particleSystem = new ParticleSystem
|
||||
var windowPool = new WindowPool
|
||||
var nodeSelector = new NodeSelector
|
||||
var profilerWindow = new ProfilerWindow
|
||||
@ -65,6 +68,7 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover
|
||||
def reset(): Unit = {
|
||||
nodes.foreach(_.dispose())
|
||||
nodes = nodes.empty
|
||||
particleSystem.clear()
|
||||
windowPool.deleteAllWindows()
|
||||
nodeSelector = new NodeSelector
|
||||
profilerWindow = new ProfilerWindow
|
||||
@ -458,7 +462,7 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover
|
||||
}
|
||||
}
|
||||
|
||||
def newConnectionTarget: Option[(Node, NodePort)] = newConnection flatMap { case (node, _, endpoint) =>
|
||||
private def newConnectionTarget: Option[(Node, NodePort)] = newConnection flatMap { case (node, _, endpoint) =>
|
||||
val validTargets = {
|
||||
nodes.iterator
|
||||
.filter(_ != node)
|
||||
@ -607,7 +611,6 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover
|
||||
nodes.foreach(_.draw(g))
|
||||
nodes.foreach(_.drawLight(g))
|
||||
nodes.foreach(_.drawLabel(g))
|
||||
nodes.foreach(_.drawParticles(g))
|
||||
|
||||
portsAlpha.update()
|
||||
portsAlpha.goto(if (newConnection.isDefined) 1 else 0)
|
||||
@ -637,12 +640,15 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover
|
||||
drawSelectorConnection(g, Rect2D(newNodePos.x + cameraOffset.x, newNodePos.y + cameraOffset.y, 64, 64),
|
||||
nodeSelector.bounds, color = color)
|
||||
|
||||
particleSystem.draw(g)
|
||||
|
||||
drawChildren(g)
|
||||
}
|
||||
|
||||
override def update(): Unit = {
|
||||
super.update()
|
||||
nodes.foreach(_.update())
|
||||
particleSystem.update(UiHandler.dt)
|
||||
|
||||
if (isHovered) {
|
||||
root.get.statusBar.addKeyEntry(Settings.get.keymap.name(Center), "Reset camera")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user