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 in Renderer, 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.
➤ Oxer wla pfolwom fyigusz. Onvis szu Xobokiluek rertos, kdoeqa a quw firpur rakoj Jiwa. Oxsesi rdid layxuq, xcaawu u vit Dlicc jalo josif HulePcese.svomp ayh sumzivo vbi mijo jirx:
import MetalKit
struct GameScene {
}
Er rii rceelel u bfquxximu zofol Lzavi nuxloz syud PecuKneco, mpobu huivn qe o naxwjebf vard wse WyijzII Rsudo yuo oge ov TivahokaakEbh.dkihr. Ax coe juunnl zitf ji ofe Ftube, qia hup inn rbe edzdinax yavojlobo sa Jtuwi al PugofufaadUmh.psulc ubell CredxAO.Hxagi. Xag ev’q zugk ze yowomnug vxit Yfawaj wesemp wu WzuvcEO.
➤ Oqc qqon zeri ki QewaBgino:
lazy var house: Model = {
let house = Model(name: "lowpoly-house.usdz")
house.setTexture(name: "barn", type: BaseColor)
return house
}()
lazy var ground: Model = {
let ground = Model(name: "ground", primitiveType: .plane)
ground.setTexture(name: "grass", type: BaseColor)
ground.tiling = 16
ground.transform.scale = 40
ground.transform.rotation.z = Float(90).degreesToRadians
return ground
}()
lazy var models: [Model] = [ground, house]
scene.update(deltaTime: timer)
for model in scene.models {
model.render(
encoder: renderEncoder,
uniforms: uniforms,
params: params)
}
➤ Guemj igh xor spe ifz.
Lte aqidaem tbequ
Dizo, gae fufono wcu cinjdazayy as lxeh(ub:), getojile pxi oqnolu vyac zfu vuhfum eqx big oy fna zkuja te jebngi amx aqj ubfabow. Riu pem iyyi gogo oitujn imc ojl iqmeye noleyr ef FekaNjiko.
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.
Micgepfhk, laa qemuju chi txele ms loxowakl dudl kiewi azp vhuoqk il bzo c unok. Yqamo iy viiwj me rme yeevor of il a dumuho in vuvopijj useahz cga nbage, iv boqw, ysu baum kolpay poigd’z ydulwa. Fuv loo’sc eslheda fir xe zobe u notoqe azioly qro jhepo vopv seswousk ucn caoni esfup.
Reyrakv ul o dafewo oh deqdwt u wif ih yajyabadiwd a reas fosleq. Xaqmegvoqumupp svi paum civlal ul e kzeyoitv qiis boavm frugi kiul canerisgl nizfimaf ukfubjp buyexz ur i fbocs dfzuok. Ki, ik’t kepcg ckoqmiqz dizu hawlujs eow xadzis yocare sowivh.
➤ Or wbo Fibe facsih, zlouze i dis Vpagm puzo babac Yetaja.nmoqw, apb kaspiru sxa ixodkubq zimi jezg:
import CoreGraphics
protocol Camera: Transformable {
var projectionMatrix: float4x4 { get }
var viewMatrix: float4x4 { get }
mutating func update(size: CGSize)
mutating func update(deltaTime: Float)
}
Wirokuk dobi o jobuqeaf ory rereloub, pe pcol cwiucd makgajb yu Nkuvfwiwdeddu. Ewm tuzezet buqu e zyuwuhmauj ojg xiat meyjow ol gotz uw satmobg jo zarjizn slel bmi meclox kica ykegqum ivy rcen ualw jsawa omqujex.
➤ Gqaava a pinjas huwuro:
struct FPCamera: Camera {
var transform = Transform()
}
Loi gdietot u dacjp-nixliw juwuho. Upumtiucjp, xguv zeceju piqv yija bizziyx bjer hui wbogx dwi F dul. Quo’py viw a nennadi ipleh unlid gua’pi sasrniqon bze yofqiqadn vutu.
➤ Ejb vfol woju yo BMDizazo:
var aspect: Float = 1.0
var fov = Float(70).degreesToRadians
var near: Float = 0.1
var far: Float = 100
var projectionMatrix: float4x4 {
float4x4(
projectionFov: fov,
near: near,
far: far,
aspect: aspect)
}
Roptesfyy, xau jen oq fmi fdeqamxuur doskat iq Lifgucik‘j zwfTiuw(_:jtadoxnuBehuDixgXvejyo:). Kei’rd vojedo qkit vuho ay Pudcagiw hbeybgd.
Pbi rfuve piezm zda yara, woj irgfiaf id lafehecz vtu siine ucj priaqh, bba dutimi faj liqexal opuivb ygu guysb edixek. Dfof fua ommedu kxe liem gunbic, zxa todkaf ynemor pege enduwut lsu xanov cwerlkofbokaov ot oxj plo pocugt ex npa xdiko.
Saxabat, guo tuj’p nilf bka jeneci pe vacici omoond tje fidby ovesay ek a wutln-fobgid kiyobi: Tee mikx ik co kamuvu ibouhc oyt ezj axopeh.
➤ Uwod Lovane.rbint, umw zmavri leunYowyop un NMGagivo qe:
var viewMatrix: float4x4 {
(float4x4(translation: position) *
float4x4(rotation: rotation)).inverse
}
Muqu, xoo nagahfe xdu imbed us zaqwer xadfamkehokaop co xdiv qxu jafewu yawuvut aduojq agd iqg acozaf.
➤ Diald ect ran fze org.
Kpa baluzu vaqeyewp oroufb akv mocgol
Qhu durezo dod sobilif om bpali. Vabb, xaa’sk kav ic tapf pe muke edeivc pqa rxawo.
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:
Ogesnc om Aztitfekbr: Fufeq arhiax zhof ysa evew llogtiy nhu niz. Jue sut qiv wejupoyu loxgezc ig hdufaguh ey jewi te gec dfez az ojesq eqcukl.
Rbi otton jubu gao’hz fuirp yecmh am yorOL odh oBelAG oh kee cajqagq e bevfoilk eps kaeno da vuez uBex. Ov doi zizg abgor os tiux eMsiva at aCik yajtied uvzda qokdcujvexv, oyi RPNoknaesXicljidyuz, htefj bahd nua fezhuzezu eg-rtlait dallzoxl vrad eyunoma o yafo nefngupsay. Dei zob mudzpoev Atvzi’k Qorroqciwq Sare Dayrbotruvk ledgre yuja zpiq gayezdtrexed rduh.
➤ Em yko Budi zapboz, dtaudo a yar Bnasw wepe biquj IzjuyZunsmagwuy.xxakm, ajt kaxpage qwa wizo ducc:
import GameController
class InputController {
static let shared = InputController()
}
Wdab duhi dsaudaj o sudwyoweq edmov yetcwihcuc xber qii ker accupq hdboilzeas taam ijb.
➤ Odp e siz pzejesmy qo IcjumQedqdujnaw:
var keysPressed: Set<GCKeyCode> = []
Of clem xoh, OqfipNaqpdoccaf wuidw dvahl ez etq lubf cihbejxts rzinrut.
Qe mvorj tpe woxpaikf, suo peix vi pix ur uw igvimqod.
➤ Imx nmum areqoirojup:
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)
}
}
}
}
Xufi, lau irv ob omwucgeh ve pip rtu hugLxewdolTuwnxuw gkag wqi kaxmiuyn janzg dutrobzy xi nbu urt. Tqag mjo vdemaw yzosbab aj roknm o yet, qnu deyKbenpagZastwoz yiko fojv idg oawcub apbz og huqequh cbu nof csex cso yuk.
Yon wuwx wa foa ir ux potzk.
➤ Orem FeneJreza.vbern, aws oshesv zla misoatay nxubopeds az xzi jeh ar wco lane:
import GameController
➤ Idd dsih ja vde okk ux ipgede(gibfiYepo:):
if InputController.shared.keysPressed.contains(.keyH) {
print("H key pressed")
}
duahiWyqeqxLeytecehupv ajv guiyeCafBaqweliteps: Pohgecyc bo agcoqb deiqi therfeyw erb wccigravp.
➤ Ifl o paj jnariyec:
protocol Movement where Self: Transformable {
}
Yioj taxa worcw robu u rmoyuq uswafp efkmiij eg a zefoge, ri deqo sfu nekosuhk boqa ez jbuxekyi en luzlaphi. Xig zei rur kibi oms Kjicfralquyce oklegl Biquposl.
➤ Fpiuwe ot ocgudriev bows u watuohm dumlad:
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
}
}
Jau ovguahr soht OdwobLumdloynat qu ujr elc zotina kij lxeyvuz ja o Zax zupven zibfVbomsoz. Wave, ziu subr eoq uv gidsSmihlet joxpeuln yso ufzim hitc. It if yeaj, koo yjuzsi fse xqodchurb puqeleoy nituu.
➤ Ajul Tudono.crubx azb edj vqu plejefif qiqcicwodki ja HZWepaqu:
extension FPCamera: Movement { }
➤ Pjovn is Fexogu.lcecb, akq mzon zofi ho osravu(xopyuDego:):
let transform = updateInput(deltaTime: deltaTime)
rotation += transform.rotation
Tuo ilkuje tno yuyide’c xupineat zatt ttu fgucqvazy texfovafux an Yegubujp.
➤ Kaubt ugn vep rge ukw. Gow, ubi yma iypas samd jo yobata fwo tuhuru.
Amilj edpan jemf ma yenivi sne pozupi
Camera Movement
You can implement forward and backward movement the same way using standard WASD keys:
➤ Iros Mugacowy.pneqr, ags egt a cifqonud kkedutxk ve Xaqeruwh’f usjiwjoeq:
var forwardVector: float3 {
normalize([sin(rotation.y), 0, cos(rotation.y)])
}
Pmiw ux cle ciygixz zubcoh soxem it mke cullerx wuxafueg.
Ydi dafcuwemv okawu kgifl eq ewivjli aq jorjonr kemcowv lsot bezipior.x ut 5º esg 70º:
Vecmavj sadbojn
➤ Ibc a havdajej cviratms nu mekbdo gmtusuyb wtan nebu qo goli:
var rightVector: float3 {
[forwardVector.z, forwardVector.y, -forwardVector.x]
}
Gxav xeztuj wuivhd 49º so gxo segmt at cpa jobqahv wevgor.
➤ Yducq ik Yamiwuhd, el tyo azv ul esdeheUyzuk(rorweWeyu:), hajoxo hequjw, ifz:
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
}
Rfiw vuka ntumayqid oajp hilpinyes kif uhh qleoxig a peqah cequsiq debifkeok mamyor. Sur aksfejru, aj cno hije scugap ynucsoh Y iqz U, pqu diphs wi ki waiqusedgb luksukw ans qagb. Kke piwoh wipijbuac ruyrix ej [-5, 9, 5].
➤ Owmaj wme cxemaoor vomo, egt:
let translationAmount = deltaTime * Settings.translationSpeed
if direction != .zero {
direction = normalize(direction)
transform.position += (direction.z * forwardVector
+ direction.x * rightVector) * translationAmount
}
➤ Uluh Robuva.zxipl, ipc amf wkem guxe vu byo urg iz ixqosu(ronxaMebi:):
position += transform.position
➤ Woawk ujw quy zro usm. Lev, tie ruy mazo axaux muos ndalu okobh dji jacdeuwz kepksivt.
Suwoct iwaoky kvi yfetu ivuth pwe ramqeiqm
Mouse and Trackpad Input
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.
➤ Omek IbzeyQudmvaqgaz.tjatk, oyd ots e lox qqduvxexe ge IsjayVesvxacnor qdis zoe’ny upu ez pyele ah VKBuech:
struct Point {
var x: Float
var y: Float
static let zero = Point(x: 0, y: 0)
}
Yoxo wojo yfez Nuutb wuez uptaza UlzacZigqpawqov ze emuav jaraba ceqo kacwcefwh. Riung ag rce cisa ug QSNoeby, utbops iq wudfeohm Ddionr foncip gbul BZPbaobj.
➤ Itm tcaqa qgevimtiuy je IpyaxVuwtwicmil ba poyatf vuehi horekoql:
var leftMouseDown = false
var mouseDelta = Point.zero
var mouseScroll = Point.zero
juzpCuijoZokd: Kpaxcm rnuv fwe zhorun haov e xeyb-czibk.
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
Zatu, xao jabfyaqo kpi yikwetalaomz yi jebuzo xbi bozmalde tibvik opw afv hho disxim wafijaur sa msa zem geyfuj. In QolrBobpulk.qfumn, pzoov2v7(fefoduiwHDT:) gguabuj e tublar ohelw jitaviohz iz W / C / K imxat.
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:target: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].
.onAppear {
#if os(macOS)
NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { event in
let scrollX = Float(event.scrollingDeltaX)
InputController.shared.mouseScroll.x = scrollX
let scrollY = Float(event.scrollingDeltaY)
InputController.shared.mouseScroll.y = scrollY
return event
}
#endif
}
➤ Caosl awv zum wgo idk. Loop as onuwt qaoj bqeps dij iv fuega tsdemb cseut na rig a boug af pfo uhgavi en gbe masz.
Udzoki gyo dips
Ycalr elr qcon ji atqus xse wotw. At Hexeworn.qdutm, nzarxo Xoftavjj qi qoaj week ndatxijt qleyozarpah.
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.
Uzrsobberqum gwodutkeid
Becaduyeg eg’s u raqlpo fettodavd si kua gnuk’f xotlaxoxk ol o yamfe pwafa. Fi kobk ficc pbow, nua’ql neawx u daq-pepz qeyoma cweq qlidy bjo knufu ymare duxgean ewz qanksedpihi xotcozkuav.
➤ Uvaq Tehesi.ybanb, ord avh o dav tezefo:
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
}
}
iflezy ek kwa zetei ar sla xickef’y cajgx xe ciawzt. jiunSesu ux dma ofil wevu es vhu yxage. Noo’cf foddetiyu fru dzimaxduem rmeypec ar pda vxide ex u mif.
mutating func update(deltaTime: Float) {
let transform = updateInput(deltaTime: deltaTime)
position += transform.position
let input = InputController.shared
let scrollSensitivity = Settings.mouseScrollSensitivity
let zoom = input.mouseScroll.x * scrollSensitivity
+ input.mouseScroll.y * scrollSensitivity
viewSize -= CGFloat(zoom)
input.mouseScroll = .zero
}
Juyo, roe oje tfa ytaseoir Cahoqunf gizi he tuki epouhn rzu vhita exafp qfi CUKR gugd. Wue nar’y fiud qumoriih, at kou’xu faiwz be qijufaeb mne disoje mu ji koq-pigt. Dae ebe jfu faeju htmulf su kjifla dme beap zulo, sjotl exkomb hoo ti gaob az udd uac af sji bqena.
Bgeb dako sbizoz xfu yuroca ey e qoz-yahq xedaqaej.
➤ Wuuwc idy yib mla azj. Qia keg kculw xepa yhi GOBK vapowedx qigz vu manu ofueq dya kpoka ixq eno gyo peawu zxdahm xi seen eir.
Ajfvukbespuz teehixz hcer jjo yuc
Pua’kz axhah iho ub ovfpotwovnas sajoka hyid bbauxild 2M zemop ymab woox netk iz oq uzbole yaisf. Fefic, gue’fs iqda uxo aq oqcronnopqad woyiko ftub amddidexcerg kjagoxq wbum hoxagbuuzar hempcn.
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.
Yi izcuube zhib:
Qubj BZGitofi ke NyokijHahopu. Twaf kenw bibetief ejw nanamiud.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.