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.
 
 

613 lines
21 KiB

package wow.doge.mygame
import java.util.concurrent.TimeoutException
import scala.annotation.switch
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 cats.syntax.show._
import com.jayfella.jme.jfx.JavaFxUI
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.cancelables.CompositeCancelable
import monix.reactive.Observable
import monix.{eval => me}
import scalafx.scene.control.Label
import scalafx.scene.control.TextArea
import scalafx.scene.layout.HBox
import scalafx.scene.layout.Priority
import scalafx.scene.layout.VBox
import scalafx.scene.paint.Color
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.player.PlayerActor
import wow.doge.mygame.game.entities.player.PlayerController
import wow.doge.mygame.game.entities.player.PlayerMovementReducer
import wow.doge.mygame.game.subsystems.input.InputMappings
import wow.doge.mygame.game.subsystems.input.PlayerCameraInput
import wow.doge.mygame.game.subsystems.input.PlayerMovementInput
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.ScriptCompiler
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.MonixDirectoryWatcher
import wow.doge.mygame.utils.MonixDirectoryWatcher.ModifyEvent
import wow.doge.mygame.utils.controls.JFXProgressBar
import wow.doge.mygame.utils.wrappers.jme
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 scriptSystemResource: Resource[UIO, ScriptCompiler] =
new ScriptSystemResource(os.pwd, logger, ScriptInitMode.Eager).init2
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 =>
logger.trace(show"Received event $pme").toTask.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.hideErrors
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]]] =
for {
r1 <- wire[GameAppResource].resource.evalMap(e =>
IO.fromEither(e)
.flatMap {
case (gameApp -> gameAppFib) =>
eval(tickEventBus, gameApp, gameAppFib)
}
.attempt
)
dirWatcher <- Resource.liftF(
MonixDirectoryWatcher(
os.pwd / "assets" / "scripts"
).hideErrors
)
sc <- scriptSystemResource
obs = dirWatcher.doOnNext {
case ModifyEvent(file, count) =>
sc.request(ScriptCompiler.GetScript(os.Path(file.path), _, true))(
15.seconds
).toTask
.void
case _ => me.Task.unit
}
_ <- Resource.make(obs.completedL.toIO.hideErrors.start)(_.cancel)
// _ <-
// dirWatcher
// .doOnNextF(event =>
// Coeval(pprint.log(show"Received file event $event")).void
// )
// .completedL
// .executeOn(schedulers.io.value)
// .startAndForget
// .toIO
// .hideErrors
} yield r1
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 => new Exception(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,
jfxUI: JavaFxUI
)(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]
)
// _ <- Task(consoleStream.println("text"))
level <- DefaultGameLevel(assetManager, viewPort)
_ <- level.addToGame(rootNode, physicsSpace)
playerActor <- createPlayerController()
// .onErrorRestart(3)
// _ <- wire[GameInputHandler.Props].begin
_ <- new InputMappings(new jme.InputManager(inputManager)).setup
// .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(show"Received Damage Event $event") >>
(if (event.victimName === "PlayerNode")
playerActor
.askL(PlayerActor.TakeDamage(event.amount, _))
.void
.onErrorHandle { case ex: TimeoutException => () }
else IO.unit)).toTask
)
.completedL
.toIO
.hideErrors
.startAndForget
// _ <-
// Observable
// .interval(1.second)
// .doOnNextF(_ =>
// playerActor
// .askL(PlayerActorSupervisor.GetStatus)
// .flatMap(s => loggerL.debug(show"Player actor status: $s"))
// // .flatMap(s =>
// // if (s == Status.Alive)
// // playerActor
// // .askL(PlayerActorSupervisor.CurrentStats )
// // .flatMap(s => loggerL.debug(show"Got state $s"))
// // else IO.unit
// // )
// .toTask
// )
// // .doOnNextF(_ =>
// // playerActor
// // .askL(PlayerActorSupervisor.GetStatus )
// // .flatMap(s => loggerL.debug(show"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(show"$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(show"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
statsObs <-
playerActor
.askL(PlayerActor.GetStatsObservable(_))
.onErrorHandleWith(TimeoutError.from)
.flatten
playerHud <-
UIO
.deferAction(implicit s =>
UIO(new VBox {
implicit val c = CompositeCancelable()
hgrow = Priority.Always
spacing = 10
style = """-fx-background-color: rgba(0,0,0,0.7);"""
stylesheets = Seq((os.rel / "main.css").toString)
children = List(
new HBox {
spacing = 5
children = List(
new Label("Health") { textFill = Color.White },
new Label("100") {
text <-- statsObs
.doOnNextF(i => loggerL.trace(show"Received stats: $i"))
.map(_.hp.toInt.toString)
textFill = Color.White
},
new JFXProgressBar {
progress = 100
minHeight = 10
progress <-- statsObs
.map(_.hp.toInt.toDouble / 100)
c += statsObs
.scanEval(me.Task.pure("green-bar")) {
case (a, b) =>
me.Task(styleClass.removeAll(a)) >>
me.Task.pure(
(b.hp.toInt: @switch) match {
case v if v > 80 => "green-bar"
case v if v > 20 && v <= 80 => "yellow-bar"
case _ => "red-bar"
}
)
}
.doOnNext(cls => me.Task(styleClass += cls))
.subscribe()
}
)
},
new HBox {
spacing = 5
children = List(
new Label("Stamina") {
textFill = Color.White
},
new Label("100") {
textFill = Color.White
text <-- statsObs
.doOnNextF(i => loggerL.trace(show"Received stats: $i"))
.map(_.stamina.toInt.toString)
},
new JFXProgressBar {
progress = 100
minHeight = 10
progress <-- statsObs.map(_.stamina.toInt.toDouble / 100)
styleClass ++= Seq("green-bar")
c += statsObs
.scanEval(me.Task.pure("green-bar")) {
case (a, b) =>
me.Task(styleClass.removeAll(a)) >>
me.Task.pure(
(b.stamina.toInt: @switch) match {
case v if v > 80 => "green-bar"
case v if v > 20 && v <= 40 => "yellow-bar"
case _ => "red-bar"
}
)
}
.doOnNext(cls => me.Task(styleClass += cls))
.subscribe()
}
)
}
)
})
)
.executeOn(schedulers.fx.value)
_ <- UIO(jfxUI += playerHud)
} yield ()
def createPlayerController(
// appScheduler: monix.execution.Scheduler
): IO[AppError, PlayerActor.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 PlayerCameraInput.CameraRotateLeft =>
val rot = rotationBuf
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
cameraPivotNode.rotate(rot)
rotationBuf
case PlayerCameraInput.CameraRotateRight =>
val rot = rotationBuf
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
cameraPivotNode.rotate(rot)
rotationBuf
case PlayerCameraInput.CameraRotateUp =>
val rot = rotationBuf
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
cameraPivotNode.rotate(rot)
rotationBuf
case PlayerCameraInput.CameraRotateDown =>
val rot = rotationBuf
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
cameraPivotNode.rotate(rot)
rotationBuf
}
}
.completedL
.toIO
.hideErrors
.startAndForget
sched <- UIO.pure(schedulers.async)
fxSched <- UIO.pure(schedulers.fx)
playerActor <- wire[PlayerController.Props].create
playerMovementReducer = new PlayerMovementReducer(playerActor, loggerL)
_ <-
inputManager
.enumObservableAction(PlayerMovementInput)
.sample(1.millis)
.scanEval(me.Task.pure(PlayerMovementReducer.State.empty))(
playerMovementReducer.value
)
.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(show"${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)
}
}