Testing out JmonkeyEngine to make a game in Scala with Akka Actors within a pure FP layer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

514 lines
15 KiB

package wow.doge.mygame
import com.jme3.app.SimpleApplication
import com.jme3.app.state.AppStateManager
import scala.reflect.ClassTag
import com.jme3.app.state.AppState
import com.jme3.scene.Node
import com.jme3.scene.Spatial
import com.simsilica.es.EntityData
import com.simsilica.es.EntityComponent
import com.simsilica.es.EntityId
import akka.actor.typed.ActorRef
import akka.util.Timeout
import akka.actor.typed.Scheduler
import monix.bio.Task
import com.jme3.input.InputManager
import com.jme3.input.controls.Trigger
import com.jme3.input.controls.InputListener
import com.jme3.math.Vector3f
import wow.doge.mygame.math.ImVector3f
import com.jme3.scene.Geometry
import wow.doge.mygame.state.CardinalDirection
import wow.doge.mygame.state.CanMove
import com.jme3.renderer.Camera
import scala.jdk.CollectionConverters._
import wow.doge.mygame.utils.JFXConsoleStreamable
import com.jme3.app.Application
import java.util.concurrent.Callable
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import com.jme3.scene.SceneGraphVisitor
import monix.reactive.Observable
import com.jme3.asset.AssetManager
import com.jme3.asset.AssetLocator
import com.jme3.input.controls.ActionListener
import monix.reactive.OverflowStrategy
import monix.execution.Ack
import monix.execution.Cancelable
import monix.execution.cancelables.SingleAssignCancelable
import com.jme3.input.Action
import com.jme3.bullet.PhysicsSpace
import com.jme3.bullet.collision.PhysicsCollisionListener
import com.jme3.bullet.collision.PhysicsCollisionEvent
import com.jme3.bullet.PhysicsTickListener
import monix.reactive.observers.Subscriber
import monix.execution.Ack.Continue
import monix.execution.Ack.Stop
case class ActionEvent(binding: Action, value: Boolean, tpf: Float)
case class PhysicsTickEvent(space: PhysicsSpace, tpf: Float)
package object implicits {
type PrePhysicsTickEvent = PhysicsTickEvent
type PhysicsTickObservable =
Observable[Either[PrePhysicsTickEvent, PhysicsTickEvent]]
implicit class JMEAppExt(val app: Application) extends AnyVal {
/**
* Blocking task. Execute on a thread pool meant for blocking operations.
* Prefer [[wow.doge.mygame.implicits.JMEAppExt#enqueueT]] instead.
*
* @param cb
* @param ec
* @return
*/
def enqueueF[T](cb: () => T)(implicit ec: ExecutionContext): Future[T] =
Future {
app
.enqueue(new Callable[T]() {
override def call(): T = cb()
})
.get()
}
/**
* Blocking task. Execute on a thread pool meant for blocking operations.
* Same as enqueue, but returns a Monix Task instead of Future
* @param cb
* @param ec
* @return
*/
def enqueueL[T](cb: () => T): Task[T] =
Task
.deferFutureAction(implicit s => enqueueF(cb))
def enqueueF[T](cb: => T) =
app.enqueue(new Runnable {
override def run() = cb
})
def enqueueT(cb: => Unit) =
Task(enqueueF(cb))
}
implicit class StateManagerExt(val sm: AppStateManager) extends AnyVal {
def state[S <: AppState]()(implicit c: ClassTag[S]): S =
sm.getState(c.runtimeClass.asInstanceOf[Class[S]])
}
implicit class SimpleApplicationExt(val sa: SimpleApplication)
extends AnyVal {
def stateManager: AppStateManager = sa.getStateManager()
def inputManager: InputManager = sa.getInputManager()
def assetManager: AssetManager = sa.getAssetManager()
def guiNode = sa.getGuiNode()
def flyCam = Option(sa.getFlyByCamera())
def camera = sa.getCamera()
def viewPort = sa.getViewPort()
}
implicit class NodeExt(val n: Node) extends AnyVal {
/**
* Attaches the given child
*
* @param s
* @return
*/
def child(s: Spatial): Node = {
n.attachChild(s)
n
}
/**
* Gets the list of children as a scala collection
*
* @return
*/
// def children = n.getChildren().asScala.toSeq
def children = Observable.fromIterable(n.getChildren().asScala)
/**
* Attach given children
*
* @param lst
*/
def children(lst: Iterable[Spatial]): Unit = {
for (c <- lst) n.child(c)
}
def depthFirst(cb: Spatial => Unit) =
n.depthFirstTraversal(new SceneGraphVisitor() {
override def visit(s: Spatial) = cb(s)
})
def observableDepthFirst(): Observable[Spatial] = {
def loop(
subscriber: Subscriber[Spatial],
spatial: Spatial
): Task[Unit] = {
//spatial can be either a node or a geometry, but it's not a sealed trait
spatial match {
case node: Node =>
Task.deferFuture(subscriber.onNext(node)).flatMap {
case Ack.Continue => {
//modifying a node's children list is forbidden
val children = node.getChildren().asScala.to(LazyList)
if (!children.isEmpty) {
Task.sequence(
children.map(c => loop(subscriber, c))
) >> Task.unit
} else {
Task.unit
}
}
case Ack.Stop => Task.unit
}
//geomtries do not/cannot have children
case g: Geometry =>
Task.deferFuture(subscriber.onNext(g)) >> Task.unit
case _ => Task.unit
}
}
Observable.create(OverflowStrategy.Unbounded) { sub =>
implicit val sched = sub.scheduler
loop(sub, n).runToFuture
}
}
def breadthFirst(cb: Spatial => Unit) =
n.breadthFirstTraversal(new SceneGraphVisitor() {
override def visit(s: Spatial) = cb(s)
})
def observableBreadthFirst(): Observable[Spatial] = {
def loop(
subscriber: Subscriber[Spatial],
spatials: LazyList[Spatial]
): Task[Unit] = {
// spatial can be either a node or a geometry, but it's not a sealed trait
spatials match {
case head #:: tail =>
head match {
case g: Geometry =>
Task.deferFuture(subscriber.onNext(g)).flatMap {
case Continue =>
loop(subscriber, tail)
case Stop => Task.unit
}
case node: Node =>
val children = node.getChildren().asScala.to(LazyList)
Task.deferFuture(subscriber.onNext(node)).flatMap {
case Continue =>
loop(subscriber, tail #::: children)
case Stop => Task.unit
}
// case _ => loop(subscriber, tail)
}
case LazyList() => Task.unit
}
}
Observable.create(OverflowStrategy.Unbounded) { sub =>
implicit val sched = sub.scheduler
loop(sub, LazyList(n)).runToFuture
}
}
}
implicit class EntityDataExt(val ed: EntityData) extends AnyVal {
def query = new EntityQuery(ed)
// def entities[T <: EntityComponent](entities: Seq[T])
}
implicit class EntityExt(val e: EntityId) extends AnyVal {
def withComponents(classes: EntityComponent*)(implicit
ed: EntityData
): EntityId = {
ed.setComponents(e, classes: _*)
e
}
}
implicit class ActorRefExt[Req](val a: ActorRef[Req]) extends AnyVal {
import akka.actor.typed.scaladsl.AskPattern._
def askT[Res](
replyTo: ActorRef[Res] => Req
)(implicit timeout: Timeout, scheduler: Scheduler): Task[Res] = {
Task.deferFuture(a.ask(replyTo)(timeout, scheduler))
}
}
// def ?[Res](replyTo: ActorRef[Res] => Req)(implicit timeout: Timeout, scheduler: Scheduler): Future[Res] = {
// ask(replyTo)(timeout, scheduler)
// }
implicit class InputManagerExt(val inputManager: InputManager)
extends AnyVal {
def withMapping(mapping: String, triggers: Trigger*): InputManager = {
inputManager.addMapping(mapping, triggers: _*)
inputManager
}
def withListener(listener: InputListener, mappings: String*) = {
inputManager.addListener(listener, mappings: _*)
inputManager
}
def observableAction(mappingNames: String*): Observable[ActionEvent] = {
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val al = new ActionListener {
override def onAction(
binding: String,
value: Boolean,
tpf: Float
): Unit = {
if (
sub.onNext(ActionEvent(Action(binding), value, tpf)) == Ack.Stop
)
c.cancel()
}
}
inputManager.addListener(al, mappingNames: _*)
c := Cancelable(() => inputManager.removeListener(al))
c
}
}
}
implicit class PhysicsSpaceExt(val space: PhysicsSpace) extends AnyVal {
def collisionObservable(): Observable[PhysicsCollisionEvent] = {
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val cl = new PhysicsCollisionListener {
override def collision(event: PhysicsCollisionEvent): Unit = {
if (sub.onNext(event) == Ack.Stop)
c.cancel()
}
}
space.addCollisionListener(cl)
c := Cancelable(() => space.removeCollisionListener(cl))
c
}
}
def physicsTickObservable(): PhysicsTickObservable = {
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val cl = new PhysicsTickListener {
override def prePhysicsTick(space: PhysicsSpace, tpf: Float): Unit = {
val event = PhysicsTickEvent(space, tpf)
if (sub.onNext(Left(event)) == Ack.Stop)
c.cancel()
}
override def physicsTick(space: PhysicsSpace, tpf: Float): Unit = {
val event = PhysicsTickEvent(space, tpf)
if (sub.onNext(Right(event)) == Ack.Stop)
c.cancel()
}
}
space.addTickListener(cl)
c := Cancelable(() => space.removeTickListener(cl))
c
}
}
}
implicit class AssetManagerExt(val am: AssetManager) extends AnyVal {
def registerLocator(
assetPath: os.RelPath,
locator: Class[_ <: AssetLocator]
): Unit = {
am.registerLocator(assetPath.toString(), locator)
}
def loadModel(assetPath: os.RelPath): Spatial = {
am.loadModel(assetPath.toString())
}
}
implicit class Vector3fExt(val v: Vector3f) extends AnyVal {
def +=(that: Vector3f) = v.addLocal(that)
def *=(that: Vector3f) = v.multLocal(that)
def -=(that: Vector3f) = v.subtractLocal(that)
def /=(that: Vector3f) = v.divideLocal(that)
def unary_- = v.negateLocal()
def immutable = ImVector3f(v.x, v.y, v.z)
}
implicit class ImVector3fExt(val v: ImVector3f) extends AnyVal {
def +(that: ImVector3f) = v.copy(v.x + that.x, v.y + that.y, v.z + that.z)
def *(that: ImVector3f) = v.copy(v.x * that.x, v.y * that.y, v.z * that.z)
def *(f: Float): ImVector3f =
// v.copy(v.x * f, v.y * f, v.x * f)
v * ImVector3f(f, f, f)
def -(that: ImVector3f) = v.copy(v.x - that.x, v.y - that.y, v.z - that.z)
def /(that: ImVector3f) = v.copy(v.x / that.x, v.y / that.y, v.z / that.z)
def unary_- = v.copy(-v.x, -v.y, -v.z)
// def unary_-(that: ImVector3f) = this.copy(this.x, this.y, this.z)
def mutable = new Vector3f(v.x, v.y, v.z)
}
// implicit val implVector3fForVector3 = new Vector3[Vector3f] {
// override def +(implicit v: Vector3f, that: com.jme3.math.Vector3f): Unit =
// v += that
// override def *(implicit v: Vector3f, that: Vector3f): Unit = v *= that
// override def -(implicit v: Vector3f, that: Vector3f): Unit = v -= that
// override def /(implicit v: Vector3f, that: Vector3f): Unit = v /= that
// }
// implicit val implImVector3fForVector3 = new Vector3[ImVector3f] {
// override def +(implicit v: ImVector3f, that: ImVector3f): Unit =
// v + that
// override def *(implicit v: ImVector3f, that: ImVector3f): Unit = v * that
// override def -(implicit v: ImVector3f, that: ImVector3f): Unit = v - that
// override def /(implicit v: ImVector3f, that: ImVector3f): Unit = v / that
// }
// def test[T](v: T)(implicit ev: Vector3[T]) = {
// import ev._
// ev.+
// }
implicit val implCanMoveForGeom = new CanMove[Spatial] {
override def move(inst: Spatial, direction: ImVector3f): Unit = {
// val v = inst.getLocalTranslation()
// inst match {
// case n: Node => println(n.getChildren())
// case _ =>
// }
inst.move(direction.mutable)
}
override def getDirection(
cam: Camera,
cardinalDir: CardinalDirection
): ImVector3f = {
// val camDir =
// cam.getDirection().immutable * 0.6f
// val camLeft = cam.getLeft().immutable * 0.4f
// val zero = ImVector3f.ZERO
// val dir = cardinalDir
// val walkDir = {
// val mutWalkDir = new Vector3f()
// if (dir.left) {
// // ctx.log.trace("left")
// mutWalkDir += (zero + camLeft).mutable
// }
// if (dir.right) {
// // ctx.log.trace("right")
// mutWalkDir += (zero + -camLeft).mutable
// }
// if (dir.up) {
// // ctx.log.trace("up")
// mutWalkDir += (zero + camDir).mutable
// }
// if (dir.down) {
// // ctx.log.trace("down")
// mutWalkDir += (zero + -camDir).mutable
// }
// mutWalkDir.immutable
// }
// walkDir
// val camDir =
// cam.getDirection().immutable * 0.6f
// val camLeft = cam.getLeft().immutable * 0.4f
val zero = ImVector3f.ZERO
val dir = cardinalDir
val walkDir = {
val mutWalkDir = new Vector3f()
if (dir.left) {
// ctx.log.trace("left")
mutWalkDir += (zero + ImVector3f(-1, 0, 0)).mutable
}
if (dir.right) {
// ctx.log.trace("right")
mutWalkDir += (zero + ImVector3f(1, 0, 0)).mutable
}
if (dir.up) {
// ctx.log.trace("up")
mutWalkDir += (zero + ImVector3f(0, 0, -1)).mutable
}
if (dir.down) {
// ctx.log.trace("down")
mutWalkDir += (zero + ImVector3f(0, 0, 1)).mutable
}
mutWalkDir.immutable
}
walkDir
}
}
implicit val implJFXConsoleStreamForTextArea =
new JFXConsoleStreamable[scalafx.scene.control.TextArea] {
override def println(
ta: scalafx.scene.control.TextArea,
text: String
): Unit =
ta.appendText(text + "\n")
override def print(
ta: scalafx.scene.control.TextArea,
text: String
): Unit =
ta.appendText(text)
}
// val TasktoUIO = new FunctionK[Task, UIO] {
// def apply[T](f: Task[T]): UIO[T] =
// f.hideErrors
// }
}
// Observable.create(OverflowStrategy.Unbounded) { sub =>
// // val c = SingleAssignCancelable()
// val visitor = new SceneGraphVisitor {
// override def visit(s: Spatial): Unit = {
// sub.onNext(s)
// // if (sub.onNext(s) == Ack.Stop)
// // c.cancel()
// }
// }
// n.depthFirstTraversal(visitor)
// // c := Cancelable(() => ???)
// // c
// Cancelable.empty