All Downloads are FREE. Search and download functionalities are using the official Maven repository.

indigo.gameengine.GameLoop.scala Maven / Gradle / Ivy

The newest version!
package indigo.gameengine

import indigo.platform.assets.AssetCollection
import indigo.shared.BoundaryLocator
import indigo.shared.IndigoLogger
import indigo.shared.Outcome
import indigo.shared.collections.Batch
import indigo.shared.config.GameConfig
import indigo.shared.dice.Dice
import indigo.shared.events.FrameTick
import indigo.shared.events.GlobalEvent
import indigo.shared.events.IndigoSystemEvent
import indigo.shared.events.InputEvent
import indigo.shared.events.InputState
import indigo.shared.platform.SceneProcessor
import indigo.shared.scenegraph.SceneUpdateFragment
import indigo.shared.time.GameTime
import indigo.shared.time.Millis
import indigo.shared.time.Seconds

import scala.collection.mutable
import scala.scalajs.js.JSConverters._

final class GameLoop[StartUpData, GameModel, ViewModel](
    rebuildGameLoop: AssetCollection => Unit,
    boundaryLocator: BoundaryLocator,
    sceneProcessor: SceneProcessor,
    gameEngine: GameEngine[StartUpData, GameModel, ViewModel],
    gameConfig: GameConfig,
    initialModel: GameModel,
    initialViewModel: ViewModel,
    frameProcessor: FrameProcessor[StartUpData, GameModel, ViewModel],
    startFrameLocked: Boolean
):

  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var _gameModelState: GameModel = initialModel
  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var _viewModelState: ViewModel = initialViewModel
  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var _runningTimeReference: Double = 0
  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var _inputState: InputState = InputState.default
  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var _running: Boolean = true
  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var _frameLocked: Boolean = startFrameLocked

  private val systemActions: mutable.Queue[IndigoSystemEvent] =
    new mutable.Queue[IndigoSystemEvent]()

  private val frameDeltaRecord: scala.scalajs.js.Array[Double] = scala.scalajs.js.Array(0.0d, 0.0d, 0.0d, 0.0d, 0.0d)

  def gameModelState: GameModel    = _gameModelState
  def viewModelState: ViewModel    = _viewModelState
  def runningTimeReference: Double = _runningTimeReference
  def lock(): Unit                 = _frameLocked = true
  def unlock(): Unit               = _frameLocked = false

  private val runner: (Double, Double, Double) => Unit =
    gameConfig.frameRateLimit match
      case None =>
        (time, timeDelta, lastUpdateTime) =>
          runFrame(time, timeDelta)
          gameEngine.platform.tick(gameEngine.gameLoop(time))

      case Some(fps) =>
        (time, timeDelta, lastUpdateTime) =>
          frameDeltaRecord.shift()
          frameDeltaRecord.push(timeDelta)

          val meanDelta = frameDeltaRecord.sum / 5.0d     // Same as number of inital entries
          val target    = gameConfig.frameRateDeltaMillis // E.g. 16.7ms for 60fps

          if timeDelta >= target then
            runFrame(time, timeDelta)
            gameEngine.platform.tick(gameEngine.gameLoop(time))
          else if timeDelta + meanDelta >= target then
            val diff = target - timeDelta
            val t    = time + diff

            gameEngine.platform.delay(
              diff,
              () => runFrame(t, timeDelta + diff)
            )
            gameEngine.platform.tick(gameEngine.gameLoop(t))
          else gameEngine.platform.tick(loop(lastUpdateTime))

  @SuppressWarnings(Array("scalafix:DisableSyntax.null"))
  def kill(): Unit =
    _running = false
    _gameModelState = null.asInstanceOf[GameModel]
    _viewModelState = null.asInstanceOf[ViewModel]
    _runningTimeReference = 0
    _inputState = null
    ()

  def loop(lastUpdateTime: Double): Double => Unit = { time =>
    _runningTimeReference = time
    val timeDelta: Double = time - lastUpdateTime
    if _running then runner(time, timeDelta, lastUpdateTime)
  }

  private def runFrame(time: Double, timeDelta: Double): Unit =
    if _frameLocked then ()
    else if systemActions.size > 0 then performSystemActions(systemActions.dequeueAll(_ => true).toList)
    else runFrameNormal(time, timeDelta)

  @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
  private def runFrameNormal(time: Double, timeDelta: Double): Unit =
    val gameTime =
      new GameTime(Millis(time.toLong).toSeconds, Millis(timeDelta.toLong).toSeconds, gameConfig.frameRateLimit)
    val events = gameEngine.globalEventStream.collect ++ Batch(FrameTick)

    // Persist input state
    _inputState = InputState.calculateNext(
      _inputState,
      events.collect { case e: InputEvent => e },
      gameEngine.gamepadInputCapture.giveGamepadState
    )

    // Run the frame processor
    val processedFrame: Outcome[(GameModel, ViewModel, SceneUpdateFragment)] =
      frameProcessor.run(
        gameEngine.startUpData,
        _gameModelState,
        _viewModelState,
        gameTime,
        events,
        _inputState,
        Dice.fromSeconds(gameTime.running),
        boundaryLocator
      )

    // Persist frame state
    val scene =
      processedFrame match
        case oe @ Outcome.Error(e, _) =>
          IndigoLogger.error("The game has crashed...")
          IndigoLogger.error(oe.reportCrash)
          throw e

        case Outcome.Result((gameModel, viewModel, sceneUpdateFragment), globalEvents) =>
          _gameModelState = gameModel
          _viewModelState = viewModel

          globalEvents.foreach(e => gameEngine.globalEventStream.pushGlobalEvent(e))

          sceneUpdateFragment

    // Play audio
    gameEngine.audioPlayer.playAudio(scene.audio)

    // Prepare scene
    val sceneData = sceneProcessor.processScene(
      gameTime,
      scene,
      gameEngine.assetMapping,
      gameEngine.renderer.renderingTechnology,
      gameConfig.advanced.batchSize,
      events.toJSArray,
      gameEngine.globalEventStream.pushGlobalEvent
    )

    // Render scene
    gameEngine.renderer.drawScene(sceneData, gameTime.running)

    // Process system events
    events
      .collect { case e: IndigoSystemEvent => e }
      .foreach(systemActions.enqueue)

  def performSystemActions(systemEvents: List[IndigoSystemEvent]): Unit =
    systemEvents.foreach { case IndigoSystemEvent.Rebuild(assetCollection) =>
      IndigoLogger.info("Rebuilding game loop from new asset collection.")
      rebuildGameLoop(assetCollection)
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy