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.
 
 

304 lines
9.6 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.SupervisorStrategy
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors
import akka.util.Timeout
import cats.syntax.show._
import monix.execution.CancelableFuture
import monix.execution.CancelablePromise
import wow.doge.mygame.game.subsystems.movement.CanMove
import wow.doge.mygame.implicits._
import wow.doge.mygame.math.ImVector3f
import wow.doge.mygame.subsystems.events.EntityMovementEvent
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedDown
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedLeft
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedRight
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedUp
import wow.doge.mygame.subsystems.events.Event
import wow.doge.mygame.subsystems.events.EventBus
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
import wow.doge.mygame.subsystems.events.TickEvent
import wow.doge.mygame.subsystems.events.TickEvent.RenderTick
import wow.doge.mygame.subsystems.movement.ImMovementActor
import wow.doge.mygame.utils.GenericTimerActor
object NpcActorSupervisor {
sealed trait Command
final case class Move(pos: ImVector3f) extends Command
private final case class InternalMove(
move: Move,
signal: CancelableFuture[NpcMovementActor.DoneMoving.type]
) extends Command
private case object DoneMoving extends Command
private case class LogError(err: Throwable) extends Command
private case class MovementFailed(err: Throwable) extends Command
final case class Props(
npcMovementActorBehavior: Behavior[NpcMovementActor.Command],
npcName: String,
initialPos: ImVector3f
) {
def behavior =
Behaviors.withMdc(Map("actorName" -> npcName))(
Behaviors.setup[Command] { ctx =>
val npcMovementActor = ctx.spawn(
(npcMovementActorBehavior),
s"npc-${npcName}-NpcMovementActor"
)
new NpcActorSupervisor(ctx, this, Children(npcMovementActor))
.idle(State())
}
)
}
final case class State()
final case class Children(
npcMovementActor: ActorRef[NpcMovementActor.Command]
)
}
class NpcActorSupervisor(
ctx: ActorContext[NpcActorSupervisor.Command],
props: NpcActorSupervisor.Props,
children: NpcActorSupervisor.Children
) {
import NpcActorSupervisor._
implicit val timeout = Timeout(1.second)
val movementTimer = ctx.spawn(
GenericTimerActor
.Props(
children.npcMovementActor,
NpcMovementActor.MovementTick,
100.millis
)
.behavior,
s"npc-${props.npcName}-NpcActorTimer"
)
def idle(state: State): Behavior[NpcActorSupervisor.Command] =
Behaviors.setup { _ =>
ctx.log.debugP(s"npcActor-${props.npcName}: Entered Idle State")
Behaviors.receiveMessage[Command] {
case m @ Move(pos) =>
ctx.ask(
children.npcMovementActor,
NpcMovementActor.MoveTo(pos, _)
) {
case Success(signal) => InternalMove(m, signal)
case Failure(exception) => LogError(exception)
}
Behaviors.same
case InternalMove(move, signal) =>
moving(state, move.pos, signal)
case LogError(err) =>
logError(err)
Behaviors.same
case _ => Behaviors.unhandled
}
}
def moving(
state: State,
targetPos: ImVector3f,
signal: CancelableFuture[NpcMovementActor.DoneMoving.type]
): Behavior[NpcActorSupervisor.Command] =
Behaviors.setup { _ =>
ctx.log.debugP(s"npcActor-${props.npcName}: Entered Moving State")
movementTimer ! GenericTimerActor.Start
ctx.pipeToSelf(signal) {
case Success(value) => DoneMoving
case Failure(exception) => MovementFailed(exception)
}
Behaviors.receiveMessagePartial[Command] {
case LogError(err) =>
logError(err)
Behaviors.same
case MovementFailed(err) =>
ctx.self ! LogError(err)
movementTimer ! GenericTimerActor.Stop
idle(state)
case m @ Move(pos) =>
movementTimer ! GenericTimerActor.Stop
children.npcMovementActor ! NpcMovementActor.StopMoving
// new movement request received, cancel previous request
signal.cancel()
ctx.ask(
children.npcMovementActor,
NpcMovementActor.MoveTo(pos, _)
) {
case Success(signal) => InternalMove(m, signal)
case Failure(exception) => MovementFailed(exception)
}
Behaviors.same
case InternalMove(move, signal) =>
moving(state, targetPos, signal)
case DoneMoving =>
movementTimer ! GenericTimerActor.Stop
idle(state)
}
}
def logError(err: Throwable) =
ctx.log.errorP(s"npcActor-${props.npcName}: " + err.getMessage)
}
object NpcMovementActor {
case object DoneMoving
sealed trait Command
case class AskPosition(replyTo: ActorRef[ImVector3f]) extends Command
case object MovementTick extends Command
case object StopMoving extends Command
case class MoveTo(
target: ImVector3f,
doneSignal: ActorRef[CancelableFuture[DoneMoving.type]]
) extends Command
final class Props[T: CanMove](
val enqueueR: Function1[() => Unit, Unit],
val initialPos: ImVector3f,
// val tickEventBus: GameEventBus[TickEvent],
val npcName: String,
val movable: T
) {
def behavior =
Behaviors.setup[Command] { ctx =>
new NpcMovementActor(ctx, this).receive(State())
}
}
final case class State()
}
class NpcMovementActor[T](
ctx: ActorContext[NpcMovementActor.Command],
props: NpcMovementActor.Props[T]
)(implicit cm: CanMove[T]) {
import NpcMovementActor._
def location = cm.location(props.movable)
def receive(state: State): Behavior[NpcMovementActor.Command] =
Behaviors.receiveMessagePartial {
case AskPosition(replyTo) =>
replyTo ! location
Behaviors.same
case MoveTo(
target: ImVector3f,
replyTo: ActorRef[CancelableFuture[DoneMoving.type]]
) =>
props.enqueueR(() => cm.move(props.movable, target - location))
val p = CancelablePromise[DoneMoving.type]()
replyTo ! p.future
ticking(p, target, state)
}
def ticking(
reachDestination: CancelablePromise[DoneMoving.type],
targetPos: ImVector3f,
state: State
): Behavior[NpcMovementActor.Command] =
Behaviors.receiveMessagePartial {
case StopMoving =>
ctx.log.debugP(
show"npcActor-${props.npcName}: Position at Stop = " + location
)
props.enqueueR(() => cm.stop(props.movable))
receive(state)
case MovementTick =>
val dst = ImVector3f.manhattanDst(targetPos, location)
if (dst <= 10f) {
ctx.self ! StopMoving
reachDestination.success(DoneMoving)
} else {
ctx.log.traceP(
show"npcActor-${props.npcName}: Difference = " + dst.formatted(
"%.2f"
)
)
ctx.log.traceP(
show"npcActor-${props.npcName}: Current pos = " + location
)
}
Behaviors.same
}
}
object NpcMovementActorNotUsed {
sealed trait Command
final case class Props(
imMovementActorBehavior: Behavior[ImMovementActor.Command],
npcName: String,
// movementActor: ActorRef[ImMovementActor.Command],
mainEventBus: ActorRef[
EventBus.Command[Event]
],
tickEventBus: GameEventBus[TickEvent]
) {
def behavior: Behavior[Command] =
Behaviors.setup { ctx =>
val movementActor = ctx.spawn(
Behaviors
.supervise(imMovementActorBehavior)
.onFailure[Exception](SupervisorStrategy.restart),
s"npc-${npcName}-MovementActorChild"
)
val npcMovementElBehavior = Behaviors.receiveMessagePartial[Event] {
case event: EntityMovementEvent =>
event match {
case MovedLeft(name, pressed) =>
if (name == npcName)
movementActor ! ImMovementActor.MovedLeft(pressed)
Behaviors.same
case MovedUp(name, pressed) =>
if (name == npcName)
movementActor ! ImMovementActor.MovedUp(pressed)
Behaviors.same
case MovedRight(name, pressed) =>
if (name == npcName)
movementActor ! ImMovementActor.MovedRight(pressed)
Behaviors.same
case MovedDown(name, pressed) =>
if (name == npcName)
movementActor ! ImMovementActor.MovedDown(pressed)
Behaviors.same
}
}
val npcMovementEl = ctx.spawn(
Behaviors
.supervise(npcMovementElBehavior)
.onFailure[Exception](SupervisorStrategy.restart),
s"npc-${npcName}-MovementEventHandler"
)
val renderTickElBehavior =
Behaviors.receiveMessage[RenderTick.type] {
case RenderTick =>
movementActor ! ImMovementActor.Tick
Behaviors.same
}
val renderTickEl =
ctx.spawn(
renderTickElBehavior,
s"npc-${npcName}-MovementTickListener"
)
mainEventBus ! EventBus.Subscribe(npcMovementEl)
tickEventBus ! EventBus.Subscribe(renderTickEl)
Behaviors.receiveMessage(msg => Behaviors.same)
}
}
}