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.
 
 

325 lines
11 KiB

package wow.doge.mygame
import akka.actor.typed.ActorRef
import akka.actor.typed.ActorSystem
import akka.actor.typed.Scheduler
import akka.actor.typed.SpawnProtocol
import akka.util.Timeout
import cats.effect.concurrent.Deferred
import com.jme3.app.state.AppStateManager
import com.jme3.asset.AssetManager
import com.jme3.asset.plugins.ZipLocator
import com.jme3.bullet.BulletAppState
import com.jme3.bullet.control.BetterCharacterControl
import com.jme3.input.InputManager
import com.jme3.renderer.Camera
import com.jme3.renderer.RenderManager
import com.jme3.renderer.ViewPort
import com.jme3.scene.Node
import com.jme3.scene.control.AbstractControl
import com.softwaremill.macwire._
import com.softwaremill.tagging._
import io.odin.Logger
import monix.bio.Fiber
import monix.bio.IO
import monix.bio.Task
import monix.execution.exceptions.DummyException
import scalafx.scene.control.TextArea
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.game.GameApp
import wow.doge.mygame.game.GameAppActor
import wow.doge.mygame.game.GameAppTags
import wow.doge.mygame.game.entities.EntityIds
import wow.doge.mygame.game.entities.NpcActorSupervisor
import wow.doge.mygame.game.entities.NpcMovementActor2
import wow.doge.mygame.game.entities.PlayerController
import wow.doge.mygame.game.entities.PlayerControllerTags
import wow.doge.mygame.game.subsystems.input.GameInputHandler
import wow.doge.mygame.game.subsystems.level.DefaultGameLevel
import wow.doge.mygame.implicits._
import wow.doge.mygame.launcher.Launcher
import wow.doge.mygame.launcher.Launcher.LauncherResult
import wow.doge.mygame.math.ImVector3f
import wow.doge.mygame.subsystems.events.EventBus
import wow.doge.mygame.subsystems.events.EventsModule
import wow.doge.mygame.subsystems.events.PlayerCameraEvent
import wow.doge.mygame.subsystems.events.PlayerMovementEvent
import wow.doge.mygame.subsystems.events.TickEvent
import wow.doge.mygame.subsystems.scriptsystem.ScriptInitMode
import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource
import wow.doge.mygame.utils.AkkaUtils
import wow.doge.mygame.utils.GenericConsoleStream
import wow.doge.mygame.utils.IOUtils
import EventsModule.GameEventBus
class MainApp(
logger: Logger[Task],
gameApp: GameApp,
implicit val spawnProtocol: ActorSystem[SpawnProtocol.Command],
jmeThread: monix.execution.Scheduler,
schedulers: Schedulers,
consoleStream: GenericConsoleStream[TextArea]
)(implicit
@annotation.unused timeout: Timeout,
@annotation.unused scheduler: Scheduler
) {
lazy val scriptSystemInit =
new ScriptSystemResource(os.pwd, spawnProtocol, ScriptInitMode.Eager).init
def gameInit: Task[Fiber[Throwable, Unit]] =
for {
eventsModule <- Task(new EventsModule(spawnProtocol))
playerMovementEventBus <- eventsModule.playerMovementEventBusTask
playerCameraEventBus <- eventsModule.playerCameraEventBusTask
mainEventBus <- eventsModule.mainEventBusTask
tickEventBus <- eventsModule.tickEventBusTask
gameAppActor <- AkkaUtils.spawnActorL2(
GameAppActor.Props(tickEventBus).create,
"gameAppActor"
)
_ <- gameAppActor !! GameAppActor.Start
gameAppFib <- gameApp.start.executeOn(jmeThread).start
/**
* schedule a task to run on the JME thread and wait for it's completion
* before proceeding forward, as a signal that the JME thread has been
* initialized, otherwise we'll get NPEs trying to access the fields
* of the game app
*/
res <- gameApp.enqueueL(() => "done")
_ <- logger.info(s"Result = $res")
/**
* JME Thread has been initialized at this point. We can now access the
* field of the game application
*/
inputManager <- gameApp.inputManager
assetManager <- gameApp.assetManager
stateManager <- gameApp.stateManager
camera <- gameApp.camera
rootNode <- gameApp.rootNode
enqueueR <- Task(gameApp.enqueue _)
viewPort <- gameApp.viewPort
_ <- logger.info("before")
// jfxUI <- gameApp.jfxUI
consoleTextArea <- Task(new TextArea {
text = "hello \n"
editable = false
wrapText = true
// maxHeight = 150
// maxWidth = 300
})
// _ <- Task(consoleStream := consoleTextArea)
// _ <- Task(jfxUI += consoleTextArea)
_ <- logger.info("after")
bulletAppState <- Task(new BulletAppState())
_ <- Task(stateManager.attach(bulletAppState))
_ <- logger.info("Initializing console stream")
_ <- wire[MainAppDelegate].init(gameApp.scheduler)
} yield (gameAppFib)
lazy val program = for {
scriptSystem <- scriptSystemInit
/**
* Signal for synchronization between the JavaFX launcher and the in-game JavaFX GUI
* Without this, we get a "Toolkit already initialized" exception. The launch button
* in the launcher completes the signal. The game init process which listens for this
* signal can then continue
*/
launchSignal <- Deferred[Task, Launcher.LauncherResult]
launcher <- new Launcher.Props(schedulers, launchSignal).create
cancelToken <- launcher.init()
launchResult <- launchSignal.get
_ <- cancelToken.cancel
_ <-
/**
* User chose to quit
*/
if (launchResult == LauncherResult.Exit)
logger.info("Exiting")
/**
* User chose launch. Wait for game window to close
*/
else
gameInit.flatMap(_.join)
} yield ()
}
/**
* Class with all dependencies in one place for easy wiring
*/
class MainAppDelegate(
gameApp: GameApp,
implicit val spawnProtocol: ActorSystem[SpawnProtocol.Command],
loggerL: Logger[Task],
playerMovementEventBus: ActorRef[
EventBus.Command[PlayerMovementEvent]
],
playerCameraEventBus: ActorRef[EventBus.Command[PlayerCameraEvent]],
tickEventBus: GameEventBus[TickEvent],
inputManager: InputManager,
assetManager: AssetManager,
stateManager: AppStateManager,
camera: Camera,
viewPort: ViewPort,
enqueueR: Function1[() => Unit, Unit],
rootNode: Node @@ GameAppTags.RootNode,
bulletAppState: BulletAppState
)(implicit
@annotation.unused timeout: Timeout,
@annotation.unused scheduler: Scheduler
) {
lazy val physicsSpace = bulletAppState.physicsSpace
def init(
appScheduler: monix.execution.Scheduler
// consoleStream: GenericConsoleStream[TextArea]
) =
for {
_ <- loggerL.info("Initializing Systems")
_ <- Task(
assetManager.registerLocator(
os.rel / "assets" / "town.zip",
classOf[ZipLocator]
)
)
_ <- loggerL.info("test")
// _ <- Task(consoleStream.println("text"))
_ <- DefaultGameLevel(assetManager, viewPort)
.addToGame(
rootNode,
bulletAppState.physicsSpace
)
.executeOn(appScheduler)
_ <- createPlayerController(appScheduler)
.absorbWith(e => DummyException("boom"))
.onErrorRestart(3)
_ <- wire[GameInputHandler.Props].begin.onErrorRestart(3)
// johnActor <- createTestNpc(appScheduler, "John").executeOn(appScheduler)
// _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))
// _ <- (johnActor !! NpcActorSupervisor.Move(
// ImVector3f(-80, 0, 100)
// )).executeAsync.delayExecution(2.seconds)
_ <-
IOUtils
.toIO(
rootNode
.observableBreadthFirst()
.doOnNext(spat => IOUtils.toTask(loggerL.debug(spat.getName())))
.completedL
)
.executeOn(appScheduler)
.startAndForget
} yield ()
def createPlayerController(
appScheduler: monix.execution.Scheduler
): IO[PlayerController.Error, Unit] = {
val playerPos = ImVector3f.ZERO
val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
lazy val playerPhysicsControl =
PlayerController.Defaults.defaultPlayerPhysicsControl
.taggedWith[PlayerControllerTags.PlayerTag]
// lazy val camNode =
// PlayerController.Defaults
// .defaultCamerNode(camera, playerPos)
// .taggedWith[PlayerControllerTags.PlayerCameraNode]
lazy val mbPlayerNode = PlayerController.Defaults
.defaultPlayerNode(
assetManager,
modelPath,
playerPos,
// camNode
playerPhysicsControl
)
lazy val cameraPivotNode = new Node(EntityIds.CameraPivot.value)
.taggedWith[PlayerControllerTags.PlayerCameraPivotNode]
for {
playerNode <- IO.fromEither(mbPlayerNode)
_ <- IO(cameraPivotNode.addControl(new FollowControl(playerNode)))
.onErrorHandleWith(e =>
IO.raiseError(PlayerController.GenericError(e.getMessage()))
)
camNode <- IO(
PlayerController.Defaults
.defaultCamerNode(camera, cameraPivotNode, playerNode, playerPos)
).onErrorHandleWith(e =>
IO.raiseError(PlayerController.GenericError(e.getMessage()))
).map(_.taggedWith[PlayerControllerTags.PlayerCameraNode])
// _ <- Task {
// val chaseCam = new ChaseCamera(camera, playerNode, inputManager)
// chaseCam.setSmoothMotion(false)
// chaseCam.setLookAtOffset(new Vector3f(0, 1.5f, 10))
// chaseCam
// }
// .onErrorHandleWith(e =>
// IO.raiseError(PlayerController.GenericError(e.getMessage()))
// )
_ <- wire[PlayerController.Props].create
} yield ()
}
def createTestNpc(
appScheduler: monix.execution.Scheduler,
npcName: String
) =
// : IO[PlayerController.Error, Unit] =
{
val initialPos = ImVector3f(100, 0, 0)
// val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
lazy val npcPhysicsControl =
new BetterCharacterControl(1f, 2.1f, 10f)
// .withJumpForce(ImVector3f(0, 5f, 0))
// val npcMovementActor = AkkaUtils.spawnActorL2(
// new NpcMovementActor2.Props(
// initialPos,
// tickEventBus,
// npcPhysicsControl
// ).create,
// s"${npcName}-npcMovementActor"
// )
lazy val mbNpcNode = PlayerController.Defaults.defaultNpcNode(
assetManager,
initialPos,
npcPhysicsControl,
npcName
)
val npcActorTask = AkkaUtils.spawnActorL2(
NpcActorSupervisor
.Props(
new NpcMovementActor2.Props(
enqueueR,
initialPos,
// tickEventBus,
npcPhysicsControl
).create,
npcName,
initialPos
)
.create,
s"${npcName}-npcActorSupervisor"
)
// .taggedWith[PlayerControllerTags.PlayerTag]
for {
npcNode <- IO.fromEither(mbNpcNode)
npcActor <- npcActorTask
_ <- IO {
physicsSpace += npcPhysicsControl
physicsSpace += npcNode
rootNode += npcNode
}
} yield (npcActor)
}
}
class FollowControl(playerNode: Node) extends AbstractControl {
override def controlUpdate(tpf: Float): Unit = {
this.spatial.setLocalTranslation(playerNode.getLocalTranslation())
}
override def controlRender(
rm: RenderManager,
vp: ViewPort
): Unit = {}
}