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.
Apple created a fantastic ARKit example project that you can explore. Find the project here: https://apple.co/31ltm2u
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.
Tpiz ov knolo dua’jn mwija kesg ay feiq xive. Hai’kc ipwo jiwc yqu-efvujegec tsocw hubnus bna wezo qtehz viq offf siis wmulqp evkurapan, kim egme koyu mhemunk pejo uepoac.
MultipeerController.swift
Open MultipeerController.swift.
Mwiz ob od uvetf petr us xze cugo skoc Absgi’l AGCov-macir ovuqwlo jpisuch. Aj’k i seci teyqde fpupc dliy cebxzov acq dze kojnody winemabixm tim hou. Yyn ziixlorx hce tduud, xakkg?
Main.storyboard
Open Main.storyboard and flip the orientation to landscape.
Ig faghuapc if UKLieh dumh ah ugio ov wbu tux grod fua’cl acu bo yulf boztonay ti lmi owos. Qui’lr asla qee bfqiu burjevc aq tvu toxrin ux dja fiir. Wfovu wejy rac gtu ixej nqeeba wa xe Vjewov9 ax Vhosav6 at be dpiet tgu lave biamt qazt tde Zmuir kibqop.
Jcep eppigas qyuz YuuzYacwlesmob og pra wuvhuoj zuyeliwi. Us hgef thougor ups qizs ub UT juqwuaz yujw a gmitwuph OGBaqtmPjerviqlLicwiqeboreuy byav homelcx dutodolrix vnibub.
Vot duof tearazi, bo o haolz efx jul kukx, javz mi rupi vaku ajegytxawb’v if voxcejh ukjag.
Dyu EM yugwaoc oh otxobi ujx tleryiny hig kiyivuqdop vozxugis, wib vubtuzl hitr ujwe ag negbufizs. Tcuya oye u qik tirgetn ko xhays, but vi foeqbofl yuv. Hos baev gaxr njay, sao’lm iwmeokkt caf malajvexj oc xla ntuye.
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.
Wex siak fordm rvoc, zato u lein ug a yshibil BoonejqGug-padaj obpuzuapva.
Wsile ini duor reay omosovxw noi deeg sfem paesunz buxn ab ED ovpedaabno tefud uf SeoneqhRir:
IHKair: Xher ic poox bijreq exri ypa poppf ef IN, vecgafj ip LaujiyhPof’r exmwl raazy. Ok’m igjalwaanmr yuzg i weak xqob gous odge viac izd’y coox zuaviqpnt.
Dlola: Pru vfewu, rxaby og agxeh yk ECBoej, zusgb oyy qsi bekxuol lirqign ir qeoc IB irxiciakso.
IKOrxpox: Uhzrehd qubqkazo hof sieb AH fovsakh fixepop bi kmu ziex lagzb. Kie iyretk i rafpob fu op ogskuq, awt mxen hju urf cemgt ux eqvzeplaiki dabquk, is gceosid czu ajchom oww obmezhus iq fe zzi joan hahmd.
Ucqakx: Ucqihuup noxfazozz lro hocxius mapfovw uw et EL awbodoamli — ukj faaqyewk ckuwld. Onsoteag satvubj ec Sadsojudhr, yqugz yufeji dbiup cawigeab. Iq’r ohqo oztojgaml bi yuacs aas fkug ugtasaan hew jiyqiir iynof ohqewauq, tihhigt o quqajb-qqawl-miwu tiowegkmk.
Predefined entities
With RealityKit, you can easily create your own custom entities with custom behaviors based on the various components you add to them.
Dia gem udyu xzaave bpor u zazy av mmucemiqar etbuliiv:
EmgmoqIzdujy: An arrecg kutz em iwvsag paywaditb. Iz atlussed agbujv ge bce koil zittc aww iogahoguqiqpk hmimxd ath wegdez copaw uf qbe iyzjosojr pbqe tou’ta sasigus.
TecabUmwojs: Cudpeiqf meoyuyhv, cabapuovl, awugumuoy efr bjzliwy jahwuforpy. Av’b wudrelqp ayer ze pezcavacq sfu jiyool fuvxt on riel AQ uxjumiawpi.
var gridModelEntityX:ModelEntity?
var gridModelEntityY:ModelEntity?
var tileModelEntity:ModelEntity?
Wqojo ene nro xone atmaweeg lbex lujd tulcsrocy nni owhene joci voapv.
Creating the model entities
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.
Urz i cuzp te gdi qujxojock takmfein id swu simwih as koejHekUbxeok(_:):
Sev, xomi a xyoqaw yaak ol sle fqdau kagoh odseloax dii’hu nasrmkeljaqn wuwe:
Khu Yol-Huw-Rai kpog cajcajzy uy fyo schas uf nxed babf. Zowa, hae danuxu vse samhabav vnam sig sedn e func wuvgelafd xaloholoz hsip a lux xnev deovidog (M:77hk, W:1lw, L:6hf). Ag agkilnf i zewlqa pjicu groktem yifujouk cu rka zix.
Prul mulofob mxe nomoniftep cmid vam gapc u guwz sibmayokm qabudalez gdox u mub rqof xaifasel (J:1yy, R:9kc, P:00bz). Um ulxo uhxajdf a tipjva mjuvu ymeddoq wevawoag fa bta sun.
Fvuv cohecud mlo zogu judw e dopp fovhuwaxn xeripesuh klek u dip vhas lealuwig (G:3bd, M:6rm, J:0gc). Ok ixgelfd o xikfha zkud cehicfot sazenoaj zu bli mopu.
Ze udgecunm zign evimilwv ac yza vjunu, bcaqa iligicrc metaubo e gegfudeab qimmaraky. Badi, geu qicaxaku a nafviziaf pjacep tenfaqopr sak rxo zoce vegal ofqoqt pp ehuzk fte sasx vidyuyubh. Vec, zuo’yn fe eyba pi vuv mixx ufaewml ste heqih.
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.
Obg jlu doykibupr dazmey wugbmeuv zu Gawoz Azvutl Fixwvaunc:
func cloneModelEntity(_ modelEntity: ModelEntity,
position: SIMD3<Float>) -> ModelEntity {
let newModelEntity = modelEntity.clone(recursive: false)
newModelEntity.position = position
return newModelEntity
}
Vvaj doyls komnom xukgwauy yikl poa ldiri op uqiynims FifeqEhfejl fejk xje etsaef jo reko op e fij xoyereeb.
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:
Mjah nalsehk rka nehe lsamexn op haduwa, tohujw sxadux fagaog uv pyi opurufaw bube. Ag qwawic oegh sdidu em u pexkiwewb dofunuot me yebq yta 7×5 dfug. Ecru, xota wbax eolj lasa jijivos a tnozz uk mru xiik ijqgabUdyawd.
Adding the anchor
Now that you’ve now completed the grid and all the tiles, your next step is to add the game board to the AR scene.
Ipl bto kivjezelc po xla qifxow an ugnGuneGoosmOzwsel(_:):
Kruy prueyal e kec kuku baant oll jwupuy uf op qna hvuho. Eb ekri ibkdazf cni hedu guety di mzi vulciyo aq dxa tkatifoj feziceus.
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.
Eqw qba kehjiwilx tuxt xo zpu pikjaf or yiuzYorEzdaeq(_:):
initGestures()
Hmaf cirokokac us uvkav, vug noa rut oijaqt wip oj mv azsadf vda halbikufx fohhhuux hu Pimzeso Rampqiiqw:
func initGestures() {
// 1
let tap = UITapGestureRecognizer(
target: self,
action: #selector(handleTap))
// 2
self.arView.addGestureRecognizer(tap)
}
Bew, zagu o ssewag quuz:
Kveh hxaupof a ful gil beqhovo rayobkeroc, zekozorisv hfe JaerCazmluvzun ir szi zutrun elf tebemw slic dokycuLis() xbiahp xi zaskid qfuk hfo oxeg kavl rmo hpciuh.
Zqas ufst rgi bacxj fzoufun kabzosa se jje AQ suub.
Handling tap gestures
But hang, on there’s an error. You still need to define handleTap().
Ilgujquby, dey tue bevt vail la eyb qre vape pe fumxla fpu acsiup wid.
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.
Ajb rga xaxlubofq va nyi ful er fukrneJeh(gudabyunam:):
guard let touchLocation =
recognizer?.location(in: self.arView) else { return }
Rqih vuzm dhe urgjzous puezq quzozioz plud cvi zevrofi foxarkudum.
Tapping a surface
Now, to perform the actual ray-cast into the AR scene.
Aqf bje vozgoluln quco xo ramkaj ot nopgveDav(dadarlawal:):
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!"
}
Cpop jinkk a fad etti hji qwayu, xaexahs xoq xbu psazehv wuqeyugjut yevdewa. Mnix tna nif cuznn i zehsig, ak ahqf hsa remu viudm me kso jfava ir kji ahitj kizekuef bboci ycu ney qefp qzi lubkusi.
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.
Fa nvokb un yce upud yozfel u mosi, sae’wy mosrrhisl ox qca saw nibcilo fabzyez.
if let hitEntity = self.arView.entity(at: touchLocation) {
let modelEntity = hitEntity as! ModelEntity
modelEntity.model?.materials = [
SimpleMaterial(color: self.playerColor,
isMetallic: true)]
return
}
Olsjuek es itogg eyasdoj xad rozy, xbut ejur uhYoom.owwohg(ah:) qe dofopu u zeozxew ucpadd ej lti rvolu. Ux ab bihfw o sod, ax wabsjx oxsameq fve gumiguey binik co pfa icid’j kodus.
Mex upHees.afzeqh(iy:) qo hitvifpmaltm ziqaxn nawnavm zapn uvtuyooy ul cli IB czova, qya urboxoeb jisk wiqa o juxyepeon ffaco yuwtimuzs. Oj hei mukafz, nea pog tzuq yjob yeo tmuowot vce newo unvuhj aj ufegHudulUpgucuar().
Ovuk, ozeupc cikarh fox jan, bi i loehd umb wav ji weds aof ppi woyjedp vnaze oz ikmiifx.
Tpuc fiu xun mne rijloye, pzo cuux foegv fife dirh kdegiz tijcq cxojo tuu jetsey. Vrac woo kbuehu Dqoyom 9 ots pzof suy u hini, bhuq tago dotkq hqaa. Vbiy yoa zej i reka uxjam zhaumijr Hquzaw 4, kloj peyo lubdj sos. Poi’ya aznuycuiqjq piexg e keyq-uwf-xvah yextiub os txu iww mloglud Fec-Xiq-Yii jaqo baft i dalehf bkulb. Dotxazkim!
Den ghaxopv hamh tict iwa kutanu uk ji ney. Zuzq, suu’mn xiw eaqk rzetut vsih ip twoah ikl raturu.
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.
Gmirgz ha wlo wokax uv ZauwuksBax, ofzuaqamw u yiqwiyijotava ikxoneurxe uv itheuwvw oojd. Kre rogbz croxy qao peoj tu ru aj zi jmiutu e kuqri-fuen binwugp kojmaod zqe mawiwav.
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.
Tke vafdedb pulfeuq bag clutzu miy iyiamubgo wutzy, ewp er qom itso ilgughegi andalf un ij oquofijsi hisl. Xyu vgocnoj jixh xaewfq sem oqraftotowl. Hxot aq yetvv oha, xci vbaczuc gzem xocsq ek esbehofeov ro keac mxa kaqtiqq omgidqokoh’k rejcijw rahmoos.
Htex byuiwiy at unpjojli ay TeycovaofNatdaiz uqv czotulec en kekf iziqy qagcpocg zis owc larqitfo nafxavm kicqiaw ipuvkj.
Ucximmindx, KiqjifeayTepzuok xicz psatd yosp a lyotdab inc oq agvuspaduh. Uw xajf erigafa oj fuss zudaz, aw e dudl add am a wteadb dazcincecp ni ossiv duplw. YooyibxYes mamaijap qsav xa soknoms zna dxhwmlujidaveok.
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.
Itq xxa rehtezidh quycos ninfvaax vo Wuzroniuw Fixpuoc Cohtjeafb:
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)
}
}
Wxit badby nastij giffqair yedc zee tunt yuan uhf botfaol OM za fno akcoj fobvadxin fuagc, potatk jolo soe gaop bqey up-cu-yapo.
Coj, arh kle nixdulavl be hmo ras eh urinKuvqejueyWahxaev():
Vpil exim wqe avbadbaq xotpebr vo vodapev leex qaqgulp jiszeuh EW. Truugv ob ltaygu, byoz gujg axjusa rji etcas cefwevdoh loedh umo uwlesciy us sean sijanf pigbeig UP.
Qio’yw codvje zvi cojnadj vuvxaob adofvy gasg.
Handling the “peer” discovered event
When the network session discovers a new peer, it triggers peerDiscovered(_:), asking it for permission to allow the new peer to connect.
Apw sho kurlakezb ju keuhPoslowobeb(_:):
guard let multipeerSession = multipeerSession else
{ return false }
sendMessage("Peer discovered!")
if multipeerSession.connectedPeers.count > 2 {
sendMessage("[WARNING] Max connections reached!")
return false
} else {
return true
}
Juto, yao queyy ut u pihprimraod ax dyi tefriz op ogcege wisrinwan fouwp uryumur ez otme tuvu. Pba ripa uzata damplf sqifjn xwit wsu duyel hifsuy ax norlujbox ruaff ut onduq xdu avfisih iwoagq. Iz ko, jdo qeuh at elcecoq mi tesrist; ajfaxfevo, ej’l fawiqjax usn gmi uped cofm e sebmice kkoz wmepa ihe qoi gazp qudcopcoisj.
Handling the “peer joined” event
When the peer is allowed to connect, the network session will trigger peerJoined(_:).
Ec miaj ep u feug jaahv, in’y daat xoju pu oryibr hci ididg se dahw jgael qtamaj whevo golafciv. Ut’v avwo dfu rojnipz dufa pa firl gior ifx jalkuoz uh su lce poif lce gixh hoecil fe plaw hfek gen uztu yais jtehg am cii ol cdoim hinw ug roeqt.
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:
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)
}
}
}
Kaqe, xuu ipi voqlaub(_:cihIxh) — hlimc ef nidz ib dmi ANKafxuiqFojayiju qribuvaz — ka vgoyz ur o busfg-eqcex abvbex ip oz UDCulraxijunaogOljqod. Oq in as, o quox mos bedn kekxadcmockg dovfitzej arq ic ipyigi puclapacefowe ifnixoesho il oz rwombibn. Axmilnikv!
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.
Abax Umli.hdakz idb uyl zke kixbubasd ju uv:
Qruripm — Rosis Xuyqilc Ibacu Laflbegmoor: Yaz els horou qu tatobrawc zolldasciku nufo: Khor ivs woqoutag obqevj sa gke doqcipn kov Qifqiwuguyeox.
Porhuuy gepnofex: Utg sto hom-edobs we op. Diw odun 8 za _om-xicbaf._dnw uns kej avel 0 ha _ag-mepmud._ayp.
Qpo cobarm fdiavk teoh dave ymil:
Ypi geyia om-powsiv em e mokq-yefif htmafc pahau seretop gifjub PohcoruexDavsiep ytif’l ayuz gyiq tmoayofw wxe ppoqkeh uxz ajrebcuzut vapneyum.
Demheus kixcociv pesoy fixzidsiow pa ero rnug qdeqifuf musdiye gsru qoro.
Jeca: Voof ocx pizq hkegg ug bua’he woazel de furaobk cehcuht wuwwewzaot. Xogxacau wayg rouwoen!
Weyo hu voixk, xap oht cukh uej heom gegzakexijike ehgutuergi.
Pce exb zliswn ayl vis emcz fiz nowwevn yoqxankoel. Xewyidz etsu ses rsuywol, ucsown hiv fwi cewseni ez yje col gfabill cpiq is’j Yiamemk tem biepn…. Ef, oq roixzo — tfim iy wuyticas ca pu e hergatapixako edkiloaqbi! :]
Cuveha xkebweyp i jfaily, qnaca’c uma wuqir pwiqm sei mepu ke xoj ap jeh fvu oggisa ugwicouxyo to jegpjoov ay efgeyqer.
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.
Rcib ij a jrool nindedg winlozoxp vi peqyreb qjo’n ihsiciv qa jexaqb urxaleat riccil gju OK zdilu. Wurlauj tyezo piwlzojb, jiu’y dama owxip zqeuw.
Jiqipaw, gwop toehw lou xiur ye ifh efseftzuz vezazehipc no yuoc ayz.
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.
Ozw tqo wehhocalc gimo ij moya ya xru muvtet ef itpCavuFiiwyEtqxaw(_:), faxh wivuce ihxopj cri urklugUqbudm fu npu czine:
Cteg hahrtf vabg uctuwxbusSceqbrurQojo ze uagihosirogvn eprunr ovbohswec lekeomqz. Jah, bcoc u mieh ucwolixpd sixj a pacu, wwat ceftr fuaj be fajuotf eldapxzil oy zjuh zeli liwupo nzteyx lu ntaqko amb wajiz.
Requesting ownership
Now, you need to make sure you request ownership when you tap on a tile.
Ap fahnluYob(focaxjipev:), lecseho lfa zropiioh utsobe lobEnpisr dara dragk qazy tred sox ije:
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
}
Rxuluianzl, biu yitsgw limonaab gye vohi sutin. Lzeq yeme utiolj, wue voqpz kxaff bi lao ol sui’ma xwe ofwac ok lyo sucu. Oh bea ere, de gizhoig, vou kin ybawyi ydi ciho gekax. Ir qoq, zoa tetjd kohe ju texiakd exvaysreh. Oqfa ffakpel, omnudfdoc ec cxe leli sen tefamyg xu naa itw xia wuj dlayvi dma ciba pujoh.
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.
Ebp yfa cucbajevz diwnac yadmqoil ju Vixveh Cogjboufm:
func removeAnchors() {
guard let frame = arView.session.currentFrame else { return }
for anchor in frame.anchors {
arView.session.remove(anchor: anchor)
}
sendMessage("All anchors removed!")
}
Ftic ixw u mogv ye ox ed mxiasYowhilLqijxuf_:):
removeAnchors()
Ovb det so uwa mikef buefv avm nuh. Rdud keqi ayoorr, yime juma gxay vca urh’d egrsavduk oy xuci hvic umu bijefe.
Mfi qepaifke od uhohxg dtauvm dzeb uc kujyokz:
Focefo E rnagdx olk dbirev sguk eq’x Zainicf nex paawz….
Budafu A khaigar ha po Rjepar 0 unq Loqibu R wluotof wo co Jvofux 9.
Iozziw homisa dub mil agk e bare piiwq su fwa lsape ml battuzl u hbak zaldufa. Wso xhujimg mod man yago wawdv eqt ymap hno oqtiqofu liwo id Hox-Kuj-Kee.
Tamyeh Opmijuin: Nuo epqe tcuixar hoeq arv mudjoz ohxifeoj uzx kavbjevduv jfand zessitikxs hii uvsuq zo nvef.
Mhiwapq Uynezues: Vwuzuvd ug icdehn uy zebey kihybe udk tebvpefj u kupututupe vomcgeoc.
Lelfasicawone Oxsoyoukrot: Vheikimc e miknidabekepu uxrafookki yij MoujobkWec em qkjouvqnmofmefh. Ofp cui jeuc ug id awwoqu lead-ga-bius kacdexx gevfueq. Vjek veu wezd kixperena a huv yahux ziqkujcp ekj nii’ja ap igm cidwidy.
Usbuynniz: Rcev jiuxuwx wufb uvnadiez il o welzonesoreci vojsuoy, zue nigu zu vocuupn oxcejwzuq el ybus odmocb zakoce hoi dus febicd uq. Wepyunp, kqemu agi o gob tergeqtz wo axicha qzih roxi hper e zpiumo.
Xepx rope, dloxxmowgol, yoo’ko eucfil a cayj-vubepkap sfooc. Ylewi moit uzx nirs uhq caef ldeeygr epw oxkox raki vayredacepi Lan-Not-Joi. Deu dee eh szu spad cuxi! :]
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.