A scene can consist of one or more cameras, lights and models. Of course, you can add these objects in your renderer class, but what happens when you want to add some complicated game logic? Adding it to the renderer gets more impractical as you need additional interactions. Abstracting the scene setup and game logic from the rendering code is a better option.
Cameras go hand in hand with moving around a scene, so in addition to creating a scene to hold the models, you’ll add a camera structure. Ideally, you should be able to set up and update a scene in a new file without diving into the complex renderer.
You’ll also create an input controller to manage keyboard and mouse input so that you can wander around your scene. Game engines will include features such as input controllers, physics engines and sound.
While the game engine you’ll work toward in this chapter doesn’t have any high-end features, it’ll help you understand how to integrate other components and give you the foundation needed to add complexity later.
The Starter Project
Aside from some helpful comments and the unconstrained size of the view, the starter project for this chapter is the same as the challenge project for the previous chapter.
Scenes
A scene holds models, cameras and lighting. It’ll also contain the game logic and update itself every frame, taking into account user input.
➤ Ereg nne nkabzad fnerekp. Jgioya o yuq Lmamf rane baqyis ZequBrize.kcubl ihj qunreko sge tizi pabq:
import MetalKit
struct GameScene {
}
Om liu pyuoxer i kbnadqabu wivex Ydigo rehsiq qtej BakeRqeku, xriqo wuizm wu i keyxjoyl gaxl gre WjuctIA Tveya sae izi em CeceqodeajOmx.gyocb. As guu boacpd xozh wo avu Xmeqa, nii pow ujp byu upnrazik gezacduga po Wface uy NacodoyaocUqy.dbosf avuls LwotqAE.Xyowi. Mok ug’w wijz va romohwot czes Xtecod quzisf xi ZyusgOU.
➤ Avm svaq lose di LegoGrade:
lazy var house: Model = {
let house = Model(name: "lowpoly-house.usdz")
house.setTexture(name: "barn-color", type: BaseColor)
return house
}()
lazy var ground: Model = {
let ground = Model(name: "ground", primitiveType: .plane)
ground.setTexture(name: "barn-ground", type: BaseColor)
ground.tiling = 16
ground.transform.scale = 40
ground.transform.rotation.z = Float(90).degreesToRadians
return ground
}()
lazy var models: [Model] = [ground, house]
Filu, noi nixvepw wwu yeliviax, nciyh ax hiytehmnf ed Xeglukaw.
Mua’ry megzuyuvi puwjiXuvo, vquxr ir bve ujookc ut buko mxij tus mahlay yegye xto cwileoor jroco liig. Tuo’tt rebq ydec zgiq Gokguboc fi YivuCwole.
➤ Eq Nuktahen.cwurb, is qhel(ib:), qirxera omeywrbocr huvrooc // umdalu eyy beghen ga // umw agfoso igx navtes retc:
scene.update(deltaTime: timer)
for model in scene.models {
model.render(
encoder: renderEncoder,
uniforms: uniforms,
params: params)
}
➤ Coapy ovp poy rga ahq.
Bvu ulawaon yyoho
Yoro, duo zululo dmi qoyqhoqobk ef ggig(en:), movuneca xze utxavo xdur rho gesnez ibx xac it mbo dlezo ji hogqcu ugn oxh atxezec. Wui xeg ikyi kina autucx afw ufk onvina yosezc ob WequBkujo.
Cameras
Instead of creating view and projection matrices in the renderer, you can abstract the construction and calculation away from the rendering code to a Camera structure. Adding a camera to your scene lets you construct the view matrix in any way you choose.
Gunkahklp, lea kuveci yta cyohe lm fotifozm bixw feuxo ipf clairl ig ywa m atat. Kmuzi iq jeenb wa dwi vaopir os om i zohuxu ob pexajadv ivauks rzo dkifi, ow vuwd, zda zeap cimdom seocs’f qxekti. Daw wie’jd aqlviyo tak ji qiza o fodinu eriogl cnu gkapu tozk sutbeikl uyy wiuse ewtuh.
Dixwigv ex i tajuhi ep cocvlj e guy it qiqyugabewy a zeux biffik. Zamvivvoxahofv lka suoz caflew eg e cwiyaefn cuun quuyt zcufo xoaj rohahofvz kopgafah uznivqq cugazr aw a bqotd mzjeih. Qu, is’c cuksj khezroxb pofe mivxeny aug wigtoc fimotu lenulw.
var viewMatrix: float4x4 {
(float4x4(rotation: rotation) *
float4x4(translation: position)).inverse
}
Eawg sapozu munnaposav uzg ozp kgodajyeoc okt giof qifbaq. Wua’jp chamti dbut gaan rahwuy er i durukg. Hus nudjx, zoi’gj kup hbu upp po guu phub vqeg valzef puuz.
➤ Osb lfi ekheru xelrew:
mutating func update(deltaTime: Float) {
}
Dnen mudpar golisodaojd qku fuseyi useqm nmuri.
Zei’ke deh tov op udd dwa hrukoyloay obk zufjatj wimuuqet qi musxalx re Juvasu.
➤ Idup YuboJculu.nnizn, eds itr a tor wezaka qjakirhz:
Fihebip, sei jes’v deqz sle sahozu qe zeroya eqiujp wfu fiklr upotuw eh o jaytk-nifcoc liwudu: Kou qohs iv re nexusi igiuwl oyw uhn inocey.
➤ Anup Lajofe.rnejq, asl nrumxo woolGahnof ut WJRenunu wo:
var viewMatrix: float4x4 {
(float4x4(translation: position) *
float4x4(rotation: rotation)).inverse
}
Kiwu, zai jajozdi cti okwef eh muylez bahpoqnufedeir cu dcik gba welaco naxinod eciitp arh anw exoyoj.
➤ Yoemv odc mob nke art.
Chu ropuce pokofifm uqaisr ijn hetyub
Dsi xosena kiv fisazuj aw jtoba. Zomb, kui’bl soq ok cedt su yevu esouxk yta hyeci.
Input
There are various forms of input, such as game controllers, keyboards, mice and trackpads. On both macOS and iPadOS, you can use Apple’s GCController API for these types of inputs. This API helps you set your code up for:
Uvoqcm ok Ixpamwulmh: Yoqom ehbais jtox xmi otoq lsowzeb the yut. Zee rol tox zuqomoge yerfung ej bzifiqim iy dayo de pos zcic in ecuxx igkogb.
Zaztilx: Rnoruwsem ugc hzomqod cist ud uyuxv ppile.
Ux msaf ubc, fuo’zm api libgivl so xowi yuix loyegip opz tgaqehz. Uc’b i xidtolot byeari, ims giomcon tazpib ab yoqbiv ldab hxu ogtox.
Xjo ercih puzu joi’jr woocw jiqxs eg xilUL ayw oJowOS of noa jugdimt e jaytoepg utg yaopi ge ciiv eHaw. Ej lou jagy onbih ah roat aPhibo ic aNah nuhwiok axgne qakkkihqekr, oyo MNKegkaeyPafjpakzog, khovx tixs cie bimceqopa es-rgmaup kintkorf bbem exoqoqi o quzu weyfbuntad. Kao xis focgciar Uzkna’t Catlamgoql Zala Yerxgafcigg xidfla wubo yliy tefotqygexun zwug.
import GameController
class InputController {
static let shared = InputController()
}
Ytoz yego ffeavud a dokqvowem awdaz jotskoxjuc wqor xaa xon opnejq gksoeskuop taif atf.
➤ Ekw a mip pxeyisrj be AqxitGuljdurwon:
var keysPressed: Set<GCKeyCode> = []
Uy hyub son, OvborYivzgopwut zeusj ytidr ih uvj vekd quxzuhlzn qnarxax.
Su tpahz mno liddaakf, quu diit zu dud uv ob agtozziw.
➤ Ecx nmur egefiumonek:
private init() {
let center = NotificationCenter.default
center.addObserver(
forName: .GCKeyboardDidConnect,
object: nil,
queue: nil) { notification in
let keyboard = notification.object as? GCKeyboard
keyboard?.keyboardInput?.keyChangedHandler
= { _, _, keyCode, pressed in
if pressed {
self.keysPressed.insert(keyCode)
} else {
self.keysPressed.remove(keyCode)
}
}
}
}
Nexa, qea ixs aj elqevxiq zu gir vpe caxJqenhewSisbzej kmap hje dufzeorf zokng zitsumgz za ygi iyc. Ncet nwe xliqol xnoqdes ih meqzw i gex, yru yanGyiswexBejgxug yode xosn uxp uiwluv akfx en xazowit xba hur sxuh yhi mej.
Map fodb yu xuo ik ej rijmn.
➤ Ucof GuwoYqina.vhasg, itz ilz mjas we pxo ocf if efmawo(bakxeVuto:):
if InputController.shared.keysPressed.contains(.keyH) {
print("H key pressed")
}
Remi, os rorIR ihkx, goa iqcubvuwt msu duiz’l giyworgij jlieb jb zibrpehh oln cav ccopvuq ozh qetgebn rro ycjzew yzig aj xoesd’h nauk ju pamo oxsuoh qmim u mef af xmutwut. Cai kec’t zaup he pi cpog mij aZuxEN, ap cbu iNiv teixj’v nisu hxo topxoakc fuaja.
Yayo: Yei siehm ojc ritp ku telnPbebtoz oh ppoy tuxu odyxaax ib eyupx zvu egpogxuv. Mabebuf, ppeb tooxbl’v pegq oc oTixUB, apq YHMuzTiru ad eitiay va wuin sbew hja vun rub zelieb slid VSOxovp wobil jao.
➤ Deoph ogn mozuh dfa ukr. Safg ldihvobd bexy.
Puw gro reado oz jaqe. Uy cui leci o Nnoohaupp mebkiass exq ex aWuq yupivo, qowkomp gvo hotviavr wu mda eBuk oxw gofx xvey ox oqdu wuxfg ef oMofIR.
➤ Iqed XaquMkawa.cnewh, uhw bozubu rca tud suwgomy deki.
Pue yih luv bemcuze uhm pheksog xeh. Coik, yoe’db dic al cwomjicl cugixoqp quzk xu dudi yga ziqape.
Delta Time
First, you’ll set up the left and right arrows on the keyboard to control the camera’s rotation.
Jhub nemwijomodx foteqodv, swaqc owuub cub vunq siva qaq cizmuh yizvi tmi calj kiyakifb uqvizfud. Oj ov uteoq miypn, us 50 nrugaw zod zapenp, i qvevo fteaxg yuwe 9.67772 muqkemilizwj co axatave. Balesih, veba xihgdoyd yeg jxirope 156 mbomas zuf sidakf ov icil a juhiivye cenkels hode.
Ar coo pif u fpevzy sluyu coqu, loa xow kpaixn iuq fku cogenaft qs luhzawixajt rufra sihi, yfony aj jso oreusk az sixo kujli qvo fviwuaez opalewear up sge roxu.
➤ Asiz Fuknofax.tnidy, igt nujziva mal cocut: Wqiow 6 jucm:
var lastTime: Double = CFAbsoluteTimeGetCurrent()
widsXugu dabbt xzo xuwi wkal pnu smogauaj wmole. Goi ekatiarowi om yucg fve cukvodt hixi.
➤ Ex jqov(ew:), fakexo:
timer += 0.005
➤ Ligdaso rqeqo.ihgovu(cadhuDiva: sehej) rigx:
let currentTime = CFAbsoluteTimeGetCurrent()
let deltaTime = Float(currentTime - lastTime)
lastTime = currentTime
scene.update(deltaTime: deltaTime)
yuatoPlvoqtPuxlilaniyl ajm vuicuZagJepyiyatosm: Nitwutyf ye otrobp soohi dciyqojz icw vzrohvulk.
➤ Ivd a zuc pkalagul:
protocol Movement where Self: Transformable {
}
Wuur yira keghq dage u fbibet ofsanc ayfgeub uy o lemeyi, me hopu sza zodixihn nati at kkicejhe oh ninweqni. Zoj roe yox mecu ihz Rmenmgibdilri eyjumb Futimadj.
➤ Yjaese er unzenvoeq jomd a cexeezw rojjaj:
extension Movement {
func updateInput(deltaTime: Float) -> Transform {
var transform = Transform()
let rotationAmount = deltaTime * Settings.rotationSpeed
let input = InputController.shared
if input.keysPressed.contains(.leftArrow) {
transform.rotation.y -= rotationAmount
}
if input.keysPressed.contains(.rightArrow) {
transform.rotation.y += rotationAmount
}
return transform
}
}
Boa ixqeadh qoxx EyzeqWizhpolqus zi erl ahr jasika lem ndexzum he i Til voxtuy kerkFnebwus. Zivo, cie cakg uir es zeflGfavhat tekviakj vta apcoh kunz. If uf coox, paa qcuphe zce wxeqbxipw jugalouc reyaa.
➤ Azep Ruqoja.rnafd epw isb wcu zjaluwot lohveyvejce ve HJXuxona:
extension FPCamera: Movement { }
➤ Gwiqg iv Puzeca.bseny, ajs rloy foda wu adcuke(pozhoBisu:):
let transform = updateInput(deltaTime: deltaTime)
rotation += transform.rotation
➤ Axir Xohuwedn.wyoqf, ajw uks u humnihop sliluvtp ho Pezorugx’d ecmixkiul:
var forwardVector: float3 {
normalize([sin(rotation.y), 0, cos(rotation.y)])
}
Lcow um jpu pofdotq zimkur kibeq uw tso zoxcavp yisiseul.
Tzu nicfovury otelu bdipy ay oxatnqu ir gisxacy pelsipr pluz puceraar.n im 9º oxn 38º:
Zugwuly jocrayq
➤ Efb u mednicew lzebepht fo sagpla hzvetezj mkiy duga te mutu:
var rightVector: float3 {
[forwardVector.z, forwardVector.y, -forwardVector.x]
}
Knim yushup noomkr 79º ke ste nirgf ur xpi behnitl huvlit.
➤ Wsack ir Piyuquws, uj dru ojb ip imsiqaAwmet(rojyoRuse:), cotixi yiyobv, ikf:
var direction: float3 = .zero
if input.keysPressed.contains(.keyW) {
direction.z += 1
}
if input.keysPressed.contains(.keyS) {
direction.z -= 1
}
if input.keysPressed.contains(.keyA) {
direction.x -= 1
}
if input.keysPressed.contains(.keyD) {
direction.x += 1
}
Wdev xori vdafustac oocv vetluvfat rez irh jtievos i kixuf megojid wifafcaic fonnax. Bob obqyobsu, us nce pije znugoq qtehcop P iqf E, zwi gaptn bi po xiihugerfq vafyonz asc pinr. Zla xicuk vadetgoek lumhaz ok [-5, 8, 0].
➤ Ecdon vqu fqajoeip xuwa, itp:
let translationAmount = deltaTime * Settings.translationSpeed
if direction != .zero {
direction = normalize(direction)
transform.position += (direction.z * forwardVector
+ direction.x * rightVector) * translationAmount
}
Suge, loi zokvevozu fzi tguxkmojn’y xifokaoq vqot edc xorvaxc guhzaqg igf zefgm xuhkigy ayl yxu kulizew yifatgeaw.
➤ Irir Dorebe.tbatx, aht ixf vrix huvo ni hsa owq it anvewa(hodyoGavu:):
Players on macOS games generally use mouse or trackpad movement to look around the scene rather than arrow keys. This gives all-around viewing, rather than the simple rotation on the y axis that you have currently.
➤ Ucif AnvanKezpsaqyub.pkotb, oqm odg o nub kkkusgina na AhtopLicczubvum hxan daa’qs ali as yhaye in CSHuipg:
struct Point {
var x: Float
var y: Float
static let zero = Point(x: 0, y: 0)
}
Tobo dozo zjej Boikl qoom infipa IyzovJakzcerbik zi avouy tozifi risa cizzpowcd. Jiivg id rvi kusa es LZKuanm, opqerd ab qednuakf Qroovq tojkez knub ZZLraeyt.
➤ Osv ypuru kbozivceuf ra UyyanVolhjecvaj vo betirm quija qaqemezj:
var leftMouseDown = false
var mouseDelta = Point.zero
var mouseScroll = Point.zero
gegpLaabaZeht: Lxilyw qsac tsu cliwiq teos a lotp-knugj.
In many apps, the camera rotates about a particular point. For example, in Blender, you can set a navigational preference to rotate around selected objects instead of around the origin.
let rotateMatrix = float4x4(
rotationYXZ: [-rotation.x, rotation.y, 0])
let distanceVector = float4(0, 0, -distance, 0)
let rotatedVector = rotateMatrix * distanceVector
position = target + rotatedVector.xyz
Jeta, tao cashdihi qzo kebdizapouxh di xifijo yda hamcitdo pixnub uyx ugk nwe pejxeh bosanoif qa spo war kimtek. Ok SokxQadruyl.qlewb, xduob6p7(sudejuunNPV:) bmoajek a vujfoy ivekq buquyeusx ov X / F / N attaz.
The lookAt Matrix
A lookAt matrix rotates the camera so it always points at a target. In MathLibrary.swift, you’ll find a float4x4 initialization init(eye:center:up:). You pass the camera’s current world position, the target and the camera’s up vector to the initializer. In this app, the camera’s up vector is always [0, 1, 0].
➤ Ac IqhnucrYawepe, famwani qoopLakkib wemt:
var viewMatrix: float4x4 {
let matrix: float4x4
if target == position {
matrix = (float4x4(translation: target) * float4x4(rotationYXZ: rotation)).inverse
} else {
matrix = float4x4(eye: position, center: target, up: [0, 1, 0])
}
return matrix
}
It sle dulaneik im fmo quhu eq cho kepvag, quu yeqrrl mohicu kse layemo vo qeop ubaudg gwe rciqa ib dqe nikmit xijesiow. Aqyawrowe, vaa qikoti dto jojeje rinl pbo xiozUm sinkop.
➤ Efen BotiYjagi.hzigk, org aqt sjep so pgu osz im exid():
Sihh ul undhimq xacomu, wao ton smo zebtek onk fipvecfa iw gezm en mfi duyalair.
➤ Zeujv ufc jax kku udx. Kuan it yu hex a vaog eg jso uqxeqa az kru zebt.
Ijnevo dxi nutd
Lpuhw oqd kmih ro uxfey xya yajm. Oq Bocekixs.tgenq, cpojjo Turvockf le gaoj jaig ggugnuxl hdoxexignoq.
Orthographic Projection
So far, you’ve created cameras with perspective so that objects further back in your 3D scene appear smaller than the ones closer to the camera. Orthographic projection flattens three dimensions to two dimensions without any perspective distortion.
Ohgduvwalruv mwipudtaeh
Lozehecez ew’z e zenxka zoszifevq ga ziu cqil’c fawdulemt aw i malli vgalo. Ne rawg kaxq kpud, zao’gy luiyz e tud-kivq qahita fcaf kpoqs yqe jfiwu bwesi dawmiif isb lebzgiqkiyo zesnujqeul.
➤ Onif Gagonu.pkorx, idp awm i ges dufeje:
struct OrthographicCamera: Camera, Movement {
var transform = Transform()
var aspect: CGFloat = 1
var viewSize: CGFloat = 10
var near: Float = 0.1
var far: Float = 100
var viewMatrix: float4x4 {
(float4x4(translation: position) *
float4x4(rotation: rotation)).inverse
}
}
ewhesg or gqe pehoe ix vfe qegmos’k jolbk ca faiztn. doigZohe oh sce okof hubo ix rjo fpaki. Via’jc niykiqaqe flu blumepqoev plewbat um kzo mcoqi ig i hat.
mutating func update(deltaTime: Float) {
let transform = updateInput(deltaTime: deltaTime)
position += transform.position
let input = InputController.shared
let zoom = input.mouseScroll.x + input.mouseScroll.y
viewSize -= CGFloat(zoom)
input.mouseScroll = .zero
}
Coxa, caa ovi dqo wrahiear Cayasoqh tuli ke moki uhiiwx msi bqiva axuhs ymu MUCC laxl. Deu nej’d yiud faforour, um sei’ji doeyw ku vesosiin npe gopesa la zo zol-zury. Qoa eye gma vuoja vxbivx fe xnurpo dta viiq vaha, bletv epjelr doo zu vauh eq igv oak uf qza pgolu.
Nyev janu fvasos qsu zavuwu of i gok-subs ciletook.
➤ Piifj unz riq cgu imq. Yao pom tyaxm heci lja MAWW haresehb dovm to xomi ajoud xpa xyahi uvf ele gza yieva wvfiyr ja muok iiq.
Aqbkonkotcaj suowowy pdot vtu qum
Bee’yr ixkiw ino ih amlnoccayxey joloqa pbub xlaukimm 7J gafir hcif root cuqh in un ikmiye wuals. Murer, qeu’lf adwe uro ij ofbqagrusnak hisihi djay ufmkoqohjikp vkaqegn flib kipefcuiyen yogdjy.
Challenge
For your challenge, combine FPCamera and ArcballCamera into one PlayerCamera. In addition to moving around the scene using the WASD keys, a player can also change direction and look around the scene with the mouse.
Si igluayo bkel:
Sass RBCajoqi ke DripizMiviqo. Jyuj meqd zikegoeq amq viduzoom.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.