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.

604 lines
21 KiB

4 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 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
4 years ago
3 years ago
3 years ago
4 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
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
3 years ago
4 years ago
3 years ago
4 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
4 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 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
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
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
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
4 years ago
  1. package wow.doge.mygame
  2. import java.util.concurrent.TimeoutException
  3. import scala.annotation.switch
  4. import scala.concurrent.duration._
  5. import akka.actor.typed.ActorRef
  6. import akka.actor.typed.SpawnProtocol
  7. import akka.util.Timeout
  8. import cats.effect.Resource
  9. import cats.effect.concurrent.Deferred
  10. import cats.syntax.eq._
  11. import cats.syntax.show._
  12. import com.jayfella.jme.jfx.JavaFxUI
  13. import com.jme3.asset.plugins.ZipLocator
  14. import com.jme3.bullet.control.BetterCharacterControl
  15. import com.jme3.input.InputManager
  16. import com.jme3.material.Material
  17. import com.jme3.material.MaterialDef
  18. import com.jme3.math.ColorRGBA
  19. import com.jme3.math.FastMath
  20. import com.jme3.math.Quaternion
  21. import com.jme3.math.Vector3f
  22. import com.jme3.renderer.Camera
  23. import com.jme3.renderer.ViewPort
  24. import com.jme3.scene.Node
  25. import com.softwaremill.macwire._
  26. import com.softwaremill.tagging._
  27. import io.odin.Logger
  28. import monix.bio.Fiber
  29. import monix.bio.IO
  30. import monix.bio.Task
  31. import monix.bio.UIO
  32. import monix.eval.Coeval
  33. import monix.execution.cancelables.CompositeCancelable
  34. import monix.reactive.Observable
  35. import monix.{eval => me}
  36. import scalafx.scene.control.Label
  37. import scalafx.scene.control.TextArea
  38. import scalafx.scene.layout.HBox
  39. import scalafx.scene.layout.Priority
  40. import scalafx.scene.layout.VBox
  41. import scalafx.scene.paint.Color
  42. import wow.doge.mygame.AppError.TimeoutError
  43. import wow.doge.mygame.executors.Schedulers
  44. import wow.doge.mygame.game.GameApp
  45. import wow.doge.mygame.game.GameAppResource
  46. import wow.doge.mygame.game.controls.FollowControl
  47. import wow.doge.mygame.game.entities.CharacterStats
  48. import wow.doge.mygame.game.entities.EntityIds
  49. import wow.doge.mygame.game.entities.NpcActorSupervisor
  50. import wow.doge.mygame.game.entities.NpcMovementActor
  51. import wow.doge.mygame.game.entities.player.PlayerActor
  52. import wow.doge.mygame.game.entities.player.PlayerController
  53. import wow.doge.mygame.game.subsystems.input.GameInputHandler
  54. import wow.doge.mygame.game.subsystems.input.PlayerCameraInput
  55. import wow.doge.mygame.game.subsystems.level.DefaultGameLevel
  56. import wow.doge.mygame.implicits._
  57. import wow.doge.mygame.launcher.Launcher
  58. import wow.doge.mygame.launcher.Launcher.LauncherResult
  59. import wow.doge.mygame.math.ImVector3f
  60. import wow.doge.mygame.subsystems.events.Event
  61. import wow.doge.mygame.subsystems.events.EventBus
  62. import wow.doge.mygame.subsystems.events.EventBus.ObservableSubscription
  63. import wow.doge.mygame.subsystems.events.EventsModule
  64. import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
  65. import wow.doge.mygame.subsystems.events.PlayerCameraEvent
  66. import wow.doge.mygame.subsystems.events.PlayerEvent
  67. import wow.doge.mygame.subsystems.events.PlayerMovementEvent
  68. import wow.doge.mygame.subsystems.events.StatsEvent.DamageEvent
  69. import wow.doge.mygame.subsystems.events.TickEvent
  70. import wow.doge.mygame.subsystems.scriptsystem.ScriptCompiler
  71. import wow.doge.mygame.subsystems.scriptsystem.ScriptInitMode
  72. import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource
  73. import wow.doge.mygame.types._
  74. import wow.doge.mygame.utils.AkkaUtils
  75. import wow.doge.mygame.utils.GenericConsoleStream
  76. import wow.doge.mygame.utils.IOUtils
  77. import wow.doge.mygame.utils.MonixDirectoryWatcher
  78. import wow.doge.mygame.utils.MonixDirectoryWatcher.ModifyEvent
  79. import wow.doge.mygame.utils.controls.JFXProgressBar
  80. import wow.doge.mygame.utils.wrappers.jme.AssetManager
  81. import wow.doge.mygame.utils.wrappers.jme.PhysicsSpace
  82. class MainApp(
  83. logger: Logger[Task],
  84. jmeThread: JmeScheduler,
  85. schedulers: Schedulers,
  86. consoleStream: GenericConsoleStream[TextArea]
  87. )(implicit
  88. spawnProtocol: ActorRef[SpawnProtocol.Command],
  89. timeout: Timeout,
  90. scheduler: AkkaScheduler
  91. ) {
  92. implicit val as = scheduler.value
  93. val scriptSystemResource: Resource[UIO, ScriptCompiler] =
  94. new ScriptSystemResource(os.pwd, logger, ScriptInitMode.Eager).init2
  95. val eventsModule = new EventsModule(scheduler, spawnProtocol)
  96. def eval(
  97. tickEventBus: GameEventBus[TickEvent],
  98. gameApp: GameApp,
  99. fib: Fiber[Nothing, Unit]
  100. ) =
  101. for {
  102. // g <- UIO.pure(gameApp)
  103. playerEventBus <- eventsModule.playerEventBus
  104. mainEventBus <- eventsModule.mainEventBus
  105. obs <-
  106. playerEventBus
  107. .askL[Observable[PlayerMovementEvent]](ObservableSubscription(_))
  108. .onErrorHandleWith(TimeoutError.from)
  109. _ <-
  110. IOUtils
  111. .toIO(
  112. obs
  113. .doOnNextF(pme =>
  114. Coeval(pprint.log(show"Received event $pme")).void
  115. )
  116. .completedL
  117. .startAndForget
  118. )
  119. .hideErrors
  120. inputManager <- gameApp.inputManager
  121. assetManager <- UIO.pure(gameApp.assetManager)
  122. camera <- gameApp.camera
  123. rootNode <- UIO.pure(gameApp.rootNode)
  124. enqueueR <- UIO(gameApp.enqueue _)
  125. viewPort <- gameApp.viewPort
  126. physicsSpace <- UIO.pure(gameApp.physicsSpace)
  127. _ <- logger.infoU("before")
  128. jfxUI <- gameApp.jfxUI.hideErrors
  129. consoleTextArea <- UIO(new TextArea {
  130. text = "hello \n"
  131. editable = false
  132. wrapText = true
  133. // maxHeight = 150
  134. // maxWidth = 300
  135. })
  136. // _ <- Task(consoleStream := consoleTextArea)
  137. // _ <- Task(jfxUI += consoleTextArea)
  138. _ <- logger.infoU("after")
  139. _ <- logger.infoU("Initializing console stream")
  140. _ <-
  141. wire[MainAppDelegate]
  142. .init()
  143. .executeOn(gameApp.scheduler.value)
  144. } yield fib
  145. def gameInit(
  146. tickEventBus: GameEventBus[TickEvent]
  147. ): Resource[UIO, Either[AppError, Fiber[Nothing, Unit]]] =
  148. for {
  149. r1 <- wire[GameAppResource].resource.evalMap(e =>
  150. IO.fromEither(e)
  151. .flatMap {
  152. case (gameApp -> gameAppFib) =>
  153. eval(tickEventBus, gameApp, gameAppFib)
  154. }
  155. .attempt
  156. )
  157. dirWatcher <- Resource.liftF(
  158. MonixDirectoryWatcher(
  159. os.pwd / "assets" / "scripts"
  160. ).hideErrors
  161. )
  162. sc <- scriptSystemResource
  163. obs = dirWatcher.doOnNext {
  164. case ModifyEvent(file, count) =>
  165. sc.request(ScriptCompiler.GetScript(os.Path(file.path), _, true))(
  166. 15.seconds
  167. ).toTask
  168. .void
  169. case _ => monix.eval.Task.unit
  170. }
  171. _ <- Resource.make(obs.completedL.toIO.hideErrors.start)(_.cancel)
  172. // _ <-
  173. // dirWatcher
  174. // .doOnNextF(event =>
  175. // Coeval(pprint.log(show"Received file event $event")).void
  176. // )
  177. // .completedL
  178. // .executeOn(schedulers.io.value)
  179. // .startAndForget
  180. // .toIO
  181. // .hideErrors
  182. } yield r1
  183. val program = for {
  184. // scriptSystem <- scriptSystemInit
  185. launchSignal <- Deferred[Task, Launcher.LauncherResult].hideErrors
  186. launcher <- new Launcher.Props(schedulers.fx, launchSignal).create
  187. launchResult <-
  188. launcher.init
  189. .use(_ => launchSignal.get)
  190. .hideErrors
  191. tickEventBus <-
  192. eventsModule.tickEventBus.hideErrorsWith(e => new Exception(e.toString))
  193. _ <-
  194. /**
  195. * User chose to quit
  196. */
  197. if (launchResult === LauncherResult.Exit)
  198. logger.infoU("Exiting")
  199. /**
  200. * User chose launch. Wait for game window to close
  201. */
  202. else
  203. gameInit(tickEventBus).use {
  204. case Right(fib) => fib.join >> Task.unit
  205. case Left(error) => IO.terminate(new Exception(error.toString))
  206. }.hideErrors
  207. } yield ()
  208. }
  209. /**
  210. * Class with all dependencies in one place for easy wiring
  211. */
  212. class MainAppDelegate(
  213. gameApp: GameApp,
  214. loggerL: Logger[Task],
  215. mainEventBus: GameEventBus[Event],
  216. playerEventBus: GameEventBus[PlayerEvent],
  217. tickEventBus: GameEventBus[TickEvent],
  218. inputManager: InputManager,
  219. assetManager: AssetManager,
  220. physicsSpace: PhysicsSpace,
  221. camera: Camera,
  222. viewPort: ViewPort,
  223. enqueueR: Function1[() => Unit, Unit],
  224. rootNode: RootNode,
  225. schedulers: Schedulers,
  226. jfxUI: JavaFxUI
  227. )(implicit
  228. spawnProtocol: ActorRef[SpawnProtocol.Command],
  229. timeout: Timeout,
  230. scheduler: AkkaScheduler
  231. ) {
  232. implicit val as = scheduler.value
  233. def init(
  234. // appScheduler: monix.execution.Scheduler
  235. // consoleStream: GenericConsoleStream[TextArea]
  236. ): IO[AppError, Unit] =
  237. for {
  238. _ <- loggerL.infoU("Initializing Systems")
  239. _ <- assetManager.registerLocator(
  240. os.rel / "assets" / "town.zip",
  241. classOf[ZipLocator]
  242. )
  243. // _ <- Task(consoleStream.println("text"))
  244. level <- DefaultGameLevel(assetManager, viewPort)
  245. _ <- level.addToGame(rootNode, physicsSpace)
  246. playerActor <- createPlayerController()
  247. // .onErrorRestart(3)
  248. _ <- wire[GameInputHandler.Props].begin
  249. // .onErrorRestart(3)
  250. johnActor <- createTestNpc("John")
  251. // _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))
  252. // _ <-
  253. // johnActor
  254. // .tellL(NpcActorSupervisor.Move(ImVector3f(-30, 0, 10)))
  255. // .delayExecution(2.seconds)
  256. _ <-
  257. rootNode.depthFirstTraversal
  258. .doOnNextF(spat => loggerL.debug(spat.getName).toTask)
  259. .completedL
  260. .toIO
  261. .hideErrors
  262. damageObs <-
  263. mainEventBus
  264. .askL[Observable[DamageEvent]](ObservableSubscription(_))
  265. .onErrorHandleWith(TimeoutError.from)
  266. _ <-
  267. damageObs
  268. .doOnNextF(event =>
  269. (loggerL.debug(show"Received Damage Event $event") >>
  270. (if (event.victimName === "PlayerNode")
  271. playerActor
  272. .askL(PlayerActor.TakeDamage(event.amount, _))
  273. .void
  274. .onErrorHandle { case ex: TimeoutException => () }
  275. else IO.unit)).toTask
  276. )
  277. .completedL
  278. .toIO
  279. .hideErrors
  280. .startAndForget
  281. // _ <-
  282. // Observable
  283. // .interval(1.second)
  284. // .doOnNextF(_ =>
  285. // playerActor
  286. // .askL(PlayerActorSupervisor.GetStatus)
  287. // .flatMap(s => loggerL.debug(show"Player actor status: $s"))
  288. // // .flatMap(s =>
  289. // // if (s == Status.Alive)
  290. // // playerActor
  291. // // .askL(PlayerActorSupervisor.CurrentStats )
  292. // // .flatMap(s => loggerL.debug(show"Got state $s"))
  293. // // else IO.unit
  294. // // )
  295. // .toTask
  296. // )
  297. // // .doOnNextF(_ =>
  298. // // playerActor
  299. // // .askL(PlayerActorSupervisor.GetStatus )
  300. // // .flatMap(s => loggerL.debug(show"Player actor status: $s"))
  301. // // .toTask
  302. // // )
  303. // .completedL
  304. // .toIO
  305. // .hideErrors
  306. // .startAndForget
  307. _ <-
  308. physicsSpace.collisionObservable
  309. // .filter(event =>
  310. // (for {
  311. // nodeA <- event.nodeA
  312. // nodeB <- event.nodeB
  313. // } yield nodeA.getName === "PlayerNode" && nodeB.getName === "John" ||
  314. // nodeB.getName === "PlayerNode" && nodeA.getName === "John")
  315. // .getOrElse(false)
  316. // )
  317. // .doOnNextF(event =>
  318. // loggerL
  319. // .debug(show"$event ${event.appliedImpulse()}")
  320. // .toTask
  321. // )
  322. .filter(_.nodeA.map(_.getName =!= "main-scene_node").getOrElse(false))
  323. .filter(_.nodeB.map(_.getName =!= "main-scene_node").getOrElse(false))
  324. .doOnNextF(event =>
  325. (for {
  326. victim <- Coeval(for {
  327. nodeA <- event.nodeA
  328. nodeB <- event.nodeB
  329. } yield if (nodeB.getName === "John") nodeA else nodeB)
  330. _ <- Coeval(
  331. victim.foreach { v =>
  332. pprint.log(show"emitted event ${v.getName}")
  333. mainEventBus ! EventBus.Publish(
  334. DamageEvent(
  335. "John",
  336. v.getName,
  337. CharacterStats.DamageHealth(10)
  338. ),
  339. "damageHandler"
  340. )
  341. }
  342. )
  343. } yield ()).void
  344. )
  345. .completedL
  346. .toIO
  347. .hideErrors
  348. .startAndForget
  349. // _ <-
  350. // IOUtils
  351. // .toIO(
  352. // rootNode
  353. // .observableBreadthFirst()
  354. // .doOnNext(spat => IOUtils.toTask(loggerL.debug(spat.getName())))
  355. // .completedL
  356. // )
  357. // .executeOn(appScheduler)
  358. // .startAndForget
  359. statsObs <-
  360. playerActor
  361. .askL(PlayerActor.GetStatsObservable(_))
  362. .onErrorHandleWith(TimeoutError.from)
  363. .flatten
  364. playerHud <-
  365. UIO
  366. .deferAction(implicit s =>
  367. UIO(new VBox {
  368. implicit val c = CompositeCancelable()
  369. hgrow = Priority.Always
  370. spacing = 10
  371. style = """-fx-background-color: rgba(0,0,0,0.7);"""
  372. stylesheets = Seq((os.rel / "main.css").toString)
  373. children = List(
  374. new HBox {
  375. spacing = 5
  376. children = List(
  377. new Label("Health") { textFill = Color.White },
  378. new Label("100") {
  379. text <-- statsObs
  380. .doOnNextF(i => loggerL.debug(show"Received stats: $i"))
  381. .map(_.hp.toInt.toString)
  382. textFill = Color.White
  383. },
  384. new JFXProgressBar {
  385. progress = 100
  386. minHeight = 10
  387. progress <-- statsObs
  388. .map(_.hp.toInt.toDouble / 100)
  389. c += statsObs
  390. .scanEval(me.Task.pure("green-bar")) {
  391. case (a, b) =>
  392. me.Task {
  393. pprint.log(show"Received $a $b")
  394. pprint.log(show"${styleClass.toString}")
  395. styleClass.removeAll(a)
  396. } >>
  397. me.Task.pure(
  398. (b.hp.toInt: @switch) match {
  399. case v if v > 80 => "green-bar"
  400. case v if v > 20 && v <= 80 => "yellow-bar"
  401. case _ => "red-bar"
  402. }
  403. )
  404. }
  405. .doOnNext(cls => me.Task(styleClass += cls))
  406. .subscribe()
  407. }
  408. )
  409. },
  410. new HBox {
  411. spacing = 5
  412. children = List(
  413. new Label("Stamina") {
  414. textFill = Color.White
  415. },
  416. new Label("100") {
  417. textFill = Color.White
  418. text <-- statsObs
  419. .doOnNextF(i => loggerL.debug(show"Received stats: $i"))
  420. .map(_.stamina.toInt.toString)
  421. },
  422. new JFXProgressBar {
  423. progress = 100
  424. minHeight = 10
  425. progress <-- statsObs.map(_.stamina.toInt.toDouble / 100)
  426. styleClass ++= Seq("green-bar")
  427. c += statsObs
  428. .scanEval(me.Task.pure("green-bar")) {
  429. case (a, b) =>
  430. me.Task {
  431. pprint.log(show"Received $a $b")
  432. pprint.log(show"${styleClass.toString}")
  433. styleClass.removeAll(a)
  434. } >>
  435. me.Task.pure(
  436. (b.stamina.toInt: @switch) match {
  437. case v if v > 80 => "green-bar"
  438. case v if v > 20 && v <= 40 => "yellow-bar"
  439. case _ => "red-bar"
  440. }
  441. )
  442. }
  443. .doOnNext(cls => me.Task(styleClass += cls))
  444. .subscribe()
  445. }
  446. )
  447. }
  448. )
  449. })
  450. )
  451. .executeOn(schedulers.fx.value)
  452. _ <- UIO(jfxUI += playerHud)
  453. } yield ()
  454. def createPlayerController(
  455. // appScheduler: monix.execution.Scheduler
  456. ): IO[AppError, PlayerActor.Ref] = {
  457. val playerPos = ImVector3f.Zero
  458. val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
  459. // val modelPath = os.rel / "Models" / "Oto" / "Oto.mesh.xml"
  460. val playerPhysicsControl =
  461. PlayerController.Defaults.defaultPlayerPhysicsControl
  462. for {
  463. playerModel <-
  464. assetManager
  465. .loadModelAs[Node](modelPath)
  466. .map(_.withRotate(0, FastMath.PI, 0))
  467. .tapEval(m => UIO(m.center()))
  468. .mapError(AppError.AssetManagerError)
  469. playerNode <- UIO(
  470. PlayerController.Defaults
  471. .defaultPlayerNode(
  472. playerPos,
  473. playerModel,
  474. playerPhysicsControl
  475. )
  476. )
  477. cameraPivotNode <- UIO(
  478. new Node(EntityIds.CameraPivot.value)
  479. .withControl(new FollowControl(playerNode))
  480. .taggedWith[PlayerController.Tags.PlayerCameraPivotNode]
  481. )
  482. camNode <- UIO(
  483. PlayerController.Defaults
  484. .defaultCamerNode(camera, playerPos)
  485. .taggedWith[PlayerController.Tags.PlayerCameraNode]
  486. )
  487. playerCameraEvents <-
  488. playerEventBus
  489. .askL[Observable[PlayerCameraEvent]](ObservableSubscription(_))
  490. .onErrorHandleWith(TimeoutError.from)
  491. _ <-
  492. inputManager
  493. .enumAnalogObservable(PlayerCameraInput)
  494. .sample(1.millis)
  495. .scan(new Quaternion) {
  496. case (rotationBuf, action) =>
  497. action.binding match {
  498. case PlayerCameraInput.CameraRotateLeft =>
  499. val rot = rotationBuf
  500. .fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
  501. cameraPivotNode.rotate(rot)
  502. rotationBuf
  503. case PlayerCameraInput.CameraRotateRight =>
  504. val rot = rotationBuf
  505. .fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
  506. cameraPivotNode.rotate(rot)
  507. rotationBuf
  508. case PlayerCameraInput.CameraRotateUp =>
  509. val rot = rotationBuf
  510. .fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
  511. cameraPivotNode.rotate(rot)
  512. rotationBuf
  513. case PlayerCameraInput.CameraRotateDown =>
  514. val rot = rotationBuf
  515. .fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
  516. cameraPivotNode.rotate(rot)
  517. rotationBuf
  518. }
  519. }
  520. .completedL
  521. .toIO
  522. .hideErrors
  523. .startAndForget
  524. sched <- UIO.pure(schedulers.async)
  525. fxSched <- UIO.pure(schedulers.fx)
  526. playerActor <- wire[PlayerController.Props].create
  527. } yield playerActor
  528. }
  529. def createTestNpc(
  530. // appScheduler: monix.execution.Scheduler,
  531. npcName: String
  532. ): IO[AppError, NpcActorSupervisor.Ref] = {
  533. val initialPos = ImVector3f(50, 5, 0)
  534. val npcPhysicsControl = new BetterCharacterControl(1.5f, 6f, 1f)
  535. // (1f, 2.1f, 10f)
  536. .withJumpForce(ImVector3f(0, 5f, 0))
  537. val npcActorTask = AkkaUtils.spawnActorL(
  538. new NpcActorSupervisor.Props(
  539. new NpcMovementActor.Props(
  540. enqueueR,
  541. initialPos,
  542. npcName,
  543. npcPhysicsControl
  544. ).behavior,
  545. npcName,
  546. initialPos
  547. ).behavior,
  548. actorName = Some(show"${npcName}-npcActorSupervisor")
  549. )
  550. (for {
  551. materialDef <-
  552. assetManager
  553. .loadAssetAs[MaterialDef](
  554. os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md"
  555. )
  556. .mapError(AppError.AssetManagerError)
  557. material = new Material(materialDef)
  558. _ = material.setColor("Color", ColorRGBA.Blue)
  559. mesh = PlayerController.Defaults.defaultMesh.withMaterial(material)
  560. npcNode = PlayerController.Defaults.defaultNpcNode(
  561. mesh,
  562. initialPos,
  563. npcPhysicsControl,
  564. npcName
  565. )
  566. _ <- (for {
  567. _ <- physicsSpace += npcPhysicsControl
  568. _ <- physicsSpace += npcNode
  569. _ <- rootNode += npcNode
  570. } yield ()).mapError(AppError.AppNodeError)
  571. npcActor <- npcActorTask
  572. } yield npcActor)
  573. }
  574. }