• Docs
  • Install
  • Follow @cosplayengine
  • v.0.9.5
  • GitHub
  • Watch
  • Examples
  1. Home
  2. Camera Tracking

Camera Tracking

  • Introduction
  • Install
  • ASCII Games
  • Demos
  • Examples
  • Macarena
  • Pong
  • Snake
  • Bird
  • Developer Guide
  • Quick Game 🏃
  • Key Concepts
  • Game Structure
  • Scenes & Objects
  • Log & Debugging
  • Pixels & Colors
  • Images
  • Keyboard Input
  • Sprite Animation
  • Shaders
  • Particle Effects
  • Fonts
  • Canvas Drawing
  • Text Input
  • Camera Tracking
  • Tile Mapping
  • Audio
  • Video
  • UI Toolkit
  • Build & Run

Camera Tracking

Camera tracking is the process by which the game engine automatically focuses on a particular scene object and tracks its movement across screen keeping so that it stays in focus. Without camera tracking a scene object could move outside the visible screen and become invisible.

Camera tracking is only required when the overall scene dimension is larger than the screen size. In such cases only the portion of the scene can be shown on the screen at any given point of time. And thus the proper camera tracking - the process determining which portion of the scene to show - becomes important.

Following diagram shows the relation between a scene, a screen, focus frame and the tracking scene object:

+--------------------------------------------------+
|scene                                             |
|         +-------------------------------+        |
|         |screen/canvas                  |        |
|         |     +--------------------+    |        |
|         |     |focus frame         |    |        |
|         |     |                    |    |        |
|         |     |       o            |    |        |
|         |     |      /|\ tracking  |    |        |
|         |     |      / \ object    |    |        |
|         |     |                    |    |        |
|         |     +--------------------+    |        |
|         |                               |        |
|         +-------------------------------+        |
|                                                  |
+--------------------------------------------------+
        

NOTE: in most cases screen canvas and scene have the same dimension.

CPCamera

Class CPCamera is a camera tracking description. It defines a rectangular subregion of the screen called focus frame. A tracking object can move freely as long as it is fully contained in that focus frame. Once the tracking object (at least partially) moves outside the focus frame, the focus frame and therefore the visible portion of the current scene will shift to bring the tracking object back into the focus frame. The amount of shift as well as the velocity of the visual shift are controlled by this descriptor.

Camera control is a property of scene and available via CPScene.getCamera method. The camera descriptor is a mutable object. Once obtained via CPScene.getCamera method you can configure it. Note that the default camera descriptor returned by the scene is not configured for tracking.

Here's a code snippet from camera example that demonstrates how to configure the camera for a given scene:

            val sc = new CPScene("scene", Option(dim), bgPx, objs)
            val cam = sc.getCamera

            cam.setFocusTrackId(Some("ufo"))
            cam.setFocusFrameInsets(new CPInsets(10, 0))
        

Parallax Effect

One of the frequent special uses of camera control is the parallax effect for scene movement. CosPlay comes with the built-in example that shows how to set up a parallax effect for the scene movements.

