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.
 
 

289 lines
8.3 KiB

package wow.doge.mygame.game
import scala.concurrent.duration._
import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.Props
import akka.actor.typed.SpawnProtocol
import akka.actor.typed.scaladsl.AskPattern._
import akka.util.Timeout
import cats.effect.Resource
import cats.effect.concurrent.Deferred
import com.jme3.app.state.AppStateManager
import com.jme3.bullet.BulletAppState
import com.jme3.input.InputManager
import com.jme3.scene.Node
import com.jme3.scene.Spatial
import com.jme3.system.AppSettings
import com.softwaremill.tagging._
import com.typesafe.scalalogging.{Logger => SLogger}
import io.odin.Logger
import monix.bio.Fiber
import monix.bio.Task
import monix.bio.UIO
import monix.catnap.ConcurrentChannel
import monix.catnap.ConsumerF
import monix.eval.Coeval
import monix.execution.CancelableFuture
import monix.execution.CancelablePromise
import monix.execution.Scheduler
import wow.doge.mygame.executors.JMERunner
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.utils.GenericTimerActor
import wow.doge.mygame.game.subsystems.ui.JFxUI
import wow.doge.mygame.implicits._
import wow.doge.mygame.utils.AkkaUtils
import wow.doge.mygame.utils.wrappers.jme._
import wow.doge.mygame.Dispatchers
object GameAppTags {
sealed trait RootNode
sealed trait GuiNode
}
class GameApp private[game] (
logger: Logger[Task],
app: SimpleAppExt,
gameActor: ActorRef[TestGameActor.Command],
gameSpawnProtocol: ActorRef[SpawnProtocol.Command]
) {
def stateManager: Task[AppStateManager] = Task(app.getStateManager())
def inputManager: Task[InputManager] = Task(app.getInputManager())
def assetManager = new AssetManager(app.getAssetManager())
def guiNode = Task(app.getGuiNode().taggedWith[GameAppTags.GuiNode])
val guiNode2 = AppNode[Task](app.getGuiNode()).taggedWith[GameAppTags.GuiNode]
def flyCam = Option(app.getFlyByCamera())
def camera = Task(app.getCamera())
def viewPort = Task(app.getViewPort())
// def rootNode = Task(app.getRootNode().taggedWith[GameAppTags.RootNode])
val rootNode =
AppNode[Task](app.getRootNode()).taggedWith[GameAppTags.RootNode]
val physicsSpace = new PhysicsSpace[Task](app.bulletAppState.physicsSpace)
def enqueue(cb: () => Unit) = app.enqueueR(() => cb())
def enqueueL[T](cb: () => T): Task[T] = app.enqueueL(cb)
def spawnGameActor[T](
behavior: Behavior[T],
actorName: String,
props: Props = Dispatchers.jmeDispatcher
)(implicit scheduler: akka.actor.typed.Scheduler) =
AkkaUtils.spawnActorL(behavior, actorName, props)(
2.seconds,
scheduler,
gameSpawnProtocol
)
def scheduler = app.scheduler
def jfxUI = JFxUI(app)
}
class GameAppResource(
logger: Logger[Task],
jmeThread: Scheduler,
schedulers: Schedulers
)(implicit
timeout: Timeout,
scheduler: akka.actor.typed.Scheduler,
spawnProtocol: ActorRef[SpawnProtocol.Command]
) {
def resource: Resource[Task, (GameApp, Fiber[Throwable, Unit])] =
Resource.make {
lazy val bullet = new BulletAppState
for {
app <- Task(new SimpleAppExt(schedulers, bullet))
_ <- UIO(JMERunner.runner = Some(app.enqueue _))
_ <- Task {
val settings = new AppSettings(true)
settings.setVSync(true)
/**
* disables the launcher
* We'll be making our own launcher anyway
*/
app.setShowSettings(false)
app.setSettings(settings)
}
fib <- Task(app.start).executeOn(jmeThread).start
_ <- Task.deferFuture(app.started)
testGameActor <- AkkaUtils.spawnActorL(
new TestGameActor.Props().create,
"testGameActor",
Dispatchers.jmeDispatcher
)
sp <- testGameActor.askL(TestGameActor.GetSpawnProtocol(_))
gameApp <- Task(new GameApp(logger, app, testGameActor, sp))
_ <- Task {
val fut = () => testGameActor.ask(TestGameActor.Stop).flatten
app.cancelToken = Some(fut)
}
} yield (gameApp, fib)
} {
case (gameApp, fib) =>
fib.cancel >> UIO(JMERunner.runner = None)
}
}
object GameApp {}
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.ActorContext
object TestGameActor {
sealed trait Command
case object Ping extends Command
case class Stop(stopSignal: ActorRef[CancelableFuture[Unit]]) extends Command
case class GetSpawnProtocol(
replyTo: ActorRef[ActorRef[SpawnProtocol.Command]]
) extends Command
import scala.concurrent.duration._
class Props() {
def create =
Behaviors.setup[Command] { ctx =>
ctx.spawn(
GenericTimerActor
.Props(ctx.self, Ping, 1000.millis)
.behavior,
"pingTimer"
) ! GenericTimerActor.Start
new TestGameActor(ctx, this).receive
}
}
}
class TestGameActor(
ctx: ActorContext[TestGameActor.Command],
props: TestGameActor.Props
) {
import TestGameActor._
val stopPromise = CancelablePromise[Unit]()
def receive =
Behaviors
.receiveMessage[Command] {
case Stop(replyTo) =>
ctx.log.infoP("stopping")
replyTo ! stopPromise.future
Behaviors.stopped
case Ping =>
ctx.log.debugP("ping")
Behaviors.same
case GetSpawnProtocol(replyTo) =>
val sp = ctx.spawn(SpawnProtocol(), "gameSpawnProtocol")
replyTo ! sp
Behaviors.same
}
.receiveSignal {
case (_, akka.actor.typed.PostStop) =>
stopPromise.success(())
Behaviors.same
}
}
object Ops {
final class AddToNode[T <: Node](private val node: T) extends AnyVal {
/**
* Pure version
*/
def apply(spatial: Spatial)(implicit logger: Logger[Task]) =
logger.debug(
s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}"
) >> Task(node.attachChild(spatial))
/**
* Impure version
*/
def apply(spatial: Spatial)(implicit logger: SLogger) =
Coeval {
logger.debug(
s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}"
)
node.attachChild(spatial)
}
}
}
object SpawnSystem {
sealed trait Result
final case object Ok extends Result
sealed trait Complete
final case object Complete extends Complete
sealed trait SpawnRequest
final case class SpawnSpatial(nodeTask: Task[Node]) extends SpawnRequest
final case class SpawnRequestWrapper(
spawnRequest: SpawnRequest,
result: Deferred[Task, Result]
)
def apply(logger: Logger[Task]) =
for {
spawnChannel <- ConcurrentChannel[Task].of[Complete, SpawnRequestWrapper]
spawnSystem <- Task(new SpawnSystem(logger, spawnChannel))
consumer <-
spawnChannel.consume
.use(consumer => spawnSystem.receive(consumer))
.startAndForget
} yield (spawnSystem)
}
class SpawnSystem(
logger: Logger[Task],
spawnChannel: ConcurrentChannel[
Task,
SpawnSystem.Complete,
SpawnSystem.SpawnRequestWrapper
]
) {
import SpawnSystem._
for {
spawnSystem <- SpawnSystem(logger)
res <- spawnSystem.request(SpawnSpatial(Task(new Node("Test"))))
} yield ()
// val spawnChannel = ConcurrentChannel[Task].of[Result, SpawnRequest]
private def receive(
consumer: ConsumerF[Task, Complete, SpawnRequestWrapper]
): Task[Unit] =
consumer.pull.flatMap {
case Right(message) =>
for {
_ <-
logger
.debug(s"Received spawn request $message")
_ <- handleSpawn(message)
} yield receive(consumer)
case Left(r) =>
logger.info("Closing Spawn System")
}
private def handleSpawn(spawnRequestWrapper: SpawnRequestWrapper) =
spawnRequestWrapper match {
case SpawnRequestWrapper(spawnRequest, result) =>
spawnRequest match {
case SpawnSpatial(spatialTask) =>
spatialTask.flatMap(spatial =>
logger.debug(
s"Spawning spatial with name ${spatial.getName()}"
) >> result
.complete(Ok)
)
}
}
def request(spawnRequest: SpawnRequest) =
for {
d <- Deferred[Task, Result]
_ <- spawnChannel.push(SpawnRequestWrapper(spawnRequest, d))
res <- d.get
} yield (res)
def stop = spawnChannel.halt(Complete)
}