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.
 
 

336 lines
11 KiB

package wow.doge.mygame
import java.util.concurrent.TimeoutException
import scala.concurrent.duration._
import akka.actor.typed.ActorRef
import akka.actor.typed.Scheduler
import akka.actor.typed.SpawnProtocol
import akka.util.Timeout
import cats.effect.Resource
import cats.effect.concurrent.Deferred
import cats.syntax.eq._
import com.jme3.asset.plugins.ZipLocator
import com.jme3.bullet.control.BetterCharacterControl
import com.jme3.input.InputManager
import com.jme3.material.Material
import com.jme3.material.MaterialDef
import com.jme3.math.ColorRGBA
import com.jme3.math.FastMath
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.bio.UIO
import monix.eval.Coeval
import monix.reactive.Observable
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.GameAppResource
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.NpcMovementActor
import wow.doge.mygame.game.entities.PlayerActorSupervisor
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.ObservableSubscription
import wow.doge.mygame.subsystems.events.EventsModule
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
import wow.doge.mygame.subsystems.events.PlayerEvent
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 wow.doge.mygame.utils.wrappers.jme.AppNode2
import wow.doge.mygame.utils.wrappers.jme.AssetManager
import wow.doge.mygame.utils.wrappers.jme.PhysicsSpace
class MainApp(
logger: Logger[Task],
jmeThread: monix.execution.Scheduler,
schedulers: Schedulers,
consoleStream: GenericConsoleStream[TextArea]
)(implicit
spawnProtocol: ActorRef[SpawnProtocol.Command],
timeout: Timeout,
scheduler: Scheduler
) {
val scriptSystemInit =
new ScriptSystemResource(os.pwd, ScriptInitMode.Eager).init
val eventsModule = new EventsModule(scheduler, spawnProtocol)
class TestClass(
playerEventBus: GameEventBus[PlayerEvent],
tickEventBus: GameEventBus[TickEvent]
)
def eval(gameApp: GameApp, fib: Fiber[Nothing, Unit]) =
for {
// g <- UIO.pure(gameApp)
playerEventBus <- eventsModule.playerEventBus
mainEventBus <- eventsModule.mainEventBus
tickEventBus <- eventsModule.tickEventBus
obs <-
playerEventBus
.askL[Observable[PlayerMovementEvent]](
ObservableSubscription(_)
)
.onErrorHandleWith {
case ex: TimeoutException =>
IO.raiseError(AppError.TimeoutError(ex.getMessage()))
}
_ <-
IOUtils
.toIO(
obs
.doOnNextF(pme => Coeval(pprint.log(s"Received event $pme")).void)
.completedL
.startAndForget
)
.hideErrors
inputManager <- gameApp.inputManager
assetManager <- UIO.pure(gameApp.assetManager)
camera <- gameApp.camera
rootNode <- UIO.pure(gameApp.rootNode)
enqueueR <- UIO(gameApp.enqueue _)
viewPort <- gameApp.viewPort
physicsSpace <- UIO.pure(gameApp.physicsSpace)
_ <- logger.infoU("before")
// jfxUI <- gameApp.jfxUI
gameAppActor <- gameApp.spawnGameActor(
GameAppActor.Props(tickEventBus).behavior,
"gameAppActor"
)
_ <- gameAppActor !! GameAppActor.Start
consoleTextArea <- UIO(new TextArea {
text = "hello \n"
editable = false
wrapText = true
// maxHeight = 150
// maxWidth = 300
})
// _ <- Task(consoleStream := consoleTextArea)
// _ <- Task(jfxUI += consoleTextArea)
_ <- logger.infoU("after")
_ <- logger.infoU("Initializing console stream")
_ <-
wire[MainAppDelegate]
.init()
.executeOn(gameApp.scheduler)
} yield fib
def gameInit: Resource[UIO, Either[AppError, Fiber[Nothing, Unit]]] =
wire[GameAppResource].resource.evalMap {
case Right(gameApp -> gameAppFib) =>
eval(gameApp, gameAppFib).attempt
case Left(error) => IO.terminate(new Exception(error.toString))
}
// val x: Task[Unit] = for {
// tickEventBus <- eventsModule.tickEventBusTask
// playerEventBus <- eventsModule.playerEventBusTask
// _ <- UIO(wire[TestClass])
// _ <- gameInit(tickEventBus).use(_.join)
// } yield ()
val program = for {
// scriptSystem <- scriptSystemInit
launchSignal <- Deferred[Task, Launcher.LauncherResult].hideErrors
launcher <- new Launcher.Props(schedulers, launchSignal).create
launchResult <- launcher.init.use(_ => launchSignal.get).hideErrors
_ <-
/**
* User chose to quit
*/
if (launchResult === LauncherResult.Exit)
logger.infoU("Exiting")
/**
* User chose launch. Wait for game window to close
*/
else
gameInit.use {
case Right(fib) => fib.join >> Task.unit
case Left(error) => IO.terminate(new Exception(error.toString))
}.hideErrors
} yield ()
}
/**
* Class with all dependencies in one place for easy wiring
*/
class MainAppDelegate(
gameApp: GameApp,
loggerL: Logger[Task],
playerEventBus: GameEventBus[PlayerEvent],
tickEventBus: GameEventBus[TickEvent],
inputManager: InputManager,
assetManager: AssetManager,
physicsSpace: PhysicsSpace,
camera: Camera,
viewPort: ViewPort,
enqueueR: Function1[() => Unit, Unit],
rootNode: AppNode2 @@ GameAppTags.RootNode
)(implicit
spawnProtocol: ActorRef[SpawnProtocol.Command],
timeout: Timeout,
scheduler: Scheduler
) {
def init(
// appScheduler: monix.execution.Scheduler
// consoleStream: GenericConsoleStream[TextArea]
): IO[AppError, Unit] =
for {
_ <- loggerL.infoU("Initializing Systems")
_ <- assetManager.registerLocator(
os.rel / "assets" / "town.zip",
classOf[ZipLocator]
)
_ <- loggerL.infoU("test")
// _ <- Task(consoleStream.println("text"))
level <- DefaultGameLevel(assetManager, viewPort)
_ <- level.addToGame(rootNode, physicsSpace)
_ <- createPlayerController()
// .onErrorRestart(3)
_ <- wire[GameInputHandler.Props].begin
// .onErrorRestart(3)
johnActor <- createTestNpc("John")
// _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))
_ <-
johnActor
.tellL(NpcActorSupervisor.Move(ImVector3f(-30, 0, 10)))
.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[AppError, ActorRef[PlayerActorSupervisor.Command]] = {
val playerPos = ImVector3f.Zero
val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
val playerPhysicsControl =
PlayerController.Defaults.defaultPlayerPhysicsControl
.taggedWith[PlayerControllerTags.PlayerTag]
for {
playerModel <-
assetManager
.loadModelAs[Node](modelPath)
.map(_.withRotate(0, FastMath.PI, 0))
.mapError(AppError.AssetManagerError)
playerNode <- UIO(
PlayerController.Defaults
.defaultPlayerNode(
playerPos,
playerModel,
playerPhysicsControl
)
)
cameraPivotNode <- UIO(
new Node(EntityIds.CameraPivot.value)
.withControl(new FollowControl(playerNode))
.taggedWith[PlayerControllerTags.PlayerCameraPivotNode]
)
camNode <- UIO(
PlayerController.Defaults
.defaultCamerNode(camera, playerPos)
.taggedWith[PlayerControllerTags.PlayerCameraNode]
)
playerActor <- wire[PlayerController.Props].create
} yield playerActor
}
def createTestNpc(
// appScheduler: monix.execution.Scheduler,
npcName: String
): IO[AppError, ActorRef[NpcActorSupervisor.Command]] = {
val initialPos = ImVector3f(50, 5, 0)
val npcPhysicsControl = new BetterCharacterControl(1.5f, 6f, 1f)
// (1f, 2.1f, 10f)
.withJumpForce(ImVector3f(0, 5f, 0))
val npcActorTask = AkkaUtils.spawnActorL(
NpcActorSupervisor
.Props(
new NpcMovementActor.Props(
enqueueR,
initialPos,
// tickEventBus,
npcName,
npcPhysicsControl
).behavior,
npcName,
initialPos
)
.behavior,
s"${npcName}-npcActorSupervisor"
)
(for {
materialDef <-
assetManager
.loadAssetAs[MaterialDef](
os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md"
)
.mapError(AppError.AssetManagerError)
material = new Material(materialDef)
_ = material.setColor("Color", ColorRGBA.Blue)
mesh = PlayerController.Defaults.defaultMesh.withMaterial(material)
npcNode = PlayerController.Defaults.defaultNpcNode(
mesh,
initialPos,
npcPhysicsControl,
npcName
)
_ <- (for {
_ <- physicsSpace += npcPhysicsControl
_ <- physicsSpace += npcNode
_ <- rootNode += npcNode
} yield ()).mapError(AppError.AppNodeError)
npcActor <- npcActorTask
} 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 = {}
}