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.

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