Here's the entire source code for this example. Note that along with in-code imagery this example performs procedural terrain generation, sprite animation and parallax movement. The example is pretty long but it shows a complete set of game mechanics for a typical horizontal scroller:

            import org.cosplay.CPArrayImage.prepSeq
            import org.cosplay.CPCanvas.ArtLineStyle.ART_SMOOTH
            import org.cosplay.CPColor.*
            import org.cosplay.CPKeyboardKey.*
            import org.cosplay.CPPixel.*
            import org.cosplay.*

            import scala.collection.mutable

            object CPCameraExample:
                def main(args: Array[String]): Unit =
                    val bgPx = ' '&&(C_BLACK, C_GRAY1)
                    val bgW = 200
                    val bgH = 40
                    val dim = CPDim(bgW, bgH)
                    val bgCanv = CPCanvas(CPDim(bgW, bgH), bgPx)

                    // Paint the starry sky.
                    val starClrs = CS_X11_CYANS ++ CS_X11_ORANGES ++ CS_X11_REDS ++ CS_X11_WHITES
                    bgCanv.fillRect(bgCanv.rect, -1, (x, y) => {
                        if y < 7 && CPRand.randFloat() < 0.02 then
                            val ch = if CPRand.randFloat() < 0.5 then '+' else '*'
                            ch&CPRand.rand(starClrs)
                        else
                            XRAY
                    })

                    // Mountains polyline.
                    val mntPts = mutable.ArrayBuffer.empty[(Int, Int)]
                    mntPts += 0 -> 39 // Initial point.
                    var x = 0
                    var i = 0
                    val lastX = bgW - 20
                    while x < lastX do
                        x += CPRand.randInt(10, 20)
                        val y = if i % 2 == 0 then CPRand.randInt(5, 20) else CPRand.randInt(25, 38)
                        mntPts += x -> y
                        i += 1
                    mntPts += bgW - 1 -> 39
                    mntPts += 0 -> 39 // Close in the polyline.

                    // Paint the mountains with white peaks.
                    bgCanv.drawArtPolyline(
                        mntPts.toSeq,
                        0,
                        // Use 'tag=1' to mark mountains outline for filling later.
                        ppx => ppx.px.withFg(C_GRAY1.lighter(1 - ppx.y.toFloat / 40)).withTag(1),
                        ART_SMOOTH
                    )
                    val blank = ' '&C_BLACK
                    // NOTE: we are using 'tag=1' to detect the outline of the mountains.
                    bgCanv.fill(11, 37, {
                        _.px.tag == 1
                    }, (x, y) =>
                        if y < 5 then 'X'&C_WHITE
                        else if y < 20 then 'X'&C_GRAY1.lighter(1 - (y - 5).toFloat / 15)
                        else blank
                    )

                    val bgSpr = new CPImageSprite("bg", 0, 0, 0, bgCanv.capture()):
                        private var lastCamFrame: CPRect = _
                        private var xf = initX.toFloat

                        override def getX: Int = xf.round
                        override def render(ctx: CPSceneObjectContext): Unit =
                            val camFrame = ctx.getCameraFrame
                            // Produce parallax effect for background sprite.
                            if lastCamFrame != null && lastCamFrame.w == camFrame.w then
                                xf -= (lastCamFrame.x - camFrame.x).sign * 0.7f
                            lastCamFrame = ctx.getCameraFrame
                            super.render(ctx)

                    // Paint the brick-like ground.
                    val brickImg = new CPArrayImage(
                        // 5x3
                        prepSeq(
                            """
                              |^^"^^
                              |___[_
                              |_[___
                            """
                        ),
                        (ch, _, _) => ch match
                            case '^' => '^'&&(C_DARK_GREEN, C_GREEN_YELLOW)
                            case '"' => '@'&&(C_GRAY3, C_GREEN_YELLOW)
                            case '{' => '['&&(C_SANDY_BROWN, C_DARK_ORANGE3)
                            case '-' => '_'&&(C_DARK_ORANGE3, C_DARK_ORANGE3)
                            case c => c&&(C_MAROON, C_DARK_ORANGE3)
                    )

                    val brickCanv = CPCanvas(CPDim(bgW, 3), bgPx)
                    for i <- 0 until bgW / brickImg.getWidth do brickCanv.drawImage(brickImg, i * 5, 0, 2)
                    val brickY = bgH - brickImg.getHeight
                    val brickSpr = new CPStaticImageSprite("bricks", 0, brickY, 2, brickCanv.capture())

                    // Paint palm trees.
                    val palmImg = new CPArrayImage(
                        prepSeq(
                            """
                              |    __ _.--..--._ _
                              | .-' _/   _/\_   \_'-.
                              ||__ /   _/x@@z\_   \__|
                              |   |___/\_x@@z  \___|
                              |          x@@z
                              |          x@@z
                              |           x@@z
                              |            x@@z
                              |             x@@z
                            """),
                        (ch, _, _) => ch match
                            case 'x' => '\\'&C_ORANGE1
                            case 'z' => '/'&C_ORANGE1
                            case '@' => '_'&C_ORANGE1
                            case c => c&C_GREEN1
                    ).trimBg()
                    val palmY = bgH - brickImg.getHeight - palmImg.getHeight
                    val palmSeq = for i <- 0 until 6 yield
                        new CPStaticImageSprite(s"palm$i", CPRand.randInt(10, bgW - 10), palmY, 3, palmImg)

                    val ufoImg = new CPArrayImage(
                        prepSeq(
                            """
                              |    .-""`""-.
                              | _/`oOoOoOoOo`\_
                              |'.-=-=-=-=-=-=-.'
                              |  `-=.=-.-=.=-'
                              |     ^  ^  ^
                            """
                        ),
                        (ch, _, _) => ch match
                            case c@('o' | 'O') => c&C_SKY_BLUE1
                            case '^' => '^'&C_ORANGE1
                            case '=' => '='&C_INDIAN_RED
                            case c => c&C_CYAN1
                    ).trimBg()

                    val ufoSpr = new CPImageSprite("ufo", 0, bgH - brickImg.getHeight - ufoImg.getHeight, 4, ufoImg):
                        private var x = initX.toFloat

                        override def getX: Int = x.round
                        override def update(ctx: CPSceneObjectContext): Unit =
                            // NOTE: we don't need to override 'render(...)' method - it stays default.
                            super.update(ctx)
                            ctx.getKbEvent match
                                case Some(evt) => evt.key match
                                    case KEY_LO_A | KEY_LEFT =>
                                        x -= (if evt.isRepeated then 0.8f else 1.0f)
                                    case KEY_LO_D | KEY_RIGHT =>
                                        x += (if evt.isRepeated then 0.8f else 1.0f)
                                    case _ => ()
                                case None => ()

                    val initDim = CPDim(bgW / 2, bgH)

                    var objs = List(
                        bgSpr,
                        brickSpr,
                        ufoSpr
                    )
                    objs ++= palmSeq

                    // Create the scene.
                    val sc = new CPScene("scene", Option(dim), bgPx, objs)
                    val cam = sc.getCamera

                    // Configure the camera tracking.
                    cam.setFocusTrackId(Some("ufo"))
                    cam.setFocusFrameInsets(new CPInsets(10, 0))

                    // Initialize the engine.
                    CPEngine.init(
                        CPGameInfo(name = "Camera Example", initDim = Option(initDim)),
                        System.console() == null || args.contains("emuterm")
                    )

                    // Start the game & wait for exit.
                    try CPEngine.startGame(sc)
                    finally CPEngine.dispose()

                    sys.exit(0)
        

NOTES:

  • Camera tracking is set up on the line 164-168.
  • Parallax effect for the background is handled on the lines 68-71 by moving background sprite slower than the camera frame movement (in the same direction).

When you run this code (or the built-in example) you will get the following:

  • On This Page
  • Camera Tracking
  • CPCamera
  • Parallax Effect
  • Example
  • Camera Example
  • Quick Links
  • Discord
  • Stack Overflow
  • GitHub
  • @cosplayengine
  • YouTube
  • API
Copyright © 2023 Rowan Games, Inc. Privacy • Docs release: 0.9.5 Latest: