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.
 
 

488 lines
16 KiB

package wow.doge.mygame
import scala.concurrent.duration._
import akka.actor.typed.ActorRef
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.math.Quaternion
import com.jme3.math.Vector3f
import com.jme3.renderer.Camera
import com.jme3.renderer.ViewPort
import com.jme3.scene.Node
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.execution.exceptions.DummyException
import monix.reactive.Observable
import scalafx.scene.control.TextArea
import wow.doge.mygame.AppError.TimeoutError
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.game.GameApp
import wow.doge.mygame.game.GameAppResource
import wow.doge.mygame.game.controls.FollowControl
import wow.doge.mygame.game.entities.CharacterStats
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.subsystems.input.GameInputHandler
import wow.doge.mygame.game.subsystems.input.PlayerCameraInput
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.Event
import wow.doge.mygame.subsystems.events.EventBus
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.PlayerCameraEvent
import wow.doge.mygame.subsystems.events.PlayerEvent
import wow.doge.mygame.subsystems.events.PlayerMovementEvent
import wow.doge.mygame.subsystems.events.StatsEvent.DamageEvent
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.types._
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.AssetManager
import wow.doge.mygame.utils.wrappers.jme.PhysicsSpace
class MainApp(
logger: Logger[Task],
jmeThread: JmeScheduler,
schedulers: Schedulers,
consoleStream: GenericConsoleStream[TextArea]
)(implicit
spawnProtocol: ActorRef[SpawnProtocol.Command],
timeout: Timeout,
scheduler: AkkaScheduler
) {
implicit val as = scheduler.value
val scriptSystemInit =
new ScriptSystemResource(os.pwd, ScriptInitMode.Eager).init
val eventsModule = new EventsModule(scheduler, spawnProtocol)
def eval(
tickEventBus: GameEventBus[TickEvent],
gameApp: GameApp,
fib: Fiber[Nothing, Unit]
) =
for {
// g <- UIO.pure(gameApp)
playerEventBus <- eventsModule.playerEventBus
mainEventBus <- eventsModule.mainEventBus
obs <-
playerEventBus
.askL[Observable[PlayerMovementEvent]](ObservableSubscription(_))
.onErrorHandleWith(TimeoutError.from)
_ <-
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
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.value)
} yield fib
def gameInit(
tickEventBus: GameEventBus[TickEvent]
): Resource[UIO, Either[AppError, Fiber[Nothing, Unit]]] =
wire[GameAppResource].resource.evalMap {
case Right(gameApp -> gameAppFib) =>
eval(tickEventBus, gameApp, gameAppFib).attempt
case Left(error) => IO.terminate(new Exception(error.toString))
}
val program = for {
// scriptSystem <- scriptSystemInit
launchSignal <- Deferred[Task, Launcher.LauncherResult].hideErrors
launcher <- new Launcher.Props(schedulers.fx, launchSignal).create
launchResult <-
launcher.init
.use(_ => launchSignal.get)
.hideErrors
tickEventBus <-
eventsModule.tickEventBus.hideErrorsWith(e => DummyException(e.toString))
_ <-
/**
* User chose to quit
*/
if (launchResult === LauncherResult.Exit)
logger.infoU("Exiting")
/**
* User chose launch. Wait for game window to close
*/
else
gameInit(tickEventBus).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],
mainEventBus: GameEventBus[Event],
playerEventBus: GameEventBus[PlayerEvent],
tickEventBus: GameEventBus[TickEvent],
inputManager: InputManager,
assetManager: AssetManager,
physicsSpace: PhysicsSpace,
camera: Camera,
viewPort: ViewPort,
enqueueR: Function1[() => Unit, Unit],
rootNode: RootNode,
schedulers: Schedulers
)(implicit
spawnProtocol: ActorRef[SpawnProtocol.Command],
timeout: Timeout,
scheduler: AkkaScheduler
) {
implicit val as = scheduler.value
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)
playerActor <- 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)
_ <-
rootNode.depthFirstTraversal
.doOnNextF(spat => loggerL.debug(spat.getName).toTask)
.completedL
.toIO
.hideErrors
damageObs <-
mainEventBus
.askL[Observable[DamageEvent]](ObservableSubscription(_))
.onErrorHandleWith(TimeoutError.from)
_ <-
damageObs
.doOnNextF(event =>
(loggerL.debug(s"Received Damage Event $event") >>
(if (event.victimName === "PlayerNode")
// playerActor !! PlayerActorSupervisor.TakeDamage(event.amount)
playerActor.askL(
PlayerActorSupervisor.TakeDamage2(event.amount, _)
)
else IO.unit)).toTask
)
.completedL
.toIO
.hideErrors
.startAndForget
_ <-
Observable
.interval(1.second)
.doOnNextF(_ =>
playerActor
.askL(PlayerActorSupervisor.GetStatus)
.flatMap(s => loggerL.debug(s"Player actor status: $s"))
// .flatMap(s =>
// if (s == Status.Alive)
// playerActor
// .askL(PlayerActorSupervisor.CurrentStats )
// .flatMap(s => loggerL.debug(s"Got state $s"))
// else IO.unit
// )
.toTask
)
// .doOnNextF(_ =>
// playerActor
// .askL(PlayerActorSupervisor.GetStatus )
// .flatMap(s => loggerL.debug(s"Player actor status: $s"))
// .toTask
// )
.completedL
.toIO
.hideErrors
.startAndForget
_ <-
physicsSpace.collisionObservable
// .filter(event =>
// (for {
// nodeA <- event.nodeA
// nodeB <- event.nodeB
// } yield nodeA.getName === "PlayerNode" && nodeB.getName === "John" ||
// nodeB.getName === "PlayerNode" && nodeA.getName === "John")
// .getOrElse(false)
// )
// .doOnNextF(event =>
// loggerL
// .debug(s"$event ${event.appliedImpulse()}")
// .toTask
// )
.filter(_.nodeA.map(_.getName =!= "main-scene_node").getOrElse(false))
.filter(_.nodeB.map(_.getName =!= "main-scene_node").getOrElse(false))
.doOnNextF(event =>
(for {
victim <- Coeval(for {
nodeA <- event.nodeA
nodeB <- event.nodeB
} yield if (nodeB.getName === "John") nodeA else nodeB)
_ <- Coeval(
victim.foreach { v =>
pprint.log(s"emitted event ${v.getName}")
mainEventBus ! EventBus.Publish(
DamageEvent(
"John",
v.getName,
CharacterStats.DamageHealth(10)
),
"damageHandler"
)
}
)
} yield ()).void
)
.completedL
.toIO
.hideErrors
.startAndForget
// _ <-
// 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, PlayerActorSupervisor.Ref] = {
val playerPos = ImVector3f.Zero
val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
// val modelPath = os.rel / "Models" / "Oto" / "Oto.mesh.xml"
val playerPhysicsControl =
PlayerController.Defaults.defaultPlayerPhysicsControl
for {
playerModel <-
assetManager
.loadModelAs[Node](modelPath)
.map(_.withRotate(0, FastMath.PI, 0))
.tapEval(m => UIO(m.center()))
.mapError(AppError.AssetManagerError)
playerNode <- UIO(
PlayerController.Defaults
.defaultPlayerNode(
playerPos,
playerModel,
playerPhysicsControl
)
)
cameraPivotNode <- UIO(
new Node(EntityIds.CameraPivot.value)
.withControl(new FollowControl(playerNode))
.taggedWith[PlayerController.Tags.PlayerCameraPivotNode]
)
camNode <- UIO(
PlayerController.Defaults
.defaultCamerNode(camera, playerPos)
.taggedWith[PlayerController.Tags.PlayerCameraNode]
)
playerCameraEvents <-
playerEventBus
.askL[Observable[PlayerCameraEvent]](ObservableSubscription(_))
.onErrorHandleWith(TimeoutError.from)
_ <-
inputManager
.enumAnalogObservable(PlayerCameraInput)
.sample(1.millis)
.scan(new Quaternion) {
case (rotationBuf, action) =>
action.binding match {
// case PlayerCameraEvent.CameraLeft =>
case PlayerCameraInput.CameraRotateLeft =>
// me.Task {
val rot = rotationBuf
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
cameraPivotNode.rotate(rot)
rotationBuf
// }
// case PlayerCameraEvent.CameraRight =>
case PlayerCameraInput.CameraRotateRight =>
// me.Task {
val rot = rotationBuf
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
cameraPivotNode.rotate(rot)
rotationBuf
// }
// case PlayerCameraEvent.CameraMovedUp =>
case PlayerCameraInput.CameraRotateUp =>
// me.Task {
val rot = rotationBuf
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
cameraPivotNode.rotate(rot)
rotationBuf
// }
// case PlayerCameraEvent.CameraMovedDown =>
case PlayerCameraInput.CameraRotateDown =>
// me.Task {
val rot = rotationBuf
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
cameraPivotNode.rotate(rot)
rotationBuf
// }
}
}
.completedL
.toIO
.hideErrors
.startAndForget
// _ <-
// Observable
// .interval(10.millis)
// .doOnNextF(_ =>
// Coeval {
// val location = playerNode.getWorldTranslation()
// cameraPivotNode.setLocalTranslation(location)
// }
// )
// .completedL
// .toIO
// .hideErrors
// .startAndForget
sched <- UIO.pure(schedulers.async)
playerActor <- wire[PlayerController.Props].create
obs <-
playerActor
.askL(PlayerActorSupervisor.GetStatsObservable2)
.onErrorHandleWith(TimeoutError.from)
_ <-
obs
.doOnNext(s => loggerL.debug(s"Got state $s").toTask)
.completedL
.toIO
.hideErrors
.startAndForget
} yield playerActor
}
def createTestNpc(
// appScheduler: monix.execution.Scheduler,
npcName: String
): IO[AppError, NpcActorSupervisor.Ref] = {
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(
new NpcActorSupervisor.Props(
new NpcMovementActor.Props(
enqueueR,
initialPos,
npcName,
npcPhysicsControl
).behavior,
npcName,
initialPos
).behavior,
actorName = Some(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)
}
}