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.

297 lines
8.5 KiB

4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. package wow.doge.mygame.game
  2. import scala.concurrent.duration._
  3. import akka.actor.typed.ActorRef
  4. import akka.actor.typed.Behavior
  5. import akka.actor.typed.Props
  6. import akka.actor.typed.SpawnProtocol
  7. import akka.actor.typed.scaladsl.AskPattern._
  8. import akka.util.Timeout
  9. import cats.effect.Resource
  10. import cats.effect.concurrent.Deferred
  11. import com.jme3.bullet.BulletAppState
  12. import com.jme3.input.InputManager
  13. import com.jme3.scene.Node
  14. import com.jme3.scene.Spatial
  15. import com.jme3.system.AppSettings
  16. import com.softwaremill.tagging._
  17. import com.typesafe.scalalogging.{Logger => SLogger}
  18. import io.odin.Logger
  19. import monix.bio.Fiber
  20. import monix.bio.IO
  21. import monix.bio.Task
  22. import monix.bio.UIO
  23. import monix.catnap.ConcurrentChannel
  24. import monix.catnap.ConsumerF
  25. import monix.eval.Coeval
  26. import monix.execution.CancelableFuture
  27. import monix.execution.CancelablePromise
  28. import monix.execution.Scheduler
  29. import wow.doge.mygame.AppError
  30. import wow.doge.mygame.AppError.TimeoutError
  31. import wow.doge.mygame.Dispatchers
  32. import wow.doge.mygame.executors.JMERunner
  33. import wow.doge.mygame.executors.Schedulers
  34. import wow.doge.mygame.game.subsystems.ui.JFxUI
  35. import wow.doge.mygame.implicits._
  36. import wow.doge.mygame.utils.AkkaUtils
  37. import wow.doge.mygame.utils.GenericTimerActor
  38. import wow.doge.mygame.utils.wrappers.jme._
  39. import wow.doge.mygame.types._
  40. object GameAppTags {
  41. sealed trait RootNode
  42. sealed trait GuiNode
  43. }
  44. class GameApp private[game] (
  45. logger: Logger[Task],
  46. app: SimpleAppExt,
  47. gameActor: ActorRef[TestGameActor.Command],
  48. gameSpawnProtocol: ActorRef[SpawnProtocol.Command],
  49. scheduler: akka.actor.typed.Scheduler
  50. ) {
  51. def inputManager: UIO[InputManager] = UIO(app.getInputManager())
  52. val assetManager = new AssetManager(app.getAssetManager())
  53. val guiNode: GuiNode =
  54. AppNode2(app.getGuiNode()).taggedWith[GameAppTags.GuiNode]
  55. // def flyCam = Option(app.getFlyByCamera())
  56. def camera = UIO(app.getCamera())
  57. def viewPort = UIO(app.getViewPort())
  58. val rootNode: RootNode =
  59. AppNode2(app.getRootNode()).taggedWith[GameAppTags.RootNode]
  60. val physicsSpace =
  61. new PhysicsSpace(app.bulletAppState.physicsSpace)
  62. def enqueue(cb: () => Unit) = app.enqueueR(() => cb())
  63. def enqueueL[T](cb: () => T): Task[T] = app.enqueueL(cb)
  64. def whenTerminated: IO[AppError, Unit] =
  65. IO.deferFuture(app.whenTerminated).onErrorHandleWith(TimeoutError.from)
  66. def spawnGameActor[T](
  67. behavior: Behavior[T],
  68. actorName: Option[String] = None,
  69. props: Props = Dispatchers.jmeDispatcher
  70. )(implicit name: sourcecode.Name) =
  71. AkkaUtils.spawnActorL(behavior, actorName, props)(
  72. 2.seconds,
  73. scheduler,
  74. gameSpawnProtocol,
  75. name
  76. )
  77. def scheduler = JmeScheduler(app.scheduler)
  78. def jfxUI = JFxUI(app)
  79. }
  80. class GameAppResource(
  81. logger: Logger[Task],
  82. jmeThread: Scheduler,
  83. schedulers: Schedulers
  84. )(implicit
  85. timeout: Timeout,
  86. scheduler: akka.actor.typed.Scheduler,
  87. spawnProtocol: ActorRef[SpawnProtocol.Command]
  88. ) {
  89. def resource
  90. : Resource[UIO, Either[AppError, (GameApp, Fiber[Nothing, Unit])]] =
  91. Resource.make(
  92. (for {
  93. app <- UIO(new SimpleAppExt(schedulers, new BulletAppState))
  94. _ <- UIO(JMERunner.runner = Some(app.enqueue _))
  95. _ <- UIO {
  96. val settings = new AppSettings(true)
  97. settings.setVSync(true)
  98. /**
  99. * disables the launcher
  100. * We'll be making our own launcher anyway
  101. */
  102. app.setShowSettings(false)
  103. app.setSettings(settings)
  104. }
  105. fib <- UIO(app.start).executeOn(jmeThread).start
  106. _ <- Task.deferFuture(app.started).onErrorHandleWith(TimeoutError.from)
  107. testGameActor <- AkkaUtils.spawnActorL(
  108. new TestGameActor.Props().create,
  109. Some("testGameActor"),
  110. props = Dispatchers.jmeDispatcher
  111. )
  112. sp <-
  113. testGameActor
  114. .askL(TestGameActor.GetSpawnProtocol)
  115. .onErrorHandleWith(TimeoutError.from)
  116. gameApp <- UIO(new GameApp(logger, app, testGameActor, sp, scheduler))
  117. _ <- UIO {
  118. val fut = () => testGameActor.ask(TestGameActor.Stop).flatten
  119. app.cancelToken = Some(fut)
  120. }
  121. } yield (gameApp, fib)).attempt
  122. ) {
  123. case Right(gameApp -> fib) => fib.cancel
  124. case Left(error) => IO.terminate(new Exception(error.toString))
  125. }
  126. }
  127. object GameApp {}
  128. import akka.actor.typed.scaladsl.Behaviors
  129. import akka.actor.typed.scaladsl.ActorContext
  130. object TestGameActor {
  131. sealed trait Command
  132. case object Ping extends Command
  133. case class Stop(stopSignal: ActorRef[CancelableFuture[Unit]]) extends Command
  134. case class GetSpawnProtocol(
  135. replyTo: ActorRef[ActorRef[SpawnProtocol.Command]]
  136. ) extends Command
  137. import scala.concurrent.duration._
  138. class Props() {
  139. def create =
  140. Behaviors.setup[Command] { ctx =>
  141. ctx.spawn(
  142. GenericTimerActor
  143. .Props(ctx.self, Ping, 1000.millis)
  144. .behavior,
  145. "pingTimer"
  146. ) ! GenericTimerActor.Start
  147. new TestGameActor(ctx, this).receive
  148. }
  149. }
  150. }
  151. class TestGameActor(
  152. ctx: ActorContext[TestGameActor.Command],
  153. props: TestGameActor.Props
  154. ) {
  155. import TestGameActor._
  156. val stopPromise = CancelablePromise[Unit]()
  157. def receive =
  158. Behaviors
  159. .receiveMessage[Command] {
  160. case Stop(replyTo) =>
  161. ctx.log.infoP("stopping")
  162. replyTo ! stopPromise.future
  163. Behaviors.stopped
  164. case Ping =>
  165. ctx.log.debugP("ping")
  166. Behaviors.same
  167. case GetSpawnProtocol(replyTo) =>
  168. val sp = ctx.spawn(SpawnProtocol(), "gameSpawnProtocol")
  169. replyTo ! sp
  170. Behaviors.same
  171. }
  172. .receiveSignal {
  173. case (_, akka.actor.typed.PostStop) =>
  174. stopPromise.success(())
  175. Behaviors.same
  176. }
  177. }
  178. object Ops {
  179. final class AddToNode[T <: Node](private val node: T) extends AnyVal {
  180. /**
  181. * Pure version
  182. */
  183. def apply(spatial: Spatial)(implicit logger: Logger[Task]) =
  184. logger.debug(
  185. s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}"
  186. ) >> Task(node.attachChild(spatial))
  187. /**
  188. * Impure version
  189. */
  190. def apply(spatial: Spatial)(implicit logger: SLogger) =
  191. Coeval {
  192. logger.debug(
  193. s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}"
  194. )
  195. node.attachChild(spatial)
  196. }
  197. }
  198. }
  199. object SpawnSystem {
  200. sealed trait Result
  201. case object Ok extends Result
  202. sealed trait Complete
  203. case object Complete extends Complete
  204. sealed trait SpawnRequest
  205. final case class SpawnSpatial(nodeTask: Task[Node]) extends SpawnRequest
  206. final case class SpawnRequestWrapper(
  207. spawnRequest: SpawnRequest,
  208. result: Deferred[Task, Result]
  209. )
  210. def apply(logger: Logger[Task]) =
  211. for {
  212. spawnChannel <- ConcurrentChannel[Task].of[Complete, SpawnRequestWrapper]
  213. spawnSystem <- Task(new SpawnSystem(logger, spawnChannel))
  214. consumer <-
  215. spawnChannel.consume
  216. .use(consumer => spawnSystem.receive(consumer))
  217. .startAndForget
  218. } yield (spawnSystem)
  219. }
  220. class SpawnSystem(
  221. logger: Logger[Task],
  222. spawnChannel: ConcurrentChannel[
  223. Task,
  224. SpawnSystem.Complete,
  225. SpawnSystem.SpawnRequestWrapper
  226. ]
  227. ) {
  228. import SpawnSystem._
  229. for {
  230. spawnSystem <- SpawnSystem(logger)
  231. res <- spawnSystem.request(SpawnSpatial(Task(new Node("Test"))))
  232. } yield ()
  233. // val spawnChannel = ConcurrentChannel[Task].of[Result, SpawnRequest]
  234. private def receive(
  235. consumer: ConsumerF[Task, Complete, SpawnRequestWrapper]
  236. ): Task[Unit] =
  237. consumer.pull.flatMap {
  238. case Right(message) =>
  239. for {
  240. _ <-
  241. logger
  242. .debug(s"Received spawn request $message")
  243. _ <- handleSpawn(message)
  244. } yield receive(consumer)
  245. case Left(r) =>
  246. logger.info("Closing Spawn System")
  247. }
  248. private def handleSpawn(spawnRequestWrapper: SpawnRequestWrapper) =
  249. spawnRequestWrapper match {
  250. case SpawnRequestWrapper(spawnRequest, result) =>
  251. spawnRequest match {
  252. case SpawnSpatial(spatialTask) =>
  253. spatialTask.flatMap(spatial =>
  254. logger.debug(
  255. s"Spawning spatial with name ${spatial.getName()}"
  256. ) >> result
  257. .complete(Ok)
  258. )
  259. }
  260. }
  261. def request(spawnRequest: SpawnRequest) =
  262. for {
  263. d <- Deferred[Task, Result]
  264. _ <- spawnChannel.push(SpawnRequestWrapper(spawnRequest, d))
  265. res <- d.get
  266. } yield (res)
  267. def stop = spawnChannel.halt(Complete)
  268. }