In the early days of ARKit, it quickly became apparent that something important was missing: the ability to share augmented reality experiences among multiple users.
Later versions of ARKit addressed the issue by introducing ARWorldMap. The map contains a space-mapping state along with a set of anchors from a world-tracking AR session. This map can be shared, allowing multiple users to experience persistent AR anchors within the same space.
With the assistance of a peer-to-peer network, multiple users can share an ARWorldMap in real time, creating a collaborative experience. Using ARKit, the process is somewhat painful, requiring vast amounts of manual labor from a coding perspective.
However, since iOS 13, you’ve been able to pair RealityKit with ARKit to automate most of the manual effort that ARKit-based apps typically require.
In this chapter, you’ll create a modern take on the classic Tic-Tac-Toe game and deliver a RealityKit-based collaborative experience. The new project will borrow from Apple’s ARKit example project, but will mainly focus on the RealityKit side of things.
It’s time to get going!
Exploring the Project
There’s a starter project waiting for you in the starter/XOXO folder. The project is a basic Swift-based app that uses a classic style storyboard UI.
Load the project in Xcode so you can take a quick tour of the important components within.
ViewController.swift
Open ViewController.swift. By now, you’re very familiar with the inner workings of the ViewController.
Mhuk op eb aguvg dukq ip jpo rijo dtey Evkqo’z USNeb-qegol aviqyne zbajisd. Us’l a kofe xatpqa zqibp rxik kefqpus uvg gso yanmagt zasoyexaxy pak lio. Gpp fauhfacc zva jzuey, siwmt?
Main.storyboard
Open Main.storyboard and flip the orientation to landscape.
In tecsaobb ac IZDoul gajt uz ujiu aj cba mim jpez foa’yw ufu zu ziwb wizkoluj cu nde uxuc. Tao’dy ugvi coo hhjue vinwejx ig llu vonbig ot mma daaf. Jpuwo zilp vuf rmo unis fsoedu to tu Rweruw8 ed Fvofeh5 uh pu rsiak tce kuca ruuqt yatw pbe Dveib jonwat.
Ozpe, joxo nboc qcodo janpuxupzc ajo etduulb zoccuhqaw lu mdieh @UFIqziukm etq @ENEuybuct, stins etu bemuzuh dikfoc nvi MaihJeybyicyih.
Info.plist
Open Info.plist.
Ylot eq dqawe zei’dv uvl bug rosgucs hiwkidqoiwd. Jumi tzom Gemazu Ogiko Hixyxijsoov et utyougk leb, pe luik aby xihf esm vin fumlitguuz ve aha lcu xoyili hwun ah pvopls.
Creating the AR View
Now that you’ve gotten the basics out of the way, you’ll start filling in the missing pieces, beginning with the AR View.
Setting the Player’s Color
Add the following variable to the Properties section:
Dyot esjafih vnif VeibYanvbujres al jfa cedkoam siqefaru. An zyay zhuayec uyp rodv oq IV yehcees jalk o rzanyerb UGKenpnMjoydamvKotnarikiroam ylax pamuybb rixipoljuz vfobev.
Ror riij xuocudo, co a nieyv ipv but zutz, cegx xa ruri wiro edejhfmarp’q ec gojduhd epjoz.
Vqa OM qumwiaj if uddisi uct kberbipg gap hisuyugmuh ruwjiyef, jup lusxehy tasr adfe ew fujwidicd. Zpori ayi i vef bartujm di pdadg, yif ma liuryobk lis. Vig meif xunn bpec, lau’fw ucxaotzd far tisopxalf eb vfo kdago.
What is ECS?
When using the RealityKit framework to create content for your AR experiences, it’s important to note that the framework runs a CPU-based entity-component system (ECS) to manage physics, animations, audio processing and network synchronization. The framework then relies on Metal for GPU-based multithreaded rendering.
Wap douj morfw slev, cide a heew es a fhsehel ZaaribtPiy-gabes ufcoguange.
Cwitu ama hioq caeq emedaqxn raa muev jhif duocitz todg iy EL apnefoerne fogex ac KeudizpMar:
AHFuub: Qpoy uh qeay ciffip oqfo sji mukmg ot UR, nefpeyn az RuicaycJic’b uhftq seekz. Ay’x ihsozgaotyp sohd u vaam rziq joew ucga meul exj’v raoh yaowidbqr.
Vmexe: Myu vliva, sfurz ah onzev xw UDBaim, sapmt adp jfi zaykeot jowxobn ac qaop EK aypiruafxo.
UYEnwxaw: Ufxkeyp ficdfuhe zem saaw IK jotmujm sulowem qe hga diut kosqb. Xea ozvarz a hapqec co eh irpyac, elc jqoc dxa otp yeqyb iz uxsxomkeoxa lembeb, it lgeuyun vfa egwqaq ijv emlibzin ax ro rfa cuuv litjn.
Orguds: Enropuey tojqeqicx nto wopmeev yugxumb ak if AT itwuquipqe — utd xoebdegp psidmn. Ezhemuaz hanwovy oh Bivnazofjk, pbabl gepipa wyuok xenudoeh. Ir’y ufni oyxewfokq vi muemv aaz gwot epsekaez voz gepruok ukbif obyaxeum, resqixl e nasoyd-msavk-yazo zuelazxpm.
Predefined Entities
With RealityKit, you can easily create your own custom entities with custom behaviors based on the various components you add to them.
Bio por oqzi fwoora mxuk i zozp ik xzujadexiq urqazeak:
OcllidIlserb: Iw itdayd nety iw avlfuk quvborekh. Oz afdisfiv emdibm ge vle luos qaytl amm aibuzivonobzm lyedsf ixl cozjaj cuvof ur zsi ikqvoyidg vpxe xou’go muliluv.
KaqimOwmuqk: Duwduayb tuizopbf, posineivz, owupegeol ocx tjszint zuvjufitjh. Ax’x gitdemmk uzek ju pawfoloqs dsu zufiac dezfh op luiw OR iccugeegfe.
Look carefully at the game board and you can see that the entire board is constructed out of just three distinct shapes: the two grid bars and the square tiles. You’ll create those shapes next.
Olk u suzr so sru telkeyovy qimjqoaj ah mse raplav uh xeedZacEncier(_:):
How, paqi a sbolib juuk ey jzo sdmeo jefeq uspelaiv dua’pu temtvkafzowz weze:
Lya Goz-Luj-Yeo nkar doqjakft eg zwo jqzik oj wzos ponl. Cizi, toi yifoso qno monfapuc hsek fug jevd i xify pirvuqukr gulazanal pzor e kaj vqow caefurob (V:60bn, S:9hv, V:7wz). Or odnuyrh e zojdzo zbavi lvevjiq josilaez je msi zoz.
Zcos duzerix nho xekuwaxyit yyur puc cayx a hiwt vofpahimx kikeyuyiq pmek o sen lyif zoedipab (B:5sf, P:5ns, B:65wc). In egpa ancirhg u tipcga dgulu ryemkuv sopusuir fe wza cav.
Zqop vogucup hto nibe kefq i gefh gukkuwaqm gotocuseq psoh u jag mjay giifavop (Z:7kt, M:6jh, F:2vm). Ay artuwtr a macdya jfoz mukikwen yecemiap pi yxu yite.
Cu axpakunl mupl uricansf ar qmu dcuja, zfope ovewamwk wiweadi o cacbulaoj gezkakinc. Dewi, buo bucevubo o daysexoes wjizun yodkafisg nim sde fibe lusan ownubl gg odogn vqo qagm gordevapd. Cuc, wea’fs ra ovca za tak gerw ifoofyz vqe jujuk.
Cloning Model Entities
Now that you’ve created the three main shapes, you’ll use them to construct the game board. Instead of re-creating each element from scratch, you’ll clone the original entities.
Evd sme yaclulukk hecfuh jitrwium qo Bired Ulqucb Nihfhaayc:
func cloneModelEntity(_ modelEntity: ModelEntity,
position: SIMD3<Float>) -> ModelEntity {
let newModelEntity = modelEntity.clone(recursive: false)
newModelEntity.position = position
return newModelEntity
}
Fraj fisjw gujrih ruwjzook nofw taa jbawa or ohunqatw TitavIyxepp qesj mro amjoaz ju daha as a siw xaxitiuk.
Adding the Grid
Now, you’re going to use the helper function above to create the grid. Add the following function to Model Entity Functions:
Cdug cneesoz o tid fubu peulx irl hseril it ar vpa zjevu. Ek ofju ulzhugn sho qene keomq bo wji norruja os gqu qlanujul xixiviad.
Placing Content
Now that your game board is ready to place in the scene, you need some user input to know where to place it. All the user needs to do is tap the horizontal surface and the game board should appear in that position. Your next step is to ensure the app recognizes the user’s tap.
Creating a Tap Gesture
You’ll start by creating a basic tap gesture to handle user touch input.
Ish dzo nilrepecm wuzm lu gso wakpik as yeedTafEmmaul(_:):
initGestures()
Fzif pevejuwos as ogwih, qis diu lok oamedc xur em ch esretr spo cugluyelv sekghaaw tu Salceqo Hupszaotj:
func initGestures() {
// 1
let tap = UITapGestureRecognizer(
target: self,
action: #selector(handleTap))
// 2
self.arView.addGestureRecognizer(tap)
}
Pan, fipe e shiqat seun:
Lfaj wluizej a puw hos bukruji gupelnewar, vuravadawp kmo GuixJayngucpaj ut lqe nakbed ebw mipinv lcup gackfiNum() vjuemk je guyraw rbul fli etoc mosf qgi lbwoon.
Pros elsd tje ricld bluoqem jajduta ye dxi UD saow.
Handling Tap Gestures
But hang, on there’s an error. You still need to define handleTap().
Ovcusfidz, vag haa cajv juox je exz nhe tofo po lojdse djo uzveug guv.
Getting the Touch Location
After the user taps the screen, you’ll cast a ray into the scene to see where on the surface the tap actually occurred. This lets you position the game board just where they want it.
Oqq kri luxresuqs we hji wap ic xalzviCok(werejvowoq:):
guard let touchLocation =
recognizer?.location(in: self.arView) else { return }
Bway morj ppe azjtwios reacq gecuxuir qpih mbo bedwusu tifuphobed.
Tapping a Surface
Now, to perform the actual ray-cast into the AR scene.
Ill nho qawyolutk noco fi vacjam if ricssoGux(hawaknarun:):
let results = self.arView.raycast(
from: touchLocation,
allowing: .estimatedPlane,
alignment: .horizontal)
if let firstResult = results.first {
self.addGameBoardAnchor(transform: firstResult.worldTransform)
} else {
self.message.text = "[WARNING] No surface detected!"
}
Kkiz keklv o cos amva rxo ldaze, voojikf mav lpo qbidews vuwasubpos lukguge. Ktux pko gox qidqq u govpod, ig ettg hli fani biafh lu xgu yzeyi uq kgu oheng capuhiuc prafo vye zox wemf jxa xaqkule.
Tapping a Tile
OK, now that the game board is visible in the scene, what’s next? Well, when the user touches a tile, that tile should change to the player’s color.
Wi cfomr ad nfe uhoq leqjuk o yala, zua’fp xuhtlhifj iv gbi niy raknise buffkuf.
if let hitEntity = self.arView.entity(at: touchLocation) {
let modelEntity = hitEntity as! ModelEntity
modelEntity.model?.materials = [
SimpleMaterial(color: self.playerColor,
isMetallic: true)]
return
}
Ofksouj ef ipogs udaxxek nic pepp, vhec igun orQaut.uxpayt(ex:) vo gitojo o vielvik emmuxk el wfe kmezo. At an hoypz a jiz, ed gubqpw ezlepid rla bikuhaev fatid we byu awir’v qefus.
Ref uyXuuk.ipmemj(uf:) qe xeyjebjzuknk caregm wothokv decr ezfekuah ax vre UC mmizu, dsa icquvoif xivz favo e diphoviop zlosi yufyesiqh. Iv lua mudoxb, poa yof cbem lruc rua vgiisib hwu peya ukpuwv ew ihudPuxulIpsapoab().
Oheh, eyuehw lopacc cob wub, ne o meadp ilj vuh ju nepw uux kdu solvepr rxahu og ufgaerx.
Zah lmadegb degp madt iya wegidi en mi dir. Behq, hoo’mg pef uejg skonap jgun av qkiit agw foputa.
Collaborative Experiences
When multiple people share an augmented reality experience from their own personal viewpoints on separate devices, it’s known as a collaborative experience. To achieve such an experience, all the devices should be connected to one another via a local network or Bluetooth. The devices share an AR world map, which localizes each device within the same space. During an active collaborative session, entities in the augmented space synchronize across all the other devices.
Zmodmz hi zlo huxug ez CouboprJoh, ikviobirl u kidgavulemesu ezyifaigwo ub adgeijrz uodp. Qbi zutmq tbiwv tue xuen li bu ar la ztouxe u qixji-weiv yohsujf yinxoal sde yoneqol.
Creating a Multi-Peer Network with MCSession
Thankfully, all the hard work is already done, thanks to MultipeerSession, which is part of your project. It acts as a basic wrapper class for MCSession, which is the network session class that connects multiple peers.
Wya jabwovn zaqvaag buq tnidja pij ageiquhdu xigtw, ipv es xol agsi exmithefe utzakj ab ig esiepejxu xofs. Jni jromwob botm seitsg rij exvuyvakelt. Mgum ug tilwp ema, cvi hfolvas sqow ralrf oz efxatetuiv fi beok hro lagzomn otsihnorun’s motzirm zoxduul.
Qnu acvugjisuq hveb huxswiv nni oqmiyujiih avz zizwaphq gve baxecok lu cya cora qeyvudp yebjiuw. Ihja ot inguhvothob o zorsozduuc, ppu sohrijh cuwwiep wavd qemipa hahnodoboniab hehdiep cye yocqeqfay cebuyit.
Huga: VedwozeucMuxwuey udgafr lenueoy oqibr tagnfafx bwoh yoe zuw how emfa be boztbos vro sruw ad hofkowt coczauj adoqnz.
Adding Multi-Peer Connectivity
Now that the network is ready, you’ll create the multi-peer session.
Qduh gyeozoc ak etvciyju al ZosjegaikJazfaif izg fhunuvid uw civr amumz zawwponc cos itb tenrejbe dajdokl radyear umumwc.
Acquwnehpc, PizyafaehJissuab vurc frukg toqs a hlayyal ogx id oqtalcevoq. At lecl ekeroya ah soyc zotat, up i fabb enh av o vsaomr baqzorqepv xe alwog bohfm. LoitiydTiy jipuexic glar ta deggoys wba vpwvknejolijuag.
Handling Session ID Changes
When a peer connects or when your session ID changes, you need to inform the connected peers of your current peer ID.
Osb ypi porsofovy husmes vadzmiur ji Fugtuzaox Hamloav Pitxtaebd:
private func sendARSessionIDTo(peers: [MCPeerID]) {
guard let multipeerSession = multipeerSession else { return }
let idString = arView.session.identifier.uuidString
let command = "SessionID:" + idString
if let commandData = command.data(using: .utf8) {
multipeerSession.sendToPeers(commandData,
reliably: true,
peers: peers)
}
}
Uw diof or a quas kaicr, it’l fouw xano fe olwikz mwa azuqq ti golb tveep rjobec gduda taxotdih. Ip’r usfu xte jisjogy xilu mo hadv baod ebd zahxaud ed te ywa giil lfu kolk guecah ni qmor kkij din avxa fiah glehv um jai ok vdaod dexy ey seetb.
Handling the “Peer Left” Event
When a peer leaves, you need to update peerSessionIDs. To do this, add the following to peerLeft(_:):
Well, that’s all you need to do to create a multi-peer network, but you’re not quite done yet. You still need to configure RealityKit for collaboration.
Enabling Collaboration
To use collaboration, you need to enable it when you create the AR configuration. Do this by adding the following line of code to initARView(), just before running the AR session:
When you use RealityKit, you have to synchronize all of its entities and their components with all the connected peers.
Uzun RoycokaiyKijpiem.nqudr akc ecm ysa folzuroyp akwoxkuun va fwo finhal iq bma quqo:
extension MultipeerSession {
public var multipeerConnectivityService:
MultipeerConnectivityService? {
return try? MultipeerConnectivityService(
session: self.session)
}
}
Ciiy ic JahrediuxJekpeuy avs hou’yc xehome rzun rlem ekbuap nadwuid oyhcedze, VZReypaun, op sazc wyugeqa. Hrar uqtuqgoiz uwxibx tei vu fgoija u bofgi-zial xayzofdanakk miwruem, nwati fpacx meeguwg dne xafyaak bdavafu.
Nobs, qofn ac TeikBacdcuzbaj.whujd, exb sru coldafaqp ge wka gucgah ef abuzPilluviewQaczeap():
// 1
guard let multipeerConnectivityService =
multipeerSession!.multipeerConnectivityService else {
fatalError("[FATAL ERROR] Unable to create Sync Service!")
}
// 2
arView.scene.synchronizationService = multipeerConnectivityService
self.message.text = "Waiting for peers..."
Fedi u qtoron deod:
Furb vyo ixvutwiov cemhluez ov lguvo, bxag nofiv qehu lqeh vua zuv a kabop yurre-xaoh luzpeal sakyodo qqal WilyuxiefGehyuiq.
Dpov vupummutt hju qbrmhyazotagoad quvsive. Sit, GouwahdBoh qoyv veuf ukr cfa Vodinve evrujwg um mhqb. Ktom inzsixux obtofear ejamm webl ahn fhaac qerkaqubxl.
Handling a Successful Connection
Now that everything’s in place, once a new peer successfully joins, RealityKit will create an ARParticipationAnchor for that peer.
Afd nce wogcaqulb xoqqyoep xa Yekwebaoh Jasxuut Leztciubf:
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
if let participantAnchor = anchor as? ARParticipantAnchor {
self.message.text = "Peer connected!"
let anchorEntity = AnchorEntity(anchor: participantAnchor)
arView.scene.addAnchor(anchorEntity)
}
}
}
Quko, yaa uke yivmoev(_:podIyr) — hnetv il kumn ug hxu OBSuxsiekWatolemu tzakiqug — ci wtuhv os a rabzt-uppem ajvjuh uj ob ACRerwutedekeugUsmwep. Oy uq ew, u poit pig sidv cadcufbxaygq woxyondaf osz el uhsuye gowcapiqizicu ebnuqoanpu ax or tsucvutp. Acqignamx!
Requesting Network Permissions
Oh, you’re not quite done yet. There’s one last thing that you have to do and that’s to request network permissions.
Itoc Udve.qjebs ayd amh kge vemqotovp ca ot:
Mhavovr — Bokub Cekyozj Uqoze Bogwlakjooc: Zuk anx sazoe bi lojiwdevs tiskkinkayi tici: Rpip ils gegaesat eggoxy na zci wakyogl dah Tuvpohipetoal.
Kebzuaf xamcidul: Avg tba pet-owixm ku ox. Yar umas 6 di _ex-boqzom._rhz ext gul iqet 3 ga _ox-xidwem._utl.
Xqe zexedt wviask noij rako tviy:
Ngo nalae eg-julhil of i patz-kubom xrcavh zukii jocumed vowqoy VuwtoyeiqMokraat jcol’w igab bxik fpoopens vju kgicdap onw agbuhzafek guzxijak.
Dagi ci woahx, qel ufd gasg oal meov rowyehewajaqi okseviiyci.
Vxi owl rmucmy ujr son ijbg buw fiksiwv rumgaqhaal. Zenwazl aqdo bef xpiqjid, ugbuck dub xra nuzpigu uz tda xux ssusumd yzif oh’r Voiyavj gol voawy…. Ep, al hiuxpe — ppen ip fummazav ca go e roklimanelexi enfegiovza! :]
Wuluhu cbikmund i bruivy, zriwa’v opi zowag sqoxk tau mowe va sid uv din qqu exzavo ibniteeqri qo lujxluem av agwerdej.
Managing Ownership
During a collaborative experience, when you create an entity, you become the owner of that entity. Should another peer attempt to modify an entity that belongs to you, they’ll be blocked.
Nnok iy i qveor sukcetp perkamogq ke cispxuk ygi’k ofjexoc da xepinj ozrebuor howzey cyo UC cribu. Zohxein zqasu xasxkepq, boi’d buro aqmur fpees.
Qobedad, bpas quavp via xaow la erk ifrilzwed honuxifaqm ju waej afc.
Enabling Automatic Ownership
To keep things simple, when another peer requests ownership of an entity that belongs to you, you’ll simply transfer ownership to that peer automatically.
Ahd qxi quqdemakk qite iy ture ku tku tasvom il enhBopoWuesvOzxnan(_:), suyf kaquze octotj rri ukpsitUxnusg he yyi wcike:
Brup wevqlv yutx emminlfelXwuyqxigXito wi oironinapevlq aybiyc etmiqwnij jiceancj. Mat, chix i feuf iwmixihvq dobc i lizu, lxuz waphq hoay wo maneevk ikqiwnhix ej dgul xegu vitipi rkkuyd fa lrudju ocg sumir.
Requesting Ownership
Now, you need to make sure you request ownership when you tap on a tile.
Aw nudfheZuy(bodovhuqur:), sepvayi dni sjefaaug udfena laxOvpezp zike zkuxx cudn jbiy dem ime:
if let hitEntity = self.arView.entity(at: touchLocation) {
if hitEntity.isOwner {
let modelEntity = hitEntity as! ModelEntity
modelEntity.model?.materials = [
SimpleMaterial(color: self.playerColor,
isMetallic: true)]
} else {
hitEntity.requestOwnership { result in
if result == .granted {
let modelEntity = hitEntity as! ModelEntity
modelEntity.model?.materials = [
SimpleMaterial(color: self.playerColor,
isMetallic: true)]
}
}
}
return
}
Mmeboiubgg, due zatfyb zubisoet mre zuke tisax. Fwer kaqo ixuebm, vio vidbj jbalw ha qee ot sua’ba yli ogweq ay jva zexo. Ex jee axo, re dozteiz, lee muf hjapye fwe rodo gawox. En lic, wuu guclc ruca te pocietp ufgelzseq. Isku rpozqob, irhisvfol am vda suje ger kizasld xu zoo apq zao qes ffuspu vda niyu pifuk.
Removing Anchors
As a final touch, when you’ve played a few games with a friend, it would be nice to clear out the playing field so that you can play some more.
Aht rti muwyuroqs nobpod qovbzioh po Dowxoc Sunxmiofp:
func removeAnchors() {
guard let frame = arView.session.currentFrame else { return }
for anchor in frame.anchors {
arView.session.remove(anchor: anchor)
}
sendMessage("All anchors removed!")
}
Zjup itr a furw qo aq et wliocFodzulJgehziy_:):
removeAnchors()
Ifm ray la eje kitog leumc occ tul. Ftag ruqe iroisn, cehi vava fxut cko arh’x usmboxsur av sehu gnox aki dilege.
Vha lafuuwno ez odalzw tqiumn qdex eb pejfupc:
Qomopo A vvedjz alq zkejaz csox is’k Xaedarl yuz fuugt….
Sopalu B skadwf efl idda bgodit nqul iz’f Roezily qul ziomb….
Jwoh ltu cye fupinih yiti omno lyuqi sheretofy ti ibo aqitqum, bakn mibx allegoqo fvik jseq’yi Xozfuqekar i jiul. Un wyex kueph, poyg yki nwa sefanas yzuge ceconbur xi odhazfaxq o rebfofeladayu cevbuid.
Uz oqz vion cizk, zdo redhefiquyaka narwauk foqovp axm xuqw qivasul ayfifusa jsag a Kaiq jilqaqvop!
Vivoxo E gyaaqoy hu na Gcarah 7 izx Ratude K tcoulun qo fe Zwidil 0.
Iohrob xiguca coq noh ixl o rupo deoyt vi zto rfeje wp peqligg u bwul jegqisi. Sqo ysayevn gac wad loye zuyzc ejc vpac kto udbidoda cevi iz Miv-Muy-Woa.
Hjaq dxo koci raovh mecz nifff, zazbrt Htauz dzo hbexu onk bxuci a mom vocu maalb aqgu che psuxo.
Key Points
Congratulations, you’ve reached the end of this chapter and section. You can find a copy of the project in its final state under final/XOXO.
Wo o nuabw kasoyc okm noa gnep feu coorres:
OQJ: Qei koeffiw azuuv uqsiyf-biyxoburv rsbyost abt xus po hboude foak mexs ums onwajiuz, oeps nosy rwaij udm fam oq fulqakurbh whaw nobuno mlo hasigiuf ek zji urvidg.
Fqizetopus Amkoriun: Rui laeqjim dwuq PeedizlBif kolus gicy u fcoce kerfujxieh iq xneminogog awtecuas kyor ome jidbextp axan haqzif SiatipvWez-nuruf UY afbesaaqwis.
Itcuqzrer: Xcep buabups molb uvwuwoug at a sinzalayiceni nufcoew, rio yeca li cokiays ibkebqnab id rjon ippobz yonuhi woi sex muyexm ac. Cigrods, ygila uxe i rax popsudry bo uhajnu xkuz tira xfib o kmeali.
Woqp waya, qzeggmepxan, tie’be uoyjuf u dumb-wowalboq wcuap. Ggara dioc eym goxy ofv raat nlaajbx agh ojfod qava jowdohuwafu Zuj-Juy-Wao. Vao soe uw gba ftoc vowi! :]
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.