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.
 
 

268 lines
9.1 KiB

package wow.doge.mygame.game.entities
import scala.concurrent.duration._
import scala.util.Failure
import scala.util.Success
import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.LogOptions
import akka.actor.typed.PostStop
import akka.actor.typed.SupervisorStrategy
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors
import akka.util.Timeout
import cats.syntax.show._
import com.typesafe.scalalogging.Logger
import monix.eval.Coeval
import monix.execution.AsyncQueue
import monix.reactive.Observable
import monix.reactive.OverflowStrategy
import monix.reactive.subjects.ConcurrentSubject
import org.slf4j.event.Level
import wow.doge.mygame.Dispatchers
import wow.doge.mygame.executors.Schedulers.AsyncScheduler
import wow.doge.mygame.game.entities.StatsActor
import wow.doge.mygame.implicits._
import wow.doge.mygame.subsystems.events.EventBus
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
import wow.doge.mygame.subsystems.events.PlayerEvent
import wow.doge.mygame.subsystems.events.TickEvent
import wow.doge.mygame.subsystems.events.TickEvent.RenderTick
import wow.doge.mygame.subsystems.movement.ImMovementActor
object PlayerActorSupervisor {
type Ref = ActorRef[PlayerActorSupervisor.Command]
sealed trait Status
object Status {
case object Alive extends Status
case object Dead extends Status
}
sealed trait Command
final case class TakeDamage(
value: CharacterStats.DamageHealth,
replyTo: ActorRef[Unit]
) extends Command
final case class ConsumeStamina(
value: CharacterStats.DamageStamina,
replyTo: ActorRef[Unit]
) extends Command
final case class Heal(value: CharacterStats.HealHealth) extends Command
final case class CurrentStats(replyTo: ActorRef[CharacterStats])
extends Command
final case class GetStatus(replyTo: ActorRef[Status]) extends Command
final case class GetStatsObservable(
replyTo: ActorRef[Observable[CharacterStats]]
) extends Command
private case object Die extends Command
private final case class DamageResponse(
response: (Boolean, CharacterStats),
replyTo: ActorRef[Unit]
) extends Command
// private final case class InternalTakeDamage(old: Int, value: Int) extends Command
private final case class LogError(ex: Throwable) extends Command
class Props(
val playerEventBus: GameEventBus[PlayerEvent],
val tickEventBus: GameEventBus[TickEvent],
val imMovementActorBehavior: Behavior[ImMovementActor.Command],
val scheduler: AsyncScheduler
) {
def behavior =
Behaviors.logMessages(
LogOptions()
.withLevel(Level.DEBUG)
.withLogger(
Logger[PlayerActorSupervisor].underlying
),
Behaviors
.setup[Command] { ctx =>
ctx.log.infoP("Starting PlayerActor")
// spawn children actors
val playerStatsActor =
ctx.spawnN(
new StatsActor.Props(
CharacterStats.Health(100),
CharacterStats.Stamina(100)
).behavior
)
val playerMovementActor =
ctx.spawnN(
Behaviors
.supervise(imMovementActorBehavior)
.onFailure[Exception](
SupervisorStrategy.restart.withLimit(2, 100.millis)
),
Dispatchers.jmeDispatcher
)
val playerMovementEl = ctx.spawnN(
Behaviors
.supervise(
new PlayerMovementEventListener.Props(
playerMovementActor,
ctx.self,
scheduler
).behavior
)
.onFailure[Exception](
SupervisorStrategy.restart.withLimit(2, 100.millis)
)
)
val renderTickEl = {
val behavior: Behavior[RenderTick.type] =
Behaviors.setup(ctx =>
Behaviors
.receiveMessage[RenderTick.type] {
case RenderTick =>
playerMovementActor ! ImMovementActor.Tick
// playerCameraActor ! PlayerCameraActor.Tick
Behaviors.same
}
.receiveSignal {
case (_, PostStop) =>
ctx.log.infoP("stopped")
Behaviors.same
}
)
ctx.spawn(behavior, "playerMovementTickListener")
}
//init listeners
playerEventBus ! EventBus.Subscribe(playerMovementEl)
tickEventBus ! EventBus.Subscribe(renderTickEl)
new PlayerActorSupervisor(
ctx,
this,
Children(playerMovementActor, playerStatsActor),
ConcurrentSubject.publish(
OverflowStrategy.DropOldAndSignal(
50,
dropped => Coeval.pure(None)
)
)(scheduler.value),
AsyncQueue.bounded(10)(scheduler.value)
).aliveState
}
)
}
final case class Children(
movementActor: ActorRef[ImMovementActor.Command],
statsActor: ActorRef[StatsActor.Command]
)
}
class PlayerActorSupervisor(
ctx: ActorContext[PlayerActorSupervisor.Command],
props: PlayerActorSupervisor.Props,
children: PlayerActorSupervisor.Children,
statsSubject: ConcurrentSubject[CharacterStats, CharacterStats],
statsQueue: AsyncQueue[CharacterStats]
) {
import PlayerActorSupervisor._
implicit val timeout = Timeout(1.second)
val aliveState =
Behaviors
.receiveMessage[Command] {
case TakeDamage(value, replyTo) =>
// children.movementActor ! ImMovementActor.MovedDown(true)
// ctx.ask(children.statsActor, StatsActor.CurrentStats(_)) {
// case Success(status) => InternalTakeDamage(status.hp, value)
// case Failure(ex) => LogError(ex)
// }
ctx.ask(children.statsActor, StatsActor.TakeDamageResult(value, _)) {
case Success(response) => DamageResponse(response, replyTo)
case Failure(ex) => LogError(ex)
}
Behaviors.same
case ConsumeStamina(value, replyTo) =>
ctx.ask(
children.statsActor,
StatsActor.ConsumeStaminaResult(value, _)
) {
case Success(response) => DamageResponse(response, replyTo)
case Failure(ex) => LogError(ex)
}
Behaviors.same
case CurrentStats(replyTo) =>
// ctx.ask(children.statsActor, StatsActor.CurrentStats())
children.statsActor ! StatsActor.CurrentStats(replyTo)
Behaviors.same
case Heal(value) =>
children.statsActor ! StatsActor.HealResult(value)
Behaviors.same
case GetStatus(replyTo) =>
replyTo ! Status.Alive
Behaviors.same
// case _ => Behaviors.unhandled
// case InternalTakeDamage(hp, damage) =>
// if (hp - damage <= 0) dead
// else {
// children.statsActor ! StatsActor.TakeDamage(damage)
// Behaviors.same
// }
case GetStatsObservable(replyTo) =>
import monix.{eval => me}
replyTo ! Observable.repeatEvalF(
me.Task.deferFuture(statsQueue.poll())
)
Behaviors.same
case DamageResponse(response, replyTo) =>
response match {
case (dead, stats) =>
if (dead) ctx.self ! Die
statsQueue
.offer(stats)
.foreach { _ =>
pprint.log(show"Published stats $stats")
replyTo ! ()
}(props.scheduler.value)
}
Behaviors.same
case Die => deadState
case LogError(ex) =>
ctx.log.error(ex.getMessage)
Behaviors.same
}
.receiveSignal {
case (_, PostStop) =>
ctx.log.infoP("stopped")
statsSubject.onComplete()
Behaviors.same
}
val deadState = Behaviors
.receiveMessage[Command] {
// case TakeDamage(value) =>
// children.statsActor ! StatsActor.TakeDamage(value)
// // children.movementActor ! ImMovementActor.MovedDown(true)
// Behaviors.same
// case CurrentStats(replyTo) =>
// // ctx.ask(children.statsActor, StatsActor.CurrentStats())
// children.statsActor ! StatsActor.CurrentStats(replyTo)
// Behaviors.same
// case Heal(_) =>
// Behaviors.same
case CurrentStats(replyTo) =>
// ctx.ask(children.statsActor, StatsActor.CurrentStats())
children.statsActor ! StatsActor.CurrentStats(replyTo)
Behaviors.same
case GetStatus(replyTo) =>
replyTo ! Status.Dead
Behaviors.same
case _ => Behaviors.unhandled
}
.receiveSignal {
case (_, PostStop) =>
ctx.log.infoP("stopped")
statsSubject.onComplete()
Behaviors.same
}
}