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))
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:
When you run this code (or the built-in example) you will get the following:
CPCamera