Table of Contents - GitHub€¦ · [GCC 4.2.1 (LLVM, Emscripten 1.5)] on linux2 This is just...

Post on 25-Apr-2020

2 views 0 download

transcript

1.1

1.2

1.2.1

1.2.2

1.2.3

1.2.4

1.2.5

1.2.6

1.2.7

1.2.8

1.2.9

1.2.10

1.2.11

1.2.12

1.2.13

1.2.14

1.2.15

1.2.16

1.2.17

1.2.18

1.3

1.3.1

1.3.2

1.3.3

1.3.4

1.3.5

1.3.6

1.3.7

1.3.8

1.3.9

1.3.10

1.3.11

1.3.12

1.3.13

1.3.14

1.3.15

1.3.16

TableofContentsIntroduction

1.0-Programmingbasics

1.1-Interactivecoding

1.2-Strings

1.3-Nilandvariables

1.4-Usingfunctions

1.5-Commentsincode

1.6-Scriptingandprinting

1.7-Makingfunctions

1.8-Booleans

1.9-Flowcontrol

1.10-While

1.11-Typechecking

1.12-Firstgame

1.13-Tables(part1)

1.14-Tables(part2)

1.15-Forloops(part1)

1.16-Forloops(part2)

1.17-Scopes

1.18-Chapterreview

2.0-IntroducingLÖVE

2.1-Upandrunning

2.2-LÖVEstructure

2.3-Geometry

2.4-Gameloop

2.5-Deltatime

2.6-Mapping

2.7-Theworld

2.8-Readingdocumentation

2.9-Modulesandorganization

2.10-Collisioncallbacks

2.11-Breakout(part1)

2.12-Breakout(part2)

2.13-Breakout(part3)

2.14-Breakout(part4)

2.15-Breakout(part5)

2.16-Binaryandbitmasks

1

1.3.181.3.17

1.4

1.4.1

1.4.2

1.4.3

1.4.4

1.4.5

2.17-Networking(part1)

2.18-Networking(part2)

3.0-Programmingin-depth

3.01-Primitivesandreferences

3.02-Higher-orderfunctions

3.03-Mapandfilter

3.04-Stackandrecursion

3.05-Reduce

2

learn2loveCurrentprogress:

Chapter1-Programmingbasics✔Chapter2-IntroducingLÖVE✔Chapter3-Programmingindepth(inprogress)Chapter4-LÖVEindepth(todo)

Viewasawebpage:link

Downloadinebookformat:pdf-epub

Whatisthisbook?ThisbookteachesprogrammingfromthegroundupinthecontextofLuaandLÖVE.Itteachesbasiccomputerscienceandsoftwarebuildingskillsalongtheway,butmoreimportantly,teachesyouhowtoteachyourselfandfindouthowtogoaboutsolvingaproblemorbuildingasolution.Toolscomeandgo,sothegoalistoteachthingsofvaluewithlessfocusontheprogramminglanguageandothertoolsusedtobuildthesoftware.Ihavebeenprogrammingsince2007,focusingonteachingmyselfbestpractices.AlongthewayIhavefoundalotofgoodandbadtutorialsontherightandwrongwaytobuildthingsandIwanttohelpothersavoidgettingstucklikeIdid.

Whoisthisfor?Anyagegroup.Kidstoo,withabitofdemonstration,helpandencouragement!Anybodythatwantstolearnbasiccomputerscience.Thisbookwilltouchonseveralcomputersciencesubjectsinordertobuildprograms.Anybodythatwantstolearntoprogram.Nopriorskillsorknowledgerequired.Anybodythatwantstolearntomakeagame.Makinggamesarefunandrequirelearningmanythingsalongtheway.We'llbuildafewthroughthisbook.AnybodythatwantstolearnLua.Althoughwewon'tdiveintotheadvancedfeaturesofthelanguage,wewillgainalargeunderstandingonhowthelanguageworksinordertoactuallybuildsomethings.Therearealreadyonlineguidesandreferencescoveringsomeofthemoreadvancedtopics.ForexperiencedprogrammerswantingtolearnLua,theProgramminginLuabookmaybesufficient.

Authorandcontributorsjaythomas:OriginalauthorJimmyStevens:Editsandsuggestionsinchapter1&2rm-code:Chapter2gettingstartedValentinChCloud:Chapter3primitivesandreferences

ContributingIssues,comments,andsuggestionscanbemadeusingtheGitHubissuespage.Todownload,build,andrunthebookoranycodeexamplesusethe"Cloneordownload"buttononthemainrepositorypage.

Introduction

3

Fordevelopersandthecurious

Feelfreetosubmitapullrequest.ThedocumentationisbuiltusingNodeJS.Ifyouwishtorunthedocumentationforlocaldevelopmentpurposes,installnodejsthenrunthesecommandsfromwithinthe learn2lovedirectoryyoudownloaded:

npminstall#Downloadsbuildtoolstothea"node_modules"folderinsidethedirectory

npmstart#Createsalocalwebservertowhereyoucanvisitthelinkhttp://localhost:4000

Oncethelocalwebserverisrunning,anyeditsyoumaketothepageswillrebuildthebookandreloadthepageyou'reviewing.

Introduction

4

Chapter1:ProgrammingBasicsThegoalofthischapteristoteachthemostnecessarybuildingblocksofprogramming.Bytheendofthechapteryouwillbebeabletobuildbasicprogramswhichwewillapplywithexercisesinthefollowingchapters.

1.0-Programmingbasics

5

Interactivecoding

What'saREPL?Programmingdoesn'ttakemucheffortbeyondloadingupaREPLandjusttyping.WhatisaREPL?It'saninteractivewindowyoucantypecodeintoanditspitsouttheresultsonscreenwhenyouhitenter.ItstandsforRead-Evaluate-Print-Loop.Thesearethe4thingstheREPLdoes:

1. Readthecodethatwasjusttyped2. Evaluate,orprocessthecodedownintoaresult3. Print,orspitouttheresult4. Loop...doeverythingagainandagainuntiltheprogrammerisdone

It'sactuallysimplerthanitsounds.Let'sgotoawebsitewithaREPLandtryitout:https://repl.it/languages/Lua

Youwillseetwowindowpanesonthewebsite:alightsideontheleftanddarksideontheright.Theright-sideistheREPLandiswhatwe'reinterestedinfornow.Ithasalotofinformationthatisn'tnecessarilyusefultousatthemoment.Somethingsimilartothis:

Lua5.1Copyright(C)1994-2006Lua.org,PUC-Rio

[GCC4.2.1(LLVM,Emscripten1.5)]onlinux2

ThisisjusttellingyouwhatprogramminglanguagethisREPLisloading,inthiscase,Lua.Ifyouclickinsidethewindowpaneandstarttypingyouwillseeyourtextappear.

Let'strytypingsomecodefortheREPLtoRead.Youalreadyknowsomecodeifyouknowarithmetic.Type:

2+2

ThenhitENTERandimmediatelytheREPLwillPrintout:

=>4

Alothappenedveryquickly.AfterhittingENTER,theREPL,Readtheline 2+2,itEvaluatedthevalueofthatstatementtobe 4,itPrinted4onthescreenforyou,thenLoopedbacktoanewlinetoawaityournextcommand.Tryoutsomemorearithmetic.Multiplication:

2*3

Subtraction:

2+2-4

Division:

6/2

Youcanuseparenthesistotellitwhichordertodotheoperations:

(2+2)*(3+1)

1.1-Interactivecoding

6

Whichgivesdifferentresultsthan:

2+2*3+1

IfyougivetheREPLasinglenumber:

12

Itwillgiveyouback 12,becausethiscan'tbesimplifieddownanyfurther.

Youcanalsodoexponentsusingthe ̂ (caret)symbol:

2^4

Numbersareatypeofdata,and +, -, /, *, ̂ , %areoperators.Statementssuchas 2-2and 23*19arealloperations.

Onelastarithmeticoperationwe'llcoverismodulo,whichisdonewiththemodulusoperator.Themodulusoperatorisrepresentedinmostlanguagesasa %(percent)symbol:

8%3

Modulusoperationsaren'tseeningradeschoolclassroomsasoftenastherest,butarequitecommoninsoftwareandcomputersciences.Thewayitworksisyoutakethe2ndnumberandsubtractitfromthebiggernumberasmanytimesaspossibleuntilthe2ndnumberisbiggerthanthe1st.Theresultiswhat'sleftofthe1stnumber.With 8%3,ifyoukeepsubtracting 3from 8thenyouendupwith 2left.

Arealworldexampleistimeelapsingonananalogclock.Imaginethefaceofaclockwiththehourhandonnoon.If25hourspassthenthehourhandgoesallthewayaroundtwiceandendson1.Thatwouldbeequivalenttowriting:

25%12

=>1

Thehourhandresetseverytimeitpasses12,so 13%12, 25%12,and 37%12wouldallequal 1.Likewise, 10%4resultsin 2because4goesinto10twice,andleavesaremainderof2.

ExercisesTrytypingdifferentmodulooperationsinandguessingwhattheanswerwillbe.Tryusingnegativenumbers( -3+-2).Tryusingasetofparenthesisinsideanothersetofparenthesis.Doesitbehaveasyouexpect?Afterrunningthroughalltheexercisespressthe'up'keyintheREPL.Whathappensandhowcanthisspeedupyourwork?

1.1-Interactivecoding

7

StringsNumbersareonetypeofdatathatcanbeoperatedon.Let'sexploreanotherdatatypewithintheREPL.TakeasetofquotesandputsometextinitandhitENTER:

"hello"

TheREPLwillprint hellobacktoyou.Thisisastring.Astringisasetofcharacters(lettersandsymbols)stringedtogetherasonesinglepieceofdata.Thisstringismadeof9characters:

"H-E-L-L-O"

Likenumbers,thereareoperatorstomakestringsplaywitheachother.Theconcatenateoperator( ..)combinesstringstogether:

"hello".."world"

What'stheresult?Noticethattheresultingstringhasnospacebetweenthetwowords.Ifyouwantedaspace,youwouldhavetoputoneinthequotestobepartoftheoperation:

"hello".."world"

Youcouldevenmakeaseparatestringwiththespaceinit:

"hello".."".."world"

Stringscanhaveanycharactersinthemthatyouwant.

"abc".."123"

"Япо́нский".."ロシア語!!"

ExercisesTryusinganarithmeticoperatoronstrings "hello"/"world".Whathappens?Tryusingtheconcatenateoperator( ..)onnumbers( 1..1).

1.2-Strings

8

Nilandvariables

Data,orthelackthereofHumanshavedifferentwaysofrepresentingalackofdata.Iftherearenosheeptocountthenwehavezerosheep.Iftherearenowordsonapagethenthepageisblank.Inacomputerwemayrepresentthenumberofsheepas 0orthemissingwordsonapageasanempty "".Thesearestilldatathough...anumberandastring.Insoftwarewhenyouwanttorepresentalackofdatawehave:

nil

Sometimescalled nullor undefineddatainotherlanguages.It'sseeminglyuseless.Youcan'tuseoperatorsonnil.

nil+nil

Thiswillprintanerrorlikeitdidwhenyoutrieddoingarithmeticonstrings.Let'stakealookatvariablesandwe'lldiscoverthepurposeof nil.

VariablesSometimesyouwanttowriteoutdata,butyouwantthatdatatobeeasytochange.Variablesletyougivedataanametoreference.Here'sanexampletotry:

name="Mandy"

"hellomynameis"..name

Sinceyoutolditwhat nameis,itknowswhatvaluetoaddtothestring "hellomynameis".Ifyoutype:

name

...andhitENTER,itwillprintoutthevaluethatbelongstothisvariabletoremindyou.The =(equal)signtellsLuathatyouwanttoassignavaluetothegivenname/variable.Youcanchangethevalueofavariableandgetdifferentresults:

name="Jeff"

"hellomynameis"..name

Assignmentisn'tthesameasitisinAlgebra.Youcanchangethevalueofavariablemultipletimes.Wecantell namethatitequalsitselfwithsomeadditionalinformationconcatenatedtoit:

name="abc"

name=name.."def"

name

Youcanassignanytypeofdatatoavariable,includingnumbers:

name="Jeff"

1.3-Nilandvariables

9

age=16

"hellomynameis"..name.."andIam"..age.."."

Youcanchangenumbersafterassignmenttoo:

age=16

age=age*2

"myagedoubledis"..age

So,whatifyoutypeinamadeupvariablename?

noname

Youwillseeithas nil,ornodatayet.Ifyoutrytouse nilinyourstringoperationyouwillgetanerror:

"hellomynameis"..nil

[string"return"hellomynameis"..nil"]:1:attempttoconcatenateanilvalue

"hellomynameis"..noname

[string"return"hellomynameis"..noname"]:1:attempttoconcatenateglobal'noname'(anilvalue)

Tryassigningavaluetoavariablename:

best_color="purple"

thenassigningthatvariabledatatoanother:

worst_color=best_color

worst_color

You'llseethatbothvariablesnowhavethevalue "purple".

Variablescanhavenamesmadeupofletters,numbersandunderscores( _).Variablenamescannotbeginwithanumberthough,otherwiseitwillthinkyou'retryingtotypeinnumberdata.Here'ssomeexamplesofvalidvariables:

my_dog="Poe"

myDog="Zia"

DOG3="Ember"

ExercisesTryoutdifferentvariablenames.Tryafewinvalidvariablesnamestoojusttoseewhattheerrormessagelookslike.It'simportanttoseeerrormessagesandunderstandthem.Theyhelpyouunderstandhowaprogrambreakssoyoucanfixit.

1.3-Nilandvariables

10

UsingfunctionsMostprogramminglanguagescomewithsomevariablesalreadydefinedforus.Luahasmany,solet'stypeoneinandhitENTERtoseewhatthevalueis:

string.reverse

=>function:0x2381b60

Ohmy.So"function"isanotherdatatype,butwhatis 0x2381b60?It'sjusttellingyouwhereinthecomputer'smemorythatfunctionexists,justincaseyouwantedtoknow.Functionsworkverydifferentlythannumbersinstrings.Essentiallyfunctionsarepre-definedinstructionsthattelltheprogramhowtododifferentthings.Theytakedataandreturnbackdifferentdata.Let'sseehowtogivethisfunctiondata:

string.reverse("hello")

=>olleh

Attheendofthefunction'svariablename, string.reverse,wetypeasetofparenthesis, string.reverse(),andputinsidetheparenthesissomedatawewantchanged( string.reverse("hello")).Makingthefunctionrunisoftencalledinvokingthefunction.Havingafunctionthatreversestextinastringforuscanbeuseful,andwecancapturethereturnvalue(theresults)ofthefunctionusingavariable.Tryitout:

greeting="hello,howareyou?"

backwards_greeting=string.reverse(greeting)

backwards_greeting

=>?uoyerawoh,olleh

Itshouldbeobviousfromthenamewhatthatfunction'spurposeis.Howaboutthisone?

string.upper("hello,howareyou?")

Nowtrycapturingthatvaluebyassigningittoavariable:

greeting="hello,howareyou?"

shouting_greeting=string.upper(greeting)

crazy_greeting=string.reverse(shouting_greeting)

Wecangetcrazier.Howaboutinvokingafunctionwheninvokinganotherfunction??

string.reverse(string.upper("hey"))

What'shappeninghereisthestringisbeinguppercasedby string.upperbutthenthevaluefrom string.upperisbeingreversedby string.reverseassoonasitisdone.It'sjustlikeinarithmeticwhenyouhavenestedparenthesis.Theinner-mostparenthesisareresolvedbeforedoingtheouter-mostparenthesis.

Let'stryonemorefunction.Thisfunctionhastwoparameters,meaningitacceptstwopiecesofdatawhichitrequirestoworkproperly.

1.4-Usingfunctions

11

Let'stryonemorefunction.Thisfunctionhastwoparameters,meaningitacceptstwopiecesofdatawhichitrequirestoworkproperly.

math.max(7,10)

Whengivingmorethanonepieceofdatatoafunction,youneedtoputacomma( ,)betweentheparameters

Thesearegreatfunctions,butwouldn'titbegreatifwecouldmakeourown?We'llgiveitashotinjustafewpages.

ExercisesSeeifyoucanfigureoutwhat math.maxdoes.Giveitdifferentnumbersandexaminetheresult.Thereisanotherfunctioncalled math.minthatalsotakestwonumbers.Whatdoesitreturn?

1.4-Usingfunctions

12

CommentsincodeSometimeswemightwanttowriteacommentinourcode–anexplanationtoafriendorourfutureselvesonwhatthepurposeofsomecodeis.Perhapswewanttowriteanotetoourselvestochangesomethinglater.Commentsworkverysimilarlyindifferentlanguagessothey'reprettyeasytoreadevenifyoudon'tunderstandtheprogramminglanguageorthecodeitself.Luadenotesacommentas --andanytextthatfollowsit:

1+1

--Thisisacodecomment

1+2

--Thisisanotherlineofcomments

3+4

Thesecommentswillbecompletelyignoredbythecomputerandaremeantforthehumantoread.Commentscanalsobeonthesamelineascode.Thecomputerwilljustignoretherestofthelinewhenitseesacommentstarting.

1+1--Thisismycomment.Thiscodeaddssomenumberstogetherincaseyoudidn'tknow!

Youwillseecommentsappearinfutureexamplecode,sodon'tletitsurpriseyou!

1.5-Commentsincode

13

ScriptingandprintingLookingbackatthewebsite,(youbookmarkedit,right?)wehavebeenusingtheREPLwindowpaneontheright,buthaven'ttalkedaboutthepaneontheleft.Thiswindowisjustatexteditor.Insteadofrunningtheprogramwitheachlineyoutype,itallowsyoutowritemultiplelinesofcodebeforeexecutingitall.Let'strytypingsomethinginit.Onceyouaredonetypingallthecodeyoucanclickthe"Run"button.

number=4

number=number+1

Butwhenyouclickrun,nothinghappens.Okwellnowthatyouarewritingaprogram,youneedtoreturndata.Solet'sprovideanotherstatementtoourprogram.

number=4

number=number+1

returnnumber

NowwhenyouclickRun,thetext =>5appearsintheconsole.Whenyoutoldittorun,itreadandevaluatedeachlineofthecode,thenwhenitgottothelinewith returnonit,theprogramstoppedandreturnedthevalueyouaskedittoreturn(inthiscase 5).Ifyouwriteanycodeafterthereturnstatement,itwillcauseanerrorwhenwetrytoruntheprogram,sothereturnstatementshouldbethelastthinginourfile.

number=4

number=number+1

returnnumber

--Thislinewillcauseanerror:

number=10

Youcanreturnanytypeofdata,notjustnumbers:

return"hello"

Rememberthoseotherfunctionsweusedbefore?Youcanwritethoseaspartofthereturnstatement.

returnstring.reverse("hello")

Sometimeswhenwritingprograms,wewanttopokearoundandseevalueswhiletheprogramisrunningandnotwaituntilitisdone.Thisissocommonthatthereisafunctionthatprovidesthisforus.

print("hello")

return"world"

The printfunctiontakesanydataandprintsthevalueinthewindowpaneontheright.

>

"hello"

=>"world"

So "hello"isbeingprintedand =>"world"isbeingreturned.Youcaninvokefunctionsonthesamelinewhenprintingifyoureallywanttogetcrazy:

1.6-Scriptingandprinting

14

print(string.reverse("hello"))

return"world"

The printfunctionand returnstatementwillbothcomeinhandywhentestingthefunctionswe'regoingtowrite.

ExercisesWhenwepassdataintoafunction,itiscalledanargument.Wepassed1argumentinto printbutitcanpassintwo,orthree,ormore.Whatdoesitlooklikewhenyouprintmultiplearguments?Funtip,whenusingatexteditoralong-sidetheREPLyoucanrunthecodewithoutthemousebypressing'command+enter'onMacand'Alt+enter'onWindows.Doesthisspeedupyourlearning?

1.6-Scriptingandprinting

15

MakingfunctionsFunctionsarethethirddatatypewe'veseen.We'veaccessedsomevariableswherefunctionsweredefinedforusandhadablastusingthem(IknowIdid).Functionsarethebuildingblocksofsoftware.YoucancomposethemthensnapthemtogetherlikeDanishplasticblocks.Ittakestimetounderstandhowtheyworkandmuchlongertomastertheirinnerpower.Sowithoutfurtherado,let'sseewhattheyactuallylooklike:

function()

return4+4

end

Typeitoutinthetexteditorwindowandletusbreakthisdownlinebylineandwordforword.Wheneverwetypefunction()wearebeginninganewfunction.The2ndlineisthebodyofourfunctionwherethingshappen.Thebodyofthefunctioncanbemanylineslong.Thebodyofthefunctioncouldalsobeempty(butthat'snotveryuseful).Onthelastlineofthefunctionbodywereturndata.Thisreturnstatementwon'tendourentireprogramthough!Itwillonlytellthecomputerwe'refinishingupwithourfunction.Thenonthethirdline,we'retellingthecomputerwe'redonewritingourfunction.Inordertousethisexamplefunction,weshouldprobablyuseavariabletogiveitaname:

add=function()

return4+4

end

Thefirstbitshouldbeunderstandable.Wedeclaredavariablecalled add,thenweassignedsomedatatoitontherightoftheequalsign.Inthiscase,ourfunction.Nowitisreadytouse.

add=function()

return4+4

end

result=add()

returnresult

=>8

We'vemadeourveryownfunctionwithourveryownnameforitandeveninvokeditandgotbackdata!Ifyouinsteadgotanerrormessage,doublecheckwhatyoutypedthatnothingismissing.Errormessagesgiveyoualinenumberofwheretofindtheerrorthatcrashedtheprogram.

Takealookforaminuteathowweinvokedourfunction:

add()

Wetypedoutthevariablenamethatourfunctionisassignedto,followedbysomeparenthesis.Inthoseparenthesisisthedatathatwepassedintoourfunction...waitaminutetheparenthesisareempty.Wedidn'tpassanydataintoourfunction.Wheneverwecalledthoseotherfunctionswepassedindata,likewhenwepassed "hello"intostring.reverse("hello").Whatifwemodifyourlinewhereweinvokeourfunctionandgiveitsomedata?

add=function()

return4+4

end

result=add(16)

1.7-Makingfunctions

16

returnresult

Itseemsitalwaysreturns =>8nomatterwhatargumentswetrytopassin.Weneedtorewindtothefirstlineofourfunctionandtakeacloselookatthisbit:

add=function()

The ()attheendof function()iswherewetellourprogramhowmanyargumentsweareaccepting.Iftheparenthesisareempty,thenourfunctionisignoringallargumentsandwilllikelyalwaysreturnthesameresult.Let'stweakthefunctionslightlyandgiveitoneparameterwiththename a.Let'salsotweakthesecondlinewhilewe'reatit:

add=function(a)

returna+4

end

result=add(16)

returnresult

=>20

Nowwhenwepassindifferentnumbers,wegetdifferentresults:

add=function(a)

returna+4

end

print(add(16))

print(add(12))

Tocompletethisfunction,let'sgiveitasecondparameterof bandmodifythereturnstatementinthefunctionbody:

add=function(a,b)

returna+b

end

print(add(16))

print(add(12))

Ifwetryandrunthecodenow,we'llgetanothererror:

[string"add=function(a,b)..."]:2:attempttoperformarithmeticonlocal'b'(anilvalue)

Let'sreadthiserrorcarefully.Itissayinginsidethesquarebracketsthatanerroroccurredwhenusingthefunctionwedefined( add=function(a,b)...).Totherightofthesquarebracketsitissayingline2( :2)ofourtextistheparticularlocationofthecrash.Totherightofthelinenumberiswhathappenedthatmadeitcrash.Ittriedtoperformadditionwith a+bbutthevalueof bwasnil.Westatedthatourfunctionrequirestwoparametersnow, aandb,andourprogramwillcrashifwetryandinvokethefunctionwithonlyoneparameter.Let'smodifythelineswhereweinvokethefunctiontogiveittwoargumentseachtimeweinvokeit:

add=function(a,b)

returna+b

end

print(add(16,10))

1.7-Makingfunctions

17

print(add(12,2))

Great,everythingisworkingagain!Withtheexperienceofourfirst,fully-functionalfunction,wecannowstarttreadingthewatersofthisgreatworld.

ExercisesTogetusedtowritingfunctions,trywritingsomecomplimentaryfunctionsnamed subtract, multiply, divide,or modulate(modulus).Makea concatenatefunctionthataccepts2stringsandreturns1combinedstring.Trymakingafunctionthattakes3ormoreparameters.

1.7-Makingfunctions

18

BooleansDatatypesarelikeelementsontheperiodictable.Themoreelementsyouhavethemorechemicalscancreate.Luckilytherearen'tasmanydatatypesasthereareelements.Infactwe'velearnedalmostallofthem.Thereareonlytwopossiblebooleans:

true

and

false

That'sright.Andyoucanassignthemtovariablesjustlikenumbers,strings,nil,andfunctions:

myboolean=true

print(myboolean)

Thecoolthingwithnumbersandstringsisyoucanusethemtocreatestatementsthatcanbeevaluatedas trueorfalse.Letmegiveanexamplebyintroducingsomenewoperators.TrytheseoutintheREPL:

5>3

=>true

5<3

=>false

"5isgreaterthan3"isatruestatementsoitreturnsa trueboolean.Naturally,"5islessthan3"isafalsestatementandreturns false.Wecanchecktoseeiftwonumbersareequalinvalue:

number=5

number==5

=>true

Byusingadoubleequal( ==)wecancomparetheequalityoftwonumbers.Thisalsoworksforstrings:

"hello"=="hello"

=>true

"hello"=="HELLO"

=>false

1.8-Booleans

19

Forstrings,oftentimeyouwillseesinglequotes ''(apostrophe)usedinsteadofregularquotes(sometimescalleddoublequotes)wrapperaroundthetext.Luadoesn'tcareaslongasthetextinsidebothstringsareidentical.Wecanprovethatwithanequalitycheck:

'hello'=="hello"

=>true

Anyways,youcanalsodotheinverseofanequalitycheckandcheckforinequality(iftwothingsarenotequal):

5~=3

=>true

"HELLO"~=string.upper("hello")

=>false

Nowlet'sdiginalittledeeperwithtwomoreoperators.Firstisthe andoperator:

3<4and4<5

=>true

ThisreadsoutalmostasplainEnglish.3islessthan4and4islessthan5.Thisisalogicallysoundstatementsoitevaluatestotrue.Justtobeclearonwhat'sactuallygoingonherethough,let'sbreakitdown.Whatwesaidisbeinggroupedinto3separateoperations:

(3<4)and(4<5)

Thetwosetsofparenthesisareevaluatedfirstandinternallythecomputerbreaksthesetwooperationsdownto:

(true)and(true)

Trueandtruearebothtrue.Thissoundssilly,butitisindeedlogicallysound.Let'stryonemorejusttogetthehangofit:

"hello"=="hello"and6>10

Finally,let'stryonemoreoperatortoputabowonthings.Sometimeswedon'tcarethatbothoperationsarecorrect.Weonlycareifone ortheotheriscorrect.

4==10or4~=10

=>true

1.8-Booleans

20

1>100or12==12or"hello"=="bananas"

=>true

Aslongasoneoftheoperationsiscorrect,theentirestatementislogicallytrue.Withtheintroductionof trueandfalsewe'vebroughtinalotofnewoperators:"greaterthan"( >),"lessthan"( <),"equal"( ==),"notequal"( ~=),"and"( and),and"or"( or).

TriviaBooleansgettheirnamefromGeorgeBoolewhoinventedbooleanalgebra,whichwe'vejustseenalittlebitof.

ExercisesTrywritingdifferentstatementswithallthenewoperators.Tryusingtwo andoperatorsinthesamestatementandseeifyoucanmakeitevaluateto true.Tryoutthesetwobonusoperatorswithsomenumbers:"greaterthanorequalto"( >=),and"lessthanorequalto"( <=).

1.8-Booleans

21

FlowcontrolTypicallythecomputerstartsatthetopofourscriptandreadseachlinedowninasequence.WemaketheprogramsjumparoundwithfunctionsinthemixTrythisoutinthetexteditor:

print("I'mcalled1st")

add=function(a,b)

print("I'mcalled5th")

returna+b

end

subtract=function(a,b)

print("I'mcalled3rd")

returna-b

end

print("I'mcalled2nd")

subtract(16,10)

print("I'mcalled4th")

add(12,2)

Wehaveafunctionthatissavedtothevariable addbutitisn'tinvokeduntilfurtherdowninthecode.Soinasenseourprogramhasworkeditswaydownthepagethenjumpedbackuptothefunctionandworkeditswaythroughthebodyofthefunctionthenpickedbackupwhereitwasbefore.Inasimilarfashion,wecanmakeourprogramtakeonepathoranotherdependingifthedatais trueor false.

noise=function(animal)

if(animal=="dog")thenreturn"woof"end

return""

end

print(noise("dog"))

print(noise("rabbit"))

Let'sanalyzethisfunctionlinebyline.Thefunctioniscallednoiseandtakesananimalname(string)asaparameter.Onthenextlineitsaysif"animalisdog"istruethenreturnsomethingspecial.Weputan endattheendofourstatementtomakeitobvioustothecomputer.Ifthestatementwasfalse,then "woof"doesnotgetreturned.Insteadanemptystring( "")getsreturned.Whenweinvokethefunctionwiththeargument"dog"thenwegetback"woof!".With"rabbit"wegetbacksilence.Maybetherabbitdoesn'twantthedogtohearwheresheis.Let'smakeourfunctionmoreversatilebyaddingmoreanimals:

noise=function(animal)

ifanimal=="dog"oranimal=="wolf"thenreturn"woof"end

ifanimal=="cat"thenreturn"meow"end

return""

end

print(noise("dog"))

print(noise("cat"))

print(noise("rabbit"))

print(noise("wolf"))

1.9-Flowcontrol

22

Wehavebranchingpathshappeningwithinourfunction.Ifweweretomapoutthesebranchesitmaylooksomethinglike:

|

+-->"woof"

+-->"meow"

|

+-->""

There'snorequirementthatastatementhastobeallwrittenoutononeline.Sometimeswhendoingmultiplethingsinsideanifstatementwemaywanttoputitonmultiplelines:

ifmy_age>17then

print("You'reanadult!")

print("Getajob!")

end

Similartofunctionshavingbodies,everythingbetween thenand endisconsideredthebodyoftheifstatement.Sometimesitisnecessaryforourbranchestohaveforkswithinthem.Let'ssayourfunctiontakesalanguageasasecond,optionalparameter:

noise=function(animal,language)

ifanimal=="dog"oranimal=="wolf"thenreturn"woof"end

ifanimal=="cat"thenreturn"meow"end

ifanimal=="bird"then

iflanguage=="spanish"thenreturn"pío"end

return"tweet"

end

return""

end

print(noise("dog"))

print(noise("rabbit"))

print(noise("bird"))

print(noise("bird","spanish"))

Theifstatementforcheckingiftheanimalisabirdis4lineslong.Oncewefindoutthattheanimalisabird,whilestillinthebodyoftheifstatement,westoptocheckandseeifthelanguageissettoSpanish.Ifitis,weendupinsideanifstatementwithinanifstatement!Otherwisewe'llreturn "tweet"ifthelanguageisn'tSpanish.Maybemappingoutthepathswillclearthingsup:

|

+-->"woof"

+-->"meow"

+----->"pío"

||

|+-->"tweet"

|

+-->""

Ourcodecangetunreadableveryquicklyifwestartnestingifstatementsinsideeachother.Fortunatelydoingsoisn'tusuallynecessary.

Let'stalkaboutanotheraspectofifstatements.SupposeIhavetwobranchesofcodethatareoppositeofeachother:

ifdaytime==truethen

thermostat=71

end

ifdaytime==falsethen

1.9-Flowcontrol

23

thermostat=68

end

Ratherthanwritingthisoutastwoifstatementsandcheckingthevalueofdaytimetwice,Icantakeadvantageofthekeyword else:

ifdaytime==truethen

thermostat=71

else

thermostat=68

end

Thatwayif daytimeisnot true,itwilldefaulttothesecondbranch.Youcouldreadthisoffalmostlikeasentence:"Ifdaytimeistruethensetthethermostatto71,otherwisesetthethermostatto68."Nothavingtocheckthingstwicewhendoingcomputationssavesustimeandmakesourprogramrunmoreefficiently.Since daytimeisabooleaninthiscase,wedon'tneedtocheckifitistrueorfalse.Wecanjustpassittotheifstatementtobecheckedfortrue/ falseandmakeouroperationevensimpler.

ifdaytimethen

thermostat=71

else

thermostat=68

end

Better."Ifdaytimethensetthermostatto71,otherwisesetthermostatto68."There'sonemorefeatureofifstatementsweshoulddiscuss.Ifthereisanotherconditionyouneedtocheck,maybeseveralmore,youcanusetheelseifkeyword.Itlookssomethinglikethis:

color="green"

ifcolor=="blue"then

print("That'smyfavoritecolor!")

elseifcolor=="green"then

print("Verysubtlechoice.Ilikeit.")

elseifcolor=="pink"then

print("Nice,boldchoice.")

else

print("Idon'tthinkthatcolorwouldmatchyourshoes.")

end

Tryitout!

Thebeginningoftheifstatement... ifcolor=="blue"then...isfalse.Thiscodegetsskippedover.Thenthenextpartoftheifstatement... elseifcolor=="green"then...istruesothatsectionofcodeunderneathit... print("Verysubtlechoice.Ilikeit.")isran.Therestoftheifstatementisskippedwithoutcheckingifitstrueornot.So elseifcolor=="pink"then/ elseareneverprocessed.

ExercisesWriteoutafunctionthattakes1parameternamed"sides".Makethefunctionreturnthenameoftheshapedependingonthenumberofsides(forinstance,"triangle").Trytomaketheifstatementincludean elseattheendtoaccountforeverythingelsethattheifdoesn't.

1.9-Flowcontrol

24

WhileAnotherwaytocheckconditionsiswiththe whilekeyword.

while1+1==2do

print("Mymathiscorrect!")

end

Whileaconditionistrue,thebody(everythingbetweenthe doand end)willberunrepeatedlyandnotstop.Soifyoutriedtorunthatbitofcode,yourscreenprobablywentcrazyprintingoverandoverinanever-endingloop.Weneedtomakesuretheconditioncangetchangedsowe'renotstuckinanever-endingloop.Let'swritealoopwecanescapeoutof.

boolean=true

--Thisconditionwillgetcheckedtwice.Thefirsttimeit

--ischeckeditwillbetrueandthebodyofthewhile-loop

--willberun.Thesecondtimetheconditionischecked,

--ourbooleanwillbefalseandthewhile-loopwon'tberunagain!

whilebooleando

print("Switchingbooleantofalse.")

boolean=false

print("Booleanhasbeensettofalse.")

end

print("Wemadeitoutoftheloop!"

Understandingthatwecanchangethewhileconditionfrominsidethebodyoftheloop,wehavethepowertowriteprogramsthatendexactlywhenwewantthemto.Canyouguesswhatthiswilldowhenwerunit?

countdown=10

whilecountdown>1do

print(countdown.."...")

--Thislineiscriticaltomakeournumbershrink.

countdown=countdown-1

end

print("Blastoff!")

...Andremembertousea >andnota <,oryourloopmayneverrun.

ExerciseComeupwithyourownideaforawhileloop.

1.10-While

25

TypecheckingLuadoesn'tcarewhattypeofdataavariablehas.

data=12

data="hello"

data=true

Tothisend,wecanusethe typefunctiontocheckwhatkindofdataavariableisholding.

type(data)

=>boolean

Wecancheckthetypeoffunction:

type(string.reverse)

type(type)

Wecanalsouseittocheckwhattypeofdataafunctionisreturningbacktous:

type(string.reverse("hello"))

=>string

type(type(12))

=>string

ConvertingdatatypesWe'vealreadyseendatatypeconversionpreviouslywhenwetooknumbersinandoperationandtransformedthatintoatrueorfalsestatement.

type(12>3)

=>boolean

Therearealsowaystoconvertbetweennumbersandstringsusing tonumberand tostring.

number=tonumber("24")

print(type(number))

string=tostring(number)

print(type(string))

number

1.11-Typechecking

26

string

Interestingbutmaybelessuseful,youcanconvertotherdatatypestostring:

print(tostring("alreadyastring"))

print(tostring(true))

print(tostring(nil))

print(tostring(tostring))

ExercisesWhichofthesestringscanbeconvertedtoanumbersuccessfully? "001", "7.12000", "5", "1,943"

1.11-Typechecking

27

FirstgameLet'slearnaboutafewnewfunctionsandthenwe'llbeabletowriteourfirstgame!

ReadinginputNotonlycanourprogramprintoutdata,butusingthefunction io.readitcantakedatatoo.Thisfunctiondoesn'tneedanyargumentsbecauseitwillpromptusinthewindowontherightforustotypeindata.

print("Enteryourname:")

name=io.read()

print("Yournameis"..name..".")

Afteryouclick"Run",theprogramwillpausewhenitruns io.read().TypeyournameandhitENTERandlook,theprogramprintsbackoutthenameyougaveit.Noticethelastprintstatement.Wecombinedthenamewithtwootherstringstoformasentence.Youcanprompttheusermultipletimesifyouneedtogetadditionalinformation:

print("Enteryourname:")

name=io.read()

print("What'syourfavoritefood?")

food=io.read()

print("Yournameis"..name.."andyourfavoritefoodis"..food..".")

Onelimitationwithdoingthisisthedatawillalwayscomeinasastring:

print("What'syourfavoritenumber?")

data=io.read()

print(type(data))

string

Inthelastsectionwetalkedaboutconvertingdatabetweendifferenttypes.Ifwewantedtofindoutwhetheryourfavoritenumberisoddoreven,wewouldneedtoconvertittoanactualnumbertoperformoperationsonit.Typethisinyourtexteditorandrunit:

print("What'syourfavoritenumber?")

data=io.read()

number=tonumber(data)

--Iftheusergaveusananswerthatisn'ta

--number,thenthevalueof"number"isnil.

ifnumber==nilthenreturn"Invalidnumber."end

ifnumber%2==0thenreturn"Yournumberiseven."end

return"Yournumberisodd."

Randomnumber

1.12-Firstgame

28

Manylanguagesgiveusaccesstoarandomnumbergenerator.Randomnessishowwegeneratesecurepasswordsandkeysintherealworld.TogeneratearandomnumberinLua,weuse math.random:

math.random(100)

=>63

Thisgeneratesarandomnumberbetween1and100.Except,ifyouruntheprogramrepeatedlyyoumaynoticethatitspitsoutthesamenumber.That'sbecausenothinginthecomputerworldisrandom.Ifwefedinrandomnoisesthroughaspeakerorwhitenoisefromanoldtelevisionsetthenourcomputercouldusethistogeneraterandomnumbers.Sincewedon'teasilyhaveaccesstothosethings,weneedtoseedLuawithsomeperceivedrandomness.

Ifwerun os.timewewillgetthecomputer'scurrenttimeinintegerform:

os.time()

=>1.529098167e+09

Thisnumberishardenoughtoguessthatitwillworkasaseedforourprogram.Let'stakethesystemtimeandfeeditinusing math.randomseedthenfromthere,Luawillbeabletogeneratea"random"numberintherangewewant(1-100).

seed_number=os.time()

math.randomseed(seed_number)

returnmath.random(100)

=>19

Success!Itisgenerateddifferentnumberseachtimewerunit,withnopattern.

PuttingitalltogetherIshouldprobablyexplainwhatthisgameis.It'squitesimple.Wewantthecomputertomakeupanumberandtheuserhastoguesswhatthenumberis.Ifthey'rewrong,thenweshouldgivethemahintandmakethemguessagain.Wecantakeadvantageofthewhilelooptomakethemcontinueguessingwhiletheirguessisincorrect.

--Thecomputer'ssecretnumber

math.randomseed(os.time())

number=math.random(100)

print("Guessmysecretnumber.Itisbetween1and100.")

guess=tonumber(io.read())

--Whiletheuser'sguessisnotequalto

--thenumber,repeatthebodyoftheloop.

whileguess~=numberdo

--Givethemsomehints

ifguess>numberthen

print("Yourguessistoohigh.")

end

ifguess<numberthen

print("Yourguessistoolow.")

end

1.12-Firstgame

29

--Makethemguessagainandagainuntiltheygetit

print("Guessagain:")

guess=tonumber(io.read())

end

--Winningmessage

print("Youguessedcorrectly!Thenumberwas"..number..".")

Let'sre-factoronebitofthiscodetomakeiteasiertoread.Whenwetalkedaboutifstatements,rememberthekeyword else?

--Thecomputer'ssecretnumber

math.randomseed(os.time())

number=math.random(100)

print("Guessmysecretnumber.Itisbetween1and100.")

guess=tonumber(io.read())

--Whiletheuser'sguessisnotequalto

--thenumber,repeatthebodyoftheloop.

whileguess~=numberdo

--Givethemsomehints

ifguess>numberthen

print("Yourguessistoohigh.")

else

print("Yourguessistoolow.")

end

--Makethemguessagainandagainuntiltheygetit

print("Guessagain:")

guess=tonumber(io.read())

end

--Winningmessage

print("Youguessedcorrectly!Thenumberwas"..number..".")

Nowthatthingsarecleaner,let'saddonefeaturetoourprogram.Itwouldbemorefunifthegamekepttrackofhowmanyguesseswemadesowecouldgivethemaspecialmessage.Let'screateavariablecalled guess_countthatwillstartat 1andincrementeverytimetheusermakesanotherguess.We'llalsogoaheadandaddsomemessagestothebottomtopraisetheuseriftheydiditinareasonablenumberofguesses.

--Thecomputer'ssecretnumber

math.randomseed(os.time())

number=math.random(100)

--Ourstartingnumberofguesses

guess_counter=1

print("Guessmysecretnumber.Itisbetween1and100.")

guess=tonumber(io.read())

--Whiletheuser'sguessisnotequalto

--thenumber,repeatthebodyoftheloop.

whileguess~=numberdo

--Incrementtheguesscounterby1

guess_counter=guess_counter+1

--Givethemsomehints

ifguess>numberthen

print("Yourguessistoohigh.")

else

print("Yourguessistoolow.")

1.12-Firstgame

30

end

--Makethemguessagainandagainuntiltheygetit

print("Guessagain:")

guess=tonumber(io.read())

end

--Winningmessages

print("Youguessedcorrectly!Thenumberwas"..number..".")

ifguess_counter<=5then

print("Amazing!Itonlytookyou"..guess_counter.."tries.")

else

print("Ittookyou"..guess_counter.."tries.Notbad.")

end

ExercisesTryaddingmoremessagestothe guess_counterfordifferentscores.Tryaddingamessagetotheifstatementwiththehintsforwhentheuserguessesaninvalidnumber.Howwouldyoudothat?Maketheconditionexitif guess_countergoesabove10andtelltheusertheylostthegamebutshouldtryagain.

1.12-Firstgame

31

Tables(part1)Tablesarethelastdatatypewe'lldiscussinthischapter.Otherlanguageshavedifferentnamesforthisdatatypelike"object","hash","map"and"dictionary",andthefeaturesmayvaryfromoneprogramminglanguagetoanother.Tablesareusedtobuildcompositedatatypeslikelists,trees,orabiggreenorcrunningacrossthescreen.Compositedatatypesarehigherorderdatastructurescreatedfrommoreprimitivedatatypeslikenumbersandstrings.Thenumberofdatastructuresyoucancreateareendless.Weneedtolearnaboutafewtonotonlyunderstandhowtableswork,buttobeabletobuildanymodernsoftware.

Thebasicsyntaxfortablesistomakeacurlybrace {(samekeyasthesquarebraceonmostkeyboards)tostartthetable,writesomedatainthetable,thenputaclosingcurlybrace }toendthetable.Soanemptytablewouldlooklikethis:

my_cool_table={}

ListsListsareusuallystartedbywritingthefirstitem,thenthesecond,andsoon.Ifwewantedtomakeagrocerylistinsoftware,itmaylooklikethis:

groceries={

[1]="beans",

[2]="bananas",

[3]="buns"

}

Okmaybeyourtypicalgrocerylistlooksdifferent.Whatdowedowiththisdatanowthatwegotit?Wecanaccessandmodifythedataasiftheywerestoredintheirownvariables.

returngroceries[1]

=>beans

Firstwespecifythevariablenameofthetable,theninsquarebracketsweputthenumberwewant.Youcanaccesstheminanyorderandmodifythemasneeded:

print(groceries[3])

groceries[1]="coffeebeans"

print(groceries[1])

buns

coffeebeans

Theorderyoudefinethemindoesn'tmatter:

groceries={

[3]="beans",

[1]="bananas",

[2]="buns"

}

1.13-Tables(part1)

32

Thenumberinsquarebracketsisthekey.Akeythatispartofanumericsequenceofkeyssuchasthislistisoftencalledanindex.So "bananas"hasanindexof 1.Thepluralofindexisindices.

Don'tforgetthecommasbetweeneachiteminyourlistoryouwillgetquitetheerrormessage:

[string"groceries={..."]:3:'}'expected(toclose'{'atline1)near'['

Whenyouaremissingacommabetweenitems,itthinksithasreachedtheendofthetablebutthenerrorsoutwhenitgoestoclosethetablebutseesanotheriteminsteadoftheclosecurlybracket }.

Anotherissueyoumayrunintoisifyoutrytoaccessakeythathasnodata.Thereisno4thiteminourtablesoifwetrytoaccessit:

print(groceries[4])

Wegetback nil,thesamewaywewouldifwetriedtoaccessavariablenamethathasnodataassignedtoit.

Writingoutlargelistscanbecomeaheadachewhenwehavetomanuallynumbereachiteminalist:

groceries={

[1]="beans",

[2]="bananas",

[3]="buns",

[4]="blueberries",

[5]="butter",

[6]="broccoli",

[7]="basil"

}

Whatifweremoveanitemorwewanttomovesomethingtoadifferentpositioninthelist?Whatapaintohavetore-indexeverything.Thankfullythereisshorthandwayofwritinglists:

groceries={

"beans",

"bananas",

"buns",

"blueberries",

"butter",

"broccoli",

"basil"

}

Thisisidenticaltothecodewrittenabove,exceptnowtheindicesareauto-generatedforme. "basil"hasanindexof 7sinceitisthe7thiteminthelist,butifIcutandpastedittothetopofmylist,it'sindexwouldbe 1andeverythingbelowitwouldberenumberedaccordingly.

LoopingoverlistsIfwewantedtoprintourgrocerylist,wecouldsaysomethinglike:

print(groceries[1])

print(groceries[2])

print(groceries[3])

print(groceries[4])

--andsoon...

1.13-Tables(part1)

33

Butthatisquiterepetitiousandrequiresupdatingifthesizeofourlistchanges.Luckilywealreadyknowaboutwhileloops.

index=1

whilegroceries[index]~=nildo

print(index,groceries[index])

--Gotothenextindexinthelist

index=index+1

end

Seehowinsteadofaccessingeachitemas groceries[1], groceries[2]...wecanjustuseavariableinthesquarebracketsinsteadofanumber.Theninsidetheloopwebumpthenumberupandaccessthenextiteminthelist.Theloopstopswhentheindexgoesbeyondthelastiteminthelistandthereisnothingthere.Sowhenindex8isread,groceries[8]isnilandthewhileconditionisnolongertrue.Whileconditionsdon'tevenneedabooleanexpression.Itcanknowwhetherornottocontinuesimplyifthegivenitemhasdataorisnil.Itcanbesimplifiedtoread:

index=1

whilegroceries[index]do

print(index,groceries[index])

--Gotothenextindexinthelist

index=index+1

end

Again,itknowstoexitwhenitsees falseor nil.Thecaveattothiswouldbeifyoumakeaspeciallistwith falseinit:

groceries={

"beans",

"bananas",

false,

"blueberries",

"butter",

"broccoli",

"basil"

}

Whenthewhileloopgetstothethirditeminthelistandsees false,itwouldstoploopingbeforeitreadstherest.It'snottypicallyagoodideatomixandmatchdifferentdatatypesinalistbecauseofissueslikethis,however,wecouldworkaroundthisifweneededto.Thereisaspecialoperatorfortablestogetthesizeofthelist.

print(#groceries)

7

Aneasywaytorememberthe #operatoristorememberthatitreturnsthe#ofitemsinalist.Usingthisoperatorwecouldwriteourwhileloopinadifferentway.

index=1

whileindex<=#groceriesdo

print(index,groceries[index])

--Gotothenextindexinthelist

index=index+1

end

1.13-Tables(part1)

34

Youcouldeventweakthisslightlytoreadthelistbackwardsifyouwantedto:

index=#groceries

whileindex>0do

print(index,groceries[index])

--Gotothenextindexinthelist

index=index-1

end

Noticewearesubtractingfromtheindexwitheachloopinordertoaccomplishthis.

ExercisesTrytomodifythewhilelooptoonlyprinteveryotheriteminthegrocerylist.(Hint:insteadofincrementingby1oneachread,youwanttoincrementmore.)Writeawhileloopthatcountsto10andpopulatesanemptytablewiththesameitem10times.(Hint:youassigntoindicesjustlikevariables, list[index]="hi".)

1.13-Tables(part1)

35

Tables(part2)Inthelastsectionwesawhowsimpleitwastomakealist.Workingwiththelistwasalittletrickyatfirstbuthopefullynottoobad.Ifwerewindback,wecanrememberthatwecreatedatablebyassigningsomekeysvalues.

boxes={

[1]="JohnDoe",

[2]="AmandaParker",

[3]="TylerReese"

}

Thinkofitlikepostofficeboxesandwelabeleachboxwithauniquenumber.Wheneverwereferenceapostalbox,wedosobyreferencingthenumberwithinthearray(list)ofboxes: boxes[2].Thelabel,orkey,isultimatelyarbitrarythough.Formakingalist,welabelthingsinanincrementalordertomakethemeasiertoloopoverandtogiveusasenseoflinearsequence.Keysdon'tneedtobenumbers.Theycouldjustaswellbestrings:

coins={

["half"]="50cents",

["quarter"]="25cents",

["dime"]="10cents",

["nickel"]="5cents",

["penny"]="1cent"

}

Whichwouldbeaccessedjustthesameway:

print(coins["nickel"])

5cents

Thiscanbereallyusefulfordoingalookupifweinsteaduseavariableforthekey.Trythisoneout:

coins={

["half"]="50cents",

["quarter"]="25cents",

["dime"]="10cents",

["nickel"]="5cents",

["penny"]="1cent"

}

print("Whichcoindoyouhave?")

response=io.read()

print("Yourcoinisworth"..coins[response]..".")

Thisisn'tfarofffromhowcertaindatabasesanddigitalserviceswork.Itemsarestoredinauniquekeythatcanbereferencedforgettingadefinitionoutoflater.That'swhythisdatastructureissometimescalledadictionary.Remember,wecanadditemstoatableafteritisdefined:

coins["silverdollar"]="1dollar"

AnothershortcutLuagivesusiswedon'tneedtousethesquarebracesorquoteswhenaddingkeysthatarestrings.

1.14-Tables(part2)

36

coins.nickel="5cents"

Thelimitationwithdoingthisisthekeysdefinedthiswaycan'thavespacesorspecialcharacters.Theymustbevalidinthesamewayvariablenamesarevalid.

coins.silverdollar="1dollar"--INVALID

coins.silver_dollar="1dollar"--Valid

coins.100="1dollar"--INVALID

Youcanusevariablenamesforkeyswhencreatingthetabletoo:

color="purple"

description="thebestcolor"

colors={

[color]=description

}

print(colors.purple)

print(colors[color])--printsthesamething

Byconvention,stringsaretypicallyusedfordictionary-liketableswhilelistsarenumbers.Don'tmakethemistakeofthinkingthesearethesame:

list={

1="someitem",

["1"]="auniqueitem"

}

Youcoulduseotherdatatypesaskeys,butyoumightfindyourresultstobeveryunexpected:

crazy_list={

[true]="works",

[false]="works",

["true"]="notthesame",

["false"]="notthesame"

}

print(crazy_list[true])

print(crazy_list[false])

print(crazy_list["true"])

print(crazy_list["false"])

crazy_key={}

crazy_list={

[crazy_key]="works"

}

print(crazy_list[crazy_key])

crazy_list={

[nil]="doesn'twork!"

}

print(crazy_list[nil])

Throwsanerror:

[string"crazy_list={..."]:2:tableindexisnil

1.14-Tables(part2)

37

Valuesinatablecanbeanytypeofdata,includingfunctions:

cat={

color="gray",

smelly=true,

make_sound=function()

print("meyuow!")

end

}

cat.make_sound()

ExercisesRemembertheearlyfunctionwemadethatreturnedtheanimalsounds?Makeafunctionwithatableinit,whereeachkeyinthetableisananimalname.Giveeachkeyavalueequaltothesoundtheanimalmakesandreturntheanimalsound.Tryinvokingthefunctionandseeifyougetbackthecorrectsound.

1.14-Tables(part2)

38

Forloops(part1)Wesawpreviouslythatwecouldusewhileloopsformanythings,butwealsosawhoweasyitwastomakeawhileloopthatdidn'trunproperly.Theprogrammerhastomakeavariabletopasstothecondition,makesuretheconditioniswrittenoutcorrectly,andthenmakesuretheconditioncanbechangedsotheloopcaneventuallyend.Thismanystepseachtimewewanttowriteasimpleloopleavesuspronetoerrorsandwastingourtime.Withforloops,wecantellaloopexactlyhowmanytimeswewantittorunandskipallthesesteps.

Numericforloops

fornumber=1,10do

print(number)

end

Onthefirstlinewearesaying"For[startingnumber]through[endingnumber]dothefollowing".The number=isavariableyouareassigningthestartingnumberto.Thevariablenamecanbewhateveryouwant.Thesecondlineisthebodyoftheloopandthethirdlineendstheloop.Ifyourunthisprogram,itwillprintthenumbers 1, 2, 3...through 10as numberisbeingincrementedby1witheachloop.Thisvariableisabitpeculiarthough,notonlybecausewedefineditinthemiddleofastatementbutbecauseitdisappearsafterwearedonewiththeloop.

fornumber=1,10do

print(number)

end

returnnumber

=>nil

Thisiscalledalocalvariable,becauseitonlyexistslocallywithintheforloop.

Forloopsactuallyhave3parameters:

startnumber-weassignthevariabletoitandthevariablewillincrementwitheachloopstopnumber-thelastnumbertoincrementtobeforestoppingtheloopstep-howmuchtoincrementbywitheachloop.Ifwedon'tspecifyastepsizeitwilldefaultto1.

Let'ssaywewantedtoprintoutonlyevennumbers.Wecouldchangethestartingnumberto2andsetthesizeofthestep(3rdparameter)to2:

fornumber=2,10,2do

print(number)

end

Ifwewantedtoiterate,orloopoverandreadeachiteminalist,itwouldlooksimilartoawhileloop.Let'slookatthewhileloopexampleagainjustforcomparison:

items={'a','b','c','d'}

index=1

whileindex<=#itemsdo

print(items[index])

index=index+1

1.15-Forloops(part1)

39

end

items={'a','b','c','d'}

forindex=1,#itemsdo

print(items[index])

end

Wecouldalsocountdownbychangingtheparametersaroundandsettingthesteptoanegative1.

items={'a','b','c','d'}

forindex=#items,1,-1do

print(items[index])

end

Inthiscasetheindexstartsatthepositionofthelastitemandstopswhenitgetstothestopnumber,1.

ExercisesModifythepreviousloopsothatitonlyprintseveryotheriteminthelist.

1.15-Forloops(part1)

40

Forloops(part2)Wecancreateadifferentstyleofforloopusingfunctions,butinordertodothat,weneedtounderstandanotheraspectoffunctionswehaven'tyetcovered.Functionscanreturnmultiplevalues.

sort_numbers=function(a,b)

ifa>bthen

returna,b

end

returnb,a

end

bigger,smaller=sort_numbers(12,18)

print(bigger)

print(smaller)

Thisfunctiontakestwonumbers,checkstoseewhichisbigger,thenreturnsboththebiggernumberfirstthenthesmallernumbersecond.Noticewedidthisbyputtingacommainthereturnstatementthenprovidingasecondvalueafterthecomma.Likewise,wewereabletocapturebothvaluesintovariablesbyputtingthefirstvariablename,acomma,thenthesecondvariable( bigger,smaller=).Wedon'tneedtocaptureeverythingreturnedfromafunction.Wecouldhavejustaseasilycalledthefunctionandonlycapturedthebiggernumberifthat'sallwewantedfromit.

bigger=sort_numbers(12,18)

GenericforloopsLet'stakealookatthesiblingtothenumericforloopcalledthegenericforloop.It'scalledgenericforloopbecauseittakesafunctionthatmakesitbehaveindifferentwaysfordifferentsituations.Itdoesn'tdoanythingonitsown.Itreliesonthefunctiontotellithowtobehave.

ipairsHere'swhatgenericforloopslooklike:

list={'dog','cat','mouse'}

forindexinipairs(list)do

print(index,list[index])

end

ipairstakesourforloopandmakesititerateovereachiteminthelistandgivesusan indexvariabletoworkwithinsidetheloop.Butwait,there'smore! ipairsprovidesuswithanothervariablethatholdsthevalueoftheitematthatindex.Tryitoutyourself:

list={'dog','cat','mouse'}

forindex,valueinipairs(list)do

print(index,value)

end

Ahyes,soconvenient!Thereisonegotchawithdoingthis.Ifyouwantedtoeditthetablefrominsidetheloop,youneedtoaccessthetabledirectly:

1.16-Forloops(part2)

41

list={'dog','cat','mouse'}

forindex,valueinipairs(list)do

list[index]=string.upper(value)

end

print(list[1])

Ifyoutrytojusteditthevalue:

list={'dog','cat','mouse'}

forindex,valueinipairs(list)do

value=string.upper(value)

end

print(list[1])

thelistwon'tbemodified,because valueisjustacopyofthedatathat'sactuallyinthelist.You'reeditingatemporarycopy.

pairsAnotherfunctionforprogrammingforloopswithspecialfunctionalityis pairs.Thiswilliterateovereverykeyinatable:

table={

cat='meow',

dog='bark'

}

forkey,valueinpairs(table)do

print(key,value)

end

Evenindices:

table={

'a',

'b',

'c',

cat='meow',

dog='bark'

}

forkey,valueinpairs(table)do

print(key,value)

end

Nosneakingpast pairsforanyofthesekeyseither:

table={

[1]='a',

[2]='b',

[3]='c',

cat='meow',

dog='bark',

[true]=false,

[{}]='what?'

}

forkey,valueinpairs(table)do

1.16-Forloops(part2)

42

print(key,value)

end

Aneasywaytorememberthedifferencebetween ipairsand pairsisthe"i"in ipairsstandsforindex.Surethere'sadifferencewhenworkingwithweirdtablesliketheoneabove,butwhycan'twejustuse pairsforregularlist-styletables?

table={

[2]='b',

[3]='c',

[1]='a'

}

forkey,valueinpairs(table)do

print(key,value)

end

3c

2b

1a

Asyoucansee,theorderoftheitemsisn'tguaranteedwith pairs. ipairsisalsooptimizedtohandlenumerickeysandwillgenerallyperformfaster,soit'sgoodtoknowthedifference.

Underthegeneric-for-loophood

ipairsand pairsarejustregularfunctionsthatweinvoke.Theyreturnafunction(yes,afunctionthatreturnsafunction!)andthisreturnedfunctionprogramsourlooptobehavehowwewant.

forkey,valueiniterator,list,start_numberdo

print(index)

end

Sothisiswhatagenericforloopreallylookslikewithoutthehelpof ipairsor pairs.Itrequires3parametersthatipairs/ pairsprovidesdatabacktothekeyandvaluevariablesthatwecanuseinsidetheloop. iterator, list,start_numberareallvariableswewouldotherwisehavetodefinewithouttheirhelp.

iteratorwouldbeafunctionweprovidetothelooplistwouldbewhatwewanttoiterateoverstart_numberwouldbethestartingindexinthelist

list={'a','b','c'}

iterator,list,start_number=ipairs(list)

forindex,valueiniterator,list,start_numberdo

print(index)

end

ipairsgivesusaniteratortopasstotheforloop,aswellasourlistwealreadyhad,andastartingnumber.Wecanprinttheresultsofipairsandseethe3thingsitgivesus:

print(ipairs(list))

function:0x156a3f0table:0x1572aa00

1.16-Forloops(part2)

43

Sotosayitagain,genericforloopsrequire3things:aniteratorfunction,ourlist,andanumber.Inordertonothavetowritethemourselves,wegeneratedthose3thingsbyinvoking ipairsthenpassingthemintotheforloopparameters.Don'tfrettoomuchifthisseemsconfusingrightnowbecausewe'renotgoingtoneedtowritecustomforloopsorcustomiterators.

Numericversusgeneric:whichtouse?

Numericforloopsaregoodforsimplecountingbutperformjustaswellormaybeevenbetterthangenericforloops.Genericforloopsaremoreadaptable.Ifyouhaveasituationwhereeitherwouldwork,justusewhicheveryouwant.Itreallywon'tmakeanydifference.

ExercisesMakealistandthenwritebothanumericforloopandgenericforloopthatiterateoverthelistandprinteachitem.Comparethetwoapproaches.Makeatablewithanimalsforkeysandthesoundstheymakeforthekeyvalues.Makeaforloopthatusespairstoiterateovereachandchangethenoisestoallcapitalletters.

1.16-Forloops(part2)

44

ScopesWhendefiningfunctions,wedefineparametersforthosefunctionswhichworklikeregularvariables.Ifwetrytoaccessaparameteroutsideafunctionwewillseethatitis nil.

addition=function(a,b)

print(a,b)

returna+b

end

addition(1,2)

print(a,b)

Theparameters aand barelocalvariables.We'veseenlocalvariableswithforloops,wherethevariablecounting_numbercouldn'tbeaccessedoutsidetheforloop:

forcounting_number=1,4do

print(counting_number)

end

print(counting_number)

Functions,forloops,andwhileloopscreateascopeeachtimetheyareran.Thingscreatedinthescope,includinglocalvariables,aredestroyedwhenthatlooporfunctioninvokeisdone.Thisishowtheprogramtidiesupafteritselfandkeepsthecomputerfromrunningoutofmemory.Theprocessofremovingunuseddatafrommemoryandreleasingcontrolofthatmemoryiscalledgarbagecollection.Luadoesthisforussowedon'thavetothinkaboutit.Variableswecreatenormallydon'tfollowthesamerules.Theywillcontinuetoexistafterthescopetheywerecreatedinhasbeendestroyed.

addition=function(a,b)

text="I'mnotgoingaway."

returna+b

end

addition(1,2)

print(text)

Eventuallyallthesevariableswemakewillfillupmemoryunnecessarily.Thiscanalsobeproblematicifweaccidentallymaketwovariablesbutusethesamename.

x=2

addition=function(a,b)

--Thismodifiesthexatthetop!

x=9

returna+b

end

print(x)

result=addition(x,y)

print(x)

1.17-Scopes

45

Whenyouwritealargeprogram,you'llinevitablymaketwovariableswiththesamename,sothiscouldbeahugeissue.Thesolutionistomakeourvariableslocalvariablesbyputtingthekeyword localbeforeallourvariableswhenwecreatethem.

addition=function(a,b)

localtext="I'monlyaccessibleinsidethefunction."

returna+b

end

addition(1,2)

print(text)

Now textisonlyinthescopeofthefunctionandnotgettingintootherpeople'sbusiness.Ifyoudon'twrite localbeforeavariable,thenwhatyouarecreatingisaglobalvariable.It'sashamethatvariablesareglobalunlessweexplicitlytellthemnottobe.Thereisneverareasontocreateglobalvariablesifyouhaveenoughknowledgetoknownotto.Soasabestpractice,allcodeexamplesgoingforward,onlylocalvariableswillbecreated.Let'sseeafewmoreexamples:

localnumber=12

--Thisfunctionhasnoparameters

localprint_numbers=function()

--Thisworks.Youcanseevariablesoutsidethefunction

print('number:',number)

--Thisdoesn'twork.Thevariabledidn'texist

--atthetime"print_numbers"wascreated.

print('number2:',number2)

end

localnumber2=18

print_numbers()

--Wealready"declared"number.Wedon'twrite"local"again.

number=13

print_numbers()

Noticewhenitprintedthatitknew numberwasupdatedto13butcouldn'ttrack number2.Aslongasavariablewascreatedbeforethescope(function'sscopeinthiscase)wascreatedthenitwillalwaystrackthelatestvalue.

Asareminder,wealreadysawwiththeforloopandwhileloopthatyoucanmodifyvariablesintheouter,orparent,scope:

localnumber=1

whilenumber<10do

number=number+1

end

Thisalsoworkswithfunctions:

localnumber=1

localmutate_number=function()

number=7

end

print(number)

mutate_number()

1.17-Scopes

46

print(number)

Whatifyoumaketwovariableswiththesamenameintwodifferentscopes?Tryrunningthisone:

localnumber=18

localshadowing=function()

localnumber=6

print(number)

end

print(number)

shadowing()

Theinner numberdoesnotaffecttheouter numberinanyway.Theouter numberisnotaccessibleinsidethefunctionaslongastheinner numberexists.Ifavariablehasthesamenameasanothervariableinaparentscopethentheparentscopevariablebecomesinaccessible:thisiscalledshadowing.Typicallyyouwouldwanttoavoidshadowingifatleastforthereasonthatusingthesamevariablenametwiceinthesamefilecanmakethecodehardertoreadandmorepronetoerrorsbeingintroduced.

Onemoreinterestingthingswithscopes.Normallyafunctioncannotseeitself:

localself_reference=function()

print(self_reference)--Thiswillbenil!

end

self_reference()

Itdoesn'tseeitselfbecausethevariableisstillbeingcreatedwhenthefunctionisbeingcreated.Butrememberthatifavariableexistsbeforethefunctiondoes,itcanseethelatestup-to-datecontentofthatvariable.Sohere'sthetricktomakethatwork:

localself_reference=nil

self_reference=function()

print(self_reference)

end

self_reference()

Thevariableisdeclared,eventhoughweassigned niltoit.Assigningniltogetavariabledeclaredisprettycommon,soLuaincludesashorthandwayofdeclaringemptyvariables:

localself_reference

self_reference=function()

print(self_reference)

end

self_reference()

Thismayseemsillythatafunctionwouldneedtoaccessitself,buttherearesomeverypowerfulapplicationsforthisthatwewillseelateron.

ExercisesDeclareaglobalvariableinsideafunction, x=5(nolocalkeyword)thentrytoprintthevariablefromoutsidethefunction.Canitbeprinted?How?

1.17-Scopes

47

1.17-Scopes

48

Chapterreview

TerminologyOperatorandOperation-Operatorsaresymbolsthatcauseanoperation,orinteractiontohappenbetweentwopiecesofdata.Anexampleoperationwouldbe 5+5. (5+5)*2wouldbetwooperations.

ModuloandModulus-Moduloisaspecialtypeofarithmeticoperationbetweentwonumbersusingamodulusoperator.Themodulusisrepresentedbya %(percentsymbol).Example: 24%2==0

VariableandValue-Variablesarenamesthatreferenceacertainpieceofdata.Thevalueiswhatisstoredinsidethevariable: variable="value"

Statement-Thisiswhenyoudosomething,likeanoperation(orgroupofoperations),declareavariable,orinvokeafunction.Forinstance,thisisaprintstatement: print("hello")

Invoke-Run/callafunction.

ParameterandArgument-Functionstellyouwhatandhowmanyparameterstheyhave.Argumentsarethedatathatgetspassedintothoseparameters.

Boolean- trueor false

Equality-Whetherornottwothingsareequal.Thisisusuallydonewithanequal( ==)comparison.

Loop-Codethatrepeats.

KeyandIndex-Keyisthenamedreferenceinatablewheredatacanbefound.Itissimilartoavariable.Indexisakeythatcomesinanorderedsequence,suchasnumberedkeysinalist.Thepluralofindexisindices.

Iterate-Loopoveralistanddosomethingwithit.

Scope-Anareawherevariablescanbecreatedthataren'taccessiblefromtheoutside.Scopesarecreatedbyfunctionsandloops.

LocalandGlobal-Localdescribesthingsaccessibleonlyinthecurrentscope,suchaslocalvariables.Globalthingsareaccessiblefromanywhereintheprogram.

Shadowing-Whenalocalvariablehasthesamenameasavariableinaparentscopeandpreventsyoufromaccessingtheparentscopevariable.

1.18-Chapterreview

49

Chapter2:IntroducingLÖVEThegoalofthischapteristoapplyallthebuildingblockswelearnedinthefirstchapterandmakethemconcretethroughpractice.Bytheendofthechapteryouwilllearnreal-worldskillssuchashowtointeractwithotherpeople'ssoftwareandbasicprinciplesfordesigningandbuildingyourownprograms.

2.0-IntroducingLÖVE

50

UpandrunningLearningbymakingisfunandeffective.Learningtointerfacewithotherpeople'ssoftwareispartofbeingaprogrammerandisanecessaryskilltohaveasone.LÖVEisaframeworkformakinggames.Aframeworkisjustasetoftoolsorfunctionalitycombinedtogethertoservealargerpurpose.InthecaseofLÖVEthisincludes,butisnotlimitedto:

Functionsforloadingimages,audio,andtextFunctionsforcreatingandmovingobjectsonscreenParametersformakingtheobjectsinteract

InstallingyourdevelopmentenvironmentTheLÖVEwebsitehaslinkstoinstallthesoftwareonyoursystem.IfyouhaveLÖVEinstalledalready,makesurethatyouatleastversion11(mysteriousmysteries)assomefunctionalitywe'llcoverheredoesn'texistinolderversions.Formobiledevicesyoucanfindacopyintheappstore.

AlongwithinstallingLÖVE,youwillneedatexteditorforcreatingLuafilesonyoursystem.I'mnotgoingtomakeanyrecommendationshere,becauseintheenditallcomesdowntopersonalpreference,butyoucancheckthislistbytheLÖVEcommunityifyouneedastartingpoint.Itfeaturesdifferenteditors(andrecommendedplugins)forLÖVEandLuadevelopment.Simplypickone.

TestthatLÖVErunsWhenyoulaunchLÖVE,(seeinstructionsbelowonhowtodothat)youwillbegreetedwithafriendlygraphicandthetext"NOGAME",meaningyouarerunningtheenginebutdidn'tgiveitagametoload.

macOSOnceyouhavedownloadedtheLÖVEbinaryformacOS(64-bitzipped),proceedtothe"Downloads"folderandunzipthearchive.Youshouldnowseeanapplicationcalled"love".

macOSmightshowyouawarningmodal,becauseyouaretryingtoopenanapplicationbyanunverifieddeveloper.Ifso,rightclickontheapplicationandchoose"open"and"open"againinthefollowingdialog.Youshouldnowbegreetedbytheno-gamescreen.

Addendum:Homebrew

IfyouarefamiliarwithdevelopmentonamacOSmachine,youmighthaveheardofHomebrew.Itisapackagemanagerwhichallowsyoutoinstallalotofprograms,librariesandsoondirectlythroughyourTerminal.

Ionlyrecommendthisapproachforadvanceddeveloperswhoknowwhattheyaredoing.ForcompletenesssakeherearethestepstoinstallLÖVEviaHomebrew.

/usr/bin/ruby-e"$(curl-fsSLhttps://raw.githubusercontent.com/Homebrew/install/master/install)"

brewtapcaskroom/cask

brewcaskinstalllove

2.1-Upandrunning

51

Oneofthebenefitsofthisapproachis,thatyoudon'thavetosetupyourownterminalalias,becauseHomebrewalsotakescareofthat.

Windows

IfashortcutforLÖVEdidn'tappearinthestartmenu,youshouldbeabletotype"love"inthesearchandseeit.

Ubuntu

Openthe"UbuntuSoftware"applicationandsearch"love2d".Clickonthetopresultandyoushouldseeafamiliarapplicationdescription:

Clickthe"Install"buttontoinstallit.Onceinstalled,youcansearchforthe"terminal"application.Oncethatisopen,type lovetolaunchtheapplication.

OtherGNU/LinuxoperatingsystemsMostdistroshaveLÖVEintheirrespectiverepositories:

Archlinux-basedsystems- sudopacman-SyloveFedora-basedsystems- yuminstalllove

Onceinstalledfromyourpackagemanager,openaterminalandtype lovetotestthatitruns.

Ifyourdistrodoesn'thaveLÖVEinthepackagemanageranalternativewaytogetitistodownloadedtheAppImageversionfromthehomepage(https://love2d.org/).AppImagefilesarelikeauniversalexecutablethatworksacrossLinuxsystemssimilartothewayan"exe"fileworksonWindows.Oncedownloaded,opentheterminal,changetothedirectorywhereyoudownloadedtheAppImageandtypethecommands:

chmoda+xlove-11.1-linux-x86_64.AppImage#Marksthefileasasafeexecutable

2.1-Upandrunning

52

./love-11.1-linux-x86_64.AppImage

love-11.1-linux-x86_64.AppImageshouldbechangedtomatchthenameofthedownloadedAppImagefile.

CreateaprojectfolderFindasafeplacetocreateafolderandgiveitthename"hello".Withinthefolder,createanewtextfilenamed"main.lua".Thiswillbewhereourgame'scodegoes.

NoteforWindows:Inordertocreateafilewiththename"main.lua",youmayneedtofirstcreateanew"TextDocument",right-clickonit,click"Properties"thenfromthepropertiesmenuchangethefileextensionfromreading"main.lua.txt"tojust"main.lua".ToavoidhavingtodothisforeveryLuafileyoucreateinthefutureyoucantellWindowstoalwaysshowthefullnameoffiles,includingtheirextension.Toenablethis,type"ControlPanel"intheprogramsearchandopenthe"ControlPanel"result.Withinthecontrolpanel,select"FileExplorerOptions".Clickthe"View"tab.Removethecheckmarkfromthe"Hideextensionsforknownfiletypes"andpressApply/OK.

CreateatestgameWithin"main.lua",writeoutthefollowingfunction:

love.draw=function()

love.graphics.print('HelloWorld!',400,300)

end

Nowlet'sfigureouthowtorunitandseewhatitdoes.

RunthegameThiswillbedifferentfordifferentoperatingsystems.

macOSStartingyourgame

ThesimplestwaytostartaLÖVEgameistodragthewholefoldercontainingthegame'ssourcefiles(notjustthemain.luafile!)ontotheapplicationfile.

2.1-Upandrunning

53

Thisalsoworkswith.lovefiles.

Usingtheterminal

IfyouarefamiliarwiththeTerminal,youcanuseitasamoreconvenientmethodofstartinggames.

Assumingthedownloaded"love"applicationisstillinyour"Downloads"folder,openanewTerminalandtypethefollowinglines(youneedtopressreturnaftereachline):

#SwitchestotheDownloadsfolder

cd~/Downloads/

#StarttheLÖVEapp

openlove.app

ThisobviouslystartsLÖVEwithano-gamescreensincewedidn'tspecifywhichfoldertoload.Let'sfixthisbytypingthefollowingcommand:

#Inmycasethefullcommandtostartthegalaxydemowouldbe:

#open-alove.app~/Downloads/galaxy

open-alove.app<path-to-your-game>

Usingaterminalalias

Wecanstillimproveonthepreviousmethodbyusinganalias.Beforewedothis,wemovethe"love"applicationbundletotheApplicationfolder.

#MovetheappfromDownloadstoApplications.

~mv~/Downloads/love.app/~/Applications/love.app

2.1-Upandrunning

54

Nowtrythefollowingcommand:

#StartLÖVEbyusingthescriptinsideoftheapplicationbundle.

~/Applications/love.app/Contents/MacOS/love<path-to-your-game>

AsyoucanseewenowcanrunLÖVEwithoutusingthe opencommand,whichalsohastheaddedbenefitofshowingthegame'sconsoleoutputdirectlyinourterminal.

Ofcourseitwouldberatherinconvenientifwehadtospecifythefullpatheachtimewewanttorunourgame,sowe'llnowsetupanaliasinyour.bash_profile(whichbasicallyactsasaconfigurationfileforyourbashsessions).

Sinceitisahiddenfileyoumightnotbeabletospotitinyourfinder,butwecansimplyedititthroughourTerminal.

#Appendsthealiasdefinitiontoanexisting.bash_profile

#orcreatesanewone.

echo"aliaslove='~/Applications/love.app/Contents/MacOS/love'">>~/.bash_profile

#Usetheupdated.bash_profileforthecurrentsession.

source~/.bash_profile

#Startyourgamethroughthealias.

love<path-to-your-game>

Andthat'sit:Youcannowquicklyrunyourgameswiththe lovealias.Thisisespeciallyhandyifyouareinsideofthegame'sdirectory,becauseallittakesnowisaquick love.tostartthegame.

Windows

FindtheshortcuttoLÖVEanddraganddropitinthegamefolderlikeso:

Thenright-clickontheLÖVEshortcutandyouwillseea"Properties"dialogwindowsimilartothis:

2.1-Upandrunning

55

The"Target"fieldmaybethesameorslightlydifferentdependingonyoursystemversion.Withoutdeletingthetextstringcurrentlyinthe"Target"field,appendthepathtoyourgamefolderinquotestotheend.Youcancopyandpastethispathfromthefolder'saddressbar.Forinstancethetargetpathinthepictureshouldgofromreading:

"C:\ProgramFiles\LOVE\love.exe"

to

"C:\ProgramFiles\LOVE\love.exe""C:\Users\IEUser\Desktop\hello"

Nowpress"OK"toclosethePropertiesdialogandclickingtheshortcutwilllaunchthegame.Ifthegameransuccessfully,youwillseeablackwindowwiththetext"Helloworld!"insmallprint.

GNU/LinuxIfyouknowthelocationofyourfolder,youcanopenaterminalandtypethecommand:

love<path-to-your-game>

Where <path-to-your-game>hasbeenchangedtotheactualfolderpathwhereyourgameresides.

Ifyouarealreadynavigatedintothegamefolder,youcanrunaterminalcommandwithinthatdirectory:

2.1-Upandrunning

56

love.

The"."simplymeans"thisfolderthatIamcurrentlyin".

Congratulations!You'vesetupyourdevelopmentenvironmentforwritingagameinLua.Ifyouhadissuesgettingthroughthis,reachouttomeeitherthroughaGitHubissueormycontactinformationandIwillupdatethisguidetoincludinganyadditionaltroubleshootingstepsforfutureusers.

Nowthatourdevelopmentenvironmentissetupandourfirstgameisrunning,trymodifyingthecodesothestring"HelloWorld!"readssomethingdifferent.It'sprettyapparentthatrunningthisfunctionprintstothescreenwhateverstringwegiveit.Butwhatarethe2ndand3rdparametersfor?

love.graphics.print('HelloWorld!',400,300)

Trymodifyingthosenumbersandseewhatitdoestothetext.

2.1-Upandrunning

57

LÖVEstructureOpenup"main.lua"andtakealookatourfirstline.Wedefinedafunctioncalled love.draw,whichimpliesthereisatablecalled loveandwecreatedakeyinitcalled draw.Indeedthisisthecase,butsomehowthefunctionwasinvokedwithoutusinghavingtowrite love.draw()andinvokeitourselves.Thisrequiresahigh-levelexplanationofwhattheengineisdoingwithourfile.

WhenLÖVEisrun,beforeourmain.luafileisran,atablecalled loveisdefinedasaglobalvariable.Wecanassignfunctionstothistable( love.draw)andaccessfunctionsalreadydefinedinit( love.graphics.print).love.graphics.printhastwodotsinit,sothatmeansthelovetableprobablylookssomethinglike:

love={

draw=nil,

graphics={

print=function()...end

}

}

The lovetablehasaplentyofothertablesnestedinit,anditputssimilarfunctionsintablestogether.Soallthefunctionsrelatingtographicsareinsidethe love.graphicstable.

Once"main.lua"isdonerunning,we'veaccessedandmodifiedthe lovetableandaddedsomenewfunctionalitytoit,tellingithowtodrawtothescreenbydefiningour love.drawfunction.Ifwedefineafunctionwiththisname,thegameenginewillseeitandinvokeit.Infact,itcontinuouslyinvokes love.drawmanytimesasecond.Toprovemypoint,let'smodifymain.luaandmakeitprintanumber.

localnumber=0

love.draw=function()

number=number+1

love.graphics.print(number,400,300)

end

Eachtimewegotoprintthenumber,weincreaseitby1.Runthisprogramandseehowquicklythenumberclimbs.

The lovetableisaseeminglycomplexstructureoftablesinsidetablesandfunctionsinsidethose,butwe'llgraduallylearnthestructureandpurposeofeachthingoverthecourseofthischapter.Inthenextsection,let'stakealookatthe2ndand3rdparametersin love.graphics.printandseehowtheywork.

2.2-LÖVEstructure

58

GeometryIfyoumodifiedthenumbers 400and 300inmain.luayouwillhaveseenthattheymovethetext.Realizingthatthey'resomekindofcoordinates,let'stalkaboutgraphs.

Whenlearningaboutgraphsingeometryclass,welearnedaboutanx-axisandy-axisandlabeledplottedpointsalongthegraph.Ifyouwantedtomark(-2,-4)thenyouwouldfindwhere-2onthex-axisintersectswith-4onthey-axis.KnowingthatXishorizontalandYisvertical,ifwehad(-2,-4)and(1,2)wecoulddrawitoutlikethis:

Thesetwopointscouldevenbeconnectedtoforma2-dimensionalline:

Beforewegettoofarondrawingpointsandlines,let'slookbackatourfunction:

love.graphics.print("HelloWorld!",400,300)

The 400istheXpositionandincreasingitwillmovethetextfurthertotheright.DecreasingtheXpositionwillmovethetextfurthertotheleft.The 300istheYposition,onedifferencebetweencomputersandgeometryclassisdataiscalculatedfromtoptobottom,soincreasingtheYpositionmovesthetextdownanddecreasingitmovesthetextup.Let'stakealookatwhatourgame'sgraphlookslikewiththepoint(5,3)highlighted:

Noticethatthetop-leftcornerofourscreenis(0,0),soifyoutriedtodrawanypointswithnegativenumberstheywouldbedrawnoffscreenwherewecan'tseethem.Anotherthingtonoteisinthegame,thecoordinatesrepresenthowmanypixelsdownandtotherightwewanttodraw.Sincecomputerscreensaremadeofsomanypixels,youneedtouselargenumberstomakeanoticeabledifference.

Ifwewantedtodrawapolygon(shape)suchasatriangleonthisgraph,wewouldhavetogivethreepoints:

Inthesameway,wecanplotoutpointsinourcodeandtellittodrawalinetoconnectthedots.Let'suselargernumbersthough.Rewritemain.luatolooklikethis:

love.draw=function()

love.graphics.polygon('line',50,0,0,100,100,100)

end

Thenumbersinthiscodecanbereadoffinpairstoidentifythecoordinates:(50,0),(0,100),(100,100)LÖVE'sphysicsenginetakesthecoordinates,startingatthefirstpointandconnectsthemwithalinesequentially.Onceitreachesthelastpoint,itdrawsalinefromthelastpointbacktothefirsttoclosetheshape.

Let'stryarectangletogetsomemorepracticein:

love.draw=function()

localrectangle={100,100,100,200,200,200,200,100}

love.graphics.polygon('line',rectangle)

end

Noticethistimeinsteadofpassingthenumbersdirectlyinto love.graphics.polygon,weputthemintoalistandpassedthelistin.Passingincoordinatesbothwayshasthesameeffect.

Anotherimportantthingtothinkaboutisifyoudrawapolygonwith4ormoresides,youneedtomakesurethepointsarelistedinthecorrectorder.Considerthefollowingexample:

Ifwefedthepointsintothefunctioninaclockwiseorcounter-clockwise/anti-clockwiseorder,therectanglewouldbedrawnthesameeitherway.Ifwefedthepointsinfromcrossdirections,wemayaccidentallydrawabowtie:

Creatingshapeswithunclosedsidesdon'tplaywellwithLÖVE'sphysicsengineassuchshapesarenotphysicallypossible.Ifyoutrytodothis,youmaynotseetheshapeyouexpect,andperhapsnothingwillbedrawn.

2.3-Geometry

59

Creatingshapeswithunclosedsidesdon'tplaywellwithLÖVE'sphysicsengineassuchshapesarenotphysicallypossible.Ifyoutrytodothis,youmaynotseetheshapeyouexpect,andperhapsnothingwillbedrawn.

ExercisesTakealookatthedocumentationforthefunctionlove.graphics.polygon.Theexampleshowstheargument'line'isastringandrepresentsthe"drawMode".Trychangingthe"drawMode"from 'line'tooneoftheotheravailableoptions(seetheexamplesorclickthedrawModelinkonthewikipage).Whatotheroptionisthere?Howdoesitwork?Tryitoutandseehowitworks!Trymakingapolygonwith5sides/points.Hint:usethesquareexampleaboveasareference.

2.3-Geometry

60

GameloopAnotheraspectcommonwithgameenginesisthatthereisaloop(likeawhileloop)thatcontinuouslyrunsandkeepsthegamegoing.Theorderthatthingshappeninvaries,butthecontentsmoreoflesslooklike:

1. Gameisstarted.Loadgamefiles.2. Beginloop.3. Checkforinputfromkeyboard,joystick,orotherperipherals.4. Ticktimeingame.5. Redrawthescreen.6. Gobacktostep2.

Duringstepsinthegameloop,LÖVEinvokescertainfunctionsinsidethe lovetable.Forinstance,everytimethescreenneedstobere-drawn,thegameloopinvokes love.draw()ifyoudefinedit.InthestepwhereLÖVEchecksforuserinput,ifthereisuserinput,itinvokes love.keypressed(PRESSED_KEY)ifwedefinedit.The PRESSED_KEYthatispassesinofcoursedependsonwhatkeytheuserpressed.Whendefining love.keypressed,itmaylooksomethinglikethis:

love.keypressed=function(pressed_key)

print('keywaspressed:',pressed_key)

end

Let'smodifymain.luatohaveacontrivedexample:

localcurrent_color={1,1,1,1}

love.draw=function()

localsquare={100,100,200,200,100,200,100,200}

--Initializethesquarewiththedefaultcolor(white)

love.graphics.setColor(current_color)

--Drawthesquare

love.graphics.polygon('fill',square)

end

love.keypressed=function(pressed_key)

ifpressed_key=='b'then

--Blue

current_color={0,0,1,1}

elseifpressed_key=='g'then

--Green

current_color={0,1,0,1}

elseifpressed_key=='r'then

--Red

current_color={1,0,0,1}

elseifpressed_key=='w'then

--White

current_color={1,1,1,1}

end

end

Whenyoupressanyofthekeys,"b","g","r",or"w",ourfunction love.keypressedwillbeinvokedandthevariablepressed_keywillbeastringmatchingoneofourletters.Thischanges current_color,whichischangingthecolorbeingdrawnin love.draw.

Inthenextsection,let'sseehowLÖVEhandlesthe"4.Ticktimeingame."stepofthegameloop.

2.4-Gameloop

61

ExercisesTryaddingafewmorecolorstotheprogram.Tounderstandhow love.graphics.setColorworks,seethedocumentation.Makeissothatiftheescapekeyispressed,thefunction love.event.quitisinvokedandthegameexits.Thestringtousefortheescapekeycanbefoundonthewiki'sKeyConstantpage.Spoilers:thesolutioncanbeseeninthenextsection.

2.4-Gameloop

62

DeltatimeHere'swhatwe'velearnedaboutthegameloopsofar:

1. Gameisstarted.Loadgamefiles.main.luaisloadedandthe lovetableisupdatedwithourmodifications.

2. Beginloop.3. Checkforinputfromkeyboard,joystick,orotherperipherals.

Iftherewaskeyboardinputandwedefined love.keypressed,invokeit,passingitinformationaboutthepressedkey.

4. Ticktimeingame.???

5. Redrawthescreen.Invoke love.drawifwedefinedit.

6. Gobacktostep2.

Let'stakealookatthefunction love.update:

love.update=function(dt)

print(dt)

end

NoteforWindows:UnlessyouarerunningLÖVEfromtheconsole,youwon'tseeanythingprintedout.Putthisfileinthegamefoldernexttomain.luaunderthename"conf.lua":

--LÖVEconfigurationfile

love.conf=function(t)

t.console=true

end

Thisconfigurationfilewillletussetspecialparametersforourgame.Don'tworrytoomuchaboutwhatalltheoptionsare,butifyou'recuriousthenyoucanfindthedocumentationhere.EssentiallythiswillopenaconsolewindowonWindowstoseeprintedvalues.

Nowrunthegameandifyouweren'tseeingthe print(dt)messagedisplayanythingyoushouldnowseeitbeinginvokedmanytimesasecond,printingoutadecimalnumber. dtstandsfordeltatimeanditrepresentstheamountofsecondsthathaspassedsincethelastgameloop.Ifthegameloops4timesasecond,thatmeans love.updateand love.drawgetinvoked4timeseachsecondaswell.Thedeltatimeinthiscasewouldberoughly 0.25asroughly1/4asecondhaspassedbetweeneachtime love.updatewascalled.Somecomputersarefasterthanotherssothenumberofgameloopspersecondwillbedifferent.Youarelikelyseeingnumbersaround 0.01orsmaller,meaningthegameisrunningroughly100framesasecond.Let'saddacountertothescreenlikebefore,butnowusingdeltatime.

localcurrent_color={1,1,1,1}

localseconds=0

love.draw=function()

localsquare={100,100,100,200,200,200,200,100}

--Printacounterclock

localclock_display='Seconds:'..seconds

love.graphics.print(clock_display,0,0,0,2,2)

2.5-Deltatime

63

--Initializethesquarewiththedefaultcolor(white)

love.graphics.setColor(current_color)

love.graphics.polygon('fill',square)

end

love.keypressed=function(pressed_key)

ifpressed_key=='b'then

--Blue

current_color={0,0,1,1}

elseifpressed_key=='g'then

--Green

current_color={0,1,0,1}

elseifpressed_key=='r'then

--Red

current_color={1,0,0,1}

elseifpressed_key=='w'then

--White

current_color={1,1,1,1}

end

ifpressed_key=='escape'then

love.event.quit()

end

end

love.update=function(dt)

--Addupallthedeltatimeaswegetit

seconds=seconds+dt

end

Imagineifwewantedtomoveacharacteracrossthescreenbutwedidn'tusedeltatime.Thecharacterwouldrunfasteronsomecomputersandsloweronothers.Computerswouldkeepgettingfasterandthegamewouldrunsofastitwouldnolongerbeplayable.Deltatimesolvesthisissueandwe'llbetakingadvantageofitforeverythingtime-basedinourgame.

ExercisesChangetheline localclock_display='Seconds:'..secondssothat secondsisformattedtodisplaywholenumbers.Hint:youwillneedtouseLua'sbuilt-in math.floorfunctiontoformat seconds.Changethexpositionoftheleftsideofthesquarefrom 100to (seconds*10)andwatchwhatthesquaredoes.

2.5-Deltatime

64

MappingLet'ssidetrackfromLÖVEforaminutetolearnaboutaconceptcalledmaps.Nottobeconfusedwithoverheadmapsaplayerwouldwalkaroundoninagame,butdatamaps.Weactuallydidmappingbackinchapter1whenwelearnedabouttables.

coins={

["half"]="50cents",

["quarter"]="25cents",

["dime"]="10cents",

["nickel"]="5cents",

["penny"]="1cent"

}

print("Whichcoindoyouhave?")

response=io.read()

print("Yourcoinisworth"..coins[response]..".")

Whenevertheusertypedinacoin,wemappedthecoinnametoavaluebylookingupthecoinnameinthetable,ordictionary.Sowhat'sthedifferencebetweentables,dictionaries,andmaps?

tablesarejustadatatypeinLuathatcanbeusedtobuilddatastructureslikelistsanddictionariesdictionariesarekey-valuestoragesusedtocentralizesimilar-purposedatainoneplaceandmakeiteasiertolookthedataupmapsaredatastructuresusedtotranslateonetypeofinformationtoanother,andadictionaryisonetypeofmap

Dictionariesaretheonlytypesofmapwe'llbeconcernedabouthere,butknowthatmapsgenerallyrefertoinstancesofdatastructuresthatdomapping.Thereareoftendiscrepanciesinterminologybetweenmathematicsandthevariousfieldsincomputerscience.Don'tbesurprisedifyouseedictionariesandmapsbeingusedsynonymouslyinothercontextslaterinlife.

Let'sdosomemappingonourcodewepreviouslywrotetogetabetterfeelforthem.Rememberallthoseif/elseifstatementsinmain.lua?

ifpressed_key=='b'then

--Blue

current_color={0,0,1,1}

elseifpressed_key=='g'then

--Green

current_color={0,1,0,1}

elseifpressed_key=='r'then

--Red

current_color={1,0,0,1}

elseifpressed_key=='w'then

--White

current_color={1,1,1,1}

end

ifpressed_key=='escape'then

love.event.quit()

end

Wecanputallthatfunctionalityinamaplikethis:

localkey_map={

b=function()

current_color={0,0,1,1}--Blue

end,

2.6-Mapping

65

g=function()

current_color={0,1,0,1}--Green

end,

r=function()

current_color={1,0,0,1}--Red

end,

w=function()

current_color={1,1,1,1}--White

end,

--Closethegame

escape=function()

love.event.quit()

end

}

Thisdoesn'tlookanymoreconcisethanourpreviouscode,butourgoalistokeepthe love.keypressedfunctioncleaninthiscase.Whenakeyispresseditwillbemappedtoakeyfunctionwedefinein key_map.Anotherimportantthingisthesefunctionscouldbemodularandmovedanywhereweneedthemtobe,andevenre-used.Let'snotgotoocrazyrightnowthough.We'llkeepthekeymapsomewherenearthetop.

localcurrent_color={1,1,1,1}

localseconds=0

localkey_map={

b=function()

current_color={0,0,1,1}--Blue

end,

g=function()

current_color={0,1,0,1}--Green

end,

r=function()

current_color={1,0,0,1}--Red

end,

w=function()

current_color={1,1,1,1}--White

end,

escape=function()

love.event.quit()

end

}

love.draw=function()

localsquare={100,100,100,200,200,200,200,100}

--Printacounterclock

localclock_display='Seconds:'..math.floor(seconds)

love.graphics.print(clock_display,0,0,0,2,2)

--Initializethesquarewiththedefaultcolor(white)

love.graphics.setColor(current_color)

love.graphics.polygon('fill',square)

end

love.keypressed=function(pressed_key)

--Checkinthekeymapifthereisafunction

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

love.update=function(dt)

--Addupallthedeltatimeaswegetit

seconds=seconds+dt

end

2.6-Mapping

66

Ifyoupressakeythatisn'tpartofthemapthenthenewifstatement( ifkey_map[pressed_key]...)willseethatkeydoesn'texistinthemapandnotdoanything. key_map[pressed_key]()isthesameassaying key_map['b'](),key_map['escape']()orwhateverthevalueof pressed_keywasatthetime.

2.6-Mapping

67

TheworldAworldisaphysicalspacewhereobjectscanbecreated(spawned)andinteract.Shapesandotherthingsdrawnonthescreenarenotimplicitlypartofaworldandwon'tinteractwitheachotherunlesstheyare.Multipleworldscanco-exist,buttheobjectsineachworldwon'tinteract.GoingforwardIwillrefertotheseobjectsasentities.

EntitiesEntitiesaremadeupofdifferentcomponentsthatallowthemtointeract.Thesearethe3fundamentalphysicalcomponents:

shape-somesortofpolygontogiveourentityaphysicalshapethatdeterminestheboundariesoftheentitybody-holdsphysicalpropertiessuchasmassfixture-attachesashapetoabody

Let'swriteanewmain.luafromscratchandseehowtheseareallwiredup.First,aworldneedstobedefined:

localworld=love.physics.newWorld(0,100)

love.physics.newWorldreturnsatable,aninstanceofaworld.Thetableholdsfunctionsthatallowustoapplyattributestotheworld.Italsoholdsalltheentitiesinourworld,whichiscurrentlynoneoninitialization.Accordingtothedocumentationon love.physics.newWorld,our1stand2ndparameterssettheXandYgravityonourworld.Wedon'twantanysidewaysgravity,butwe'llgoaheadandsetanarbitrarynumberfortheverticalgravity.

Whilefocusingontheworld,weshouldallowtheworldtoknowwheneverwegetanupdatetothedeltatime.Aworldwithouttimewouldbefrozen;Bylettingtheworldknowaboutthepassageoftime,itcanknowwhetheritneedstomakeanentityfallanothermeterortwometers…

love.update=function(dt)

world.update(world,dt)

end

Actually,let'sdoonetrickhere.WhencallingafunctioninLuaandthefirstparameterofthefunctionisthetablethefunctionisstoredin,youcanuseashortcutnotation:

love.update=function(dt)

world:update(dt)

end

Asidefrombeingeasiertowrite,you'llseethiswayofinvokingfunctionsusedallovertheplaceintheLÖVEdocumentation.

Finally,we'lladdanentitytothegamein4steps:

1. Createatabletostoreallthepiecesofourentitytogether.Notentirelynecessarybutwe'lllearnlaterwhythisstepmakesthingseasier.

2. Createabody.Thiswillbeaddedtotheentitytableandtheworld.3. Createtheshapewewanttheentitytohave.4. Createafixturetoattachthebodyandshapetogether.

--Triangleisthenameofourfirstentity

localtriangle={}

2.7-Theworld

68

triangle.body=love.physics.newBody(world,200,200,'dynamic')

--Givethetrianglesomeweight

triangle.body:setMass(32)

triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)

triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)

love.draw=function()

love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))

end

Aftercreatingthe bodytableinside triangle,wecalled triangle.body.setMasstosetaweightpropertyonourtrianglesoitcanfall.Noticewewrote triangle.body:setMass(32),whichisthesameassayingtriangle.body.setMass(triangle.body,32)butshorterandmoreconventionaltothewaytheLÖVEdocumentationwrites.

What'sgoingoninside love.drawlooksprettycrazysolet'sbreakthelonglineup.

love.graphics.polygon(

'line',

triangle.body:getWorldPoints(triangle.shape:getPoints())

)

We'veused love.graphics.polygonpreviouslysoitspurposeshouldalreadybefamiliar.Thefirstparameter 'line'istellingitthatwewantanoutlineofashapedrawn.Thesecondparameterisatablecontainingthepointsthatneedtobeoutlined.Togetthetriangle'spointswecall triangle.shape:getPoints(),butthisonlyreturnstheshapeofthetriangleandtherelativepositionofthepoints.Bythencallingtriangle.body:getWorldPoints(triangle.shape:getPoints())weconvertthoserelativepointstotheirabsolutepositionasthat'swhatthepolygondrawingfunctionneedstoknowsoitcandrawthetriangleexactlywhereitissupposedtobeonthescreen.

Let'sputitalltogetherandaddonemoreentityintothemixsothetwocaninteract:

localworld=love.physics.newWorld(0,100)

--Triangleisthenameofourfirstentity

localtriangle={}

triangle.body=love.physics.newBody(world,200,200,'dynamic')

--Givethetrianglesomeweight

triangle.body.setMass(triangle.body,32)

triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)

triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)

--Anotherentity

localbar={}

bar.body=love.physics.newBody(world,200,450,'static')

bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)

bar.fixture=love.physics.newFixture(bar.body,bar.shape)

localkey_map={

escape=function()

love.event.quit()

end

}

love.draw=function()

love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))

love.graphics.polygon('line',bar.body:getWorldPoints(bar.shape:getPoints()))

end

love.keypressed=function(pressed_key)

--Checkinthekeymapifthereisafunction

2.7-Theworld

69

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

love.update=function(dt)

world:update(dt)

end

Thisisalottodigestsodon'thesitatetore-readthroughthiscodeseveraltimesifnecessary.Therewerealotofnewfunctionsintroducedinthissection,sointhenextsectionwe'lltakeadeeperlookatthedocumentationandreadmoreaboutthemandtheircomponents.

ExercisesTrychangingthemass( triangle.body:setMass)andgravity( love.physics.newWorld)andseewhathappens.

2.7-Theworld

70

ReadingdocumentationYoutypicallyrunintotwotypesofdocumentationforsoftware:guidesandAPIdocumentation.Guideswouldbeinformationongettingstarted,tutorials,andbooks.AnAPI(applicationprogramminginterface)isaportionofsoftwarethataprogrammerwritesforhis/herprogramtoallowfellowprogrammerstointeractwiththeirapplication.AsforLÖVE'sprogramminginterface,mostofyourinteractionswiththeframeworkaredonethroughthe loveglobalvariablethattheframeworkpurposelyexposes.APIdocumentationisthemostfundamentalformofsoftwaredocumentationbecausewithoutit,youwouldnotknowwhatallthefunctionsintheprogramdounlessyouwereresourcefulenoughtogoinandstudyallthesourcecodeandfigureeachfunctionoutonyourown.

ThedocumentationforLÖVE(bookmarkthis!)iswritteninawikistylewhereeachtableandfunctionhasanarticlethatdescribeshowtouseit.Fromhereyoushouldseemanymoduleslisted.Clickon love.physics.Again,love.physicsisjustatablewithfunctionsinit.Sowithinthearticleweseeeachofthefunctionsstoredinitincludingthefunctions love.physics.newBodywhichweusedtocreateourentities'bodies, love.physics.newPolygonShapewhichweusedtocreatetheirshapes,and love.physics.newFixturewhichweusedtocreatetheirfixtures.Wealsosee love.physics.newWorldwhichcreatedourworldtable.Let'slookatthefirstfunction'sarticle.

Clickingonthearticlefor love.physics.newBodywegetasynopsisshowinghowthefunctionmightbeused,alongwithadescriptionofitsparametersandwhatthefunctionreturns.Overthecourseofdifferentversionsoftheframework,functionsmaybemodifiedsointhecaseofthisfunctionyoucanseeexamplesofhowtouseitdifferentlyindifferentversionsofLÖVE.Sincethefunctionliststhatitreturnsabody,clickthelinktogoovertothearticleonthebodytable'sdocumentation.

There'salotoffunctionsherethatsetpropertiesonthebodyandgetpropertiesfromthebody.Oneofthemisthebody:setMassfunctionweusedtogiveourentityweight.Wecanseethatittakesoneparameterandthattheparameterismeanttosimulatehowmanykilogramsofmassthebodyhas.Weoriginallytolditthatourtriangleinthelastsectionweighed32kilograms,whichifyouthinktheobjectfelltoofastortooslowthenyoumayneedtoadjustyourworld'sgravitytomatchyourexpectation.

Nowlet'sgobackto love.physicsforamomentandtakealookatoneoftheothercomponentsweaddedtoourpreviouscode,thefixture.Wepreviouslycreatedafixturetablebycalling triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape).However,wehaven'tseenanyofthesefunctionsinthefixturetablethatcouldcomeinhandy.Forinstance,wecouldgiveourtrianglebouncinessbyinvokingfixture:setRestitution.Ourtrianglefixtureisnamed triangle.fixturethough,not fixture.If0isnobounciness(default)and1is100%,trymodifyingthegamecodeandaddingarestitutionof75%:

triangle.fixture:setRestitution(0.75)

Tryrunningthegameandseehowthatworks.Ifyousettherestitutionto 1orhigherthenthetrianglewon'tstopbouncingandwillbounceitselfrightoffthescreen.

CallbacksLet'sbacktracknowtothemainarticleaboutthelovetable.Ifyouscrolldownalittleonthepage,you'llseeasectiontitled"Callbacks"thatcontainssomefunctionswe'vebecomefamiliarwithsuchas love.drawand love.update.Thisisalistofallthefunctionsinthegameloopthatwehaveandhaven'ttalkedaboutyet.A"callback"isafunctionyoucreateandgivetoanAPI(the lovetableinthiscase)thatwilllatergetinvokedasneeded.Creatingfunctionswiththesenamesallowyoutotapintospecificportionsofthegameloopandtriggeryourownevents.

2.8-Readingdocumentation

71

Let'stakealookatthe love.keypressedcallbackforinstance.Inthesynopsisyouseethatithas3parameters(thedocumentationmistakenlycallsparameters"arguments").Weusedthefirstparameter keytoseewhatkeywaspressed.Ifyoueverneedtoknowwhatkeysareavailable,youcanclickonthelinkprovidednexttotheparametername, KeyConstanttoseeawell-definedlistofalltheavailablekeystringspassedintothisparameter.Thesecondparameter scancodewedidn'ttalkabout,butithasawell-defined Scancodearticleexplainingwhatitis.Ifyouarenotfamiliarwithscancodes,takeaminutetoreaditandperhapsyou'lllearnaboutafeatureyoumaywanttouseinyourgame.

Onemorecallbackwe'lllookatwhilewe'rehereis love.focus.Takeamomenttostophereandreadwhatitdoesandwhatparametersittakesbeforecontinuing.Nowitwouldbereallycoolifweweremakingagameandwhentheuserswitchedtoanotherapplicationwindow,thegameautomaticallypausedfortheuser.Sofirstlet'sstartbyimplementingapausefeatureinourearliergamecode:

localworld=love.physics.newWorld(0,9.81*128)

--Triangleisthenameofourfirstentity

localtriangle={}

triangle.body=love.physics.newBody(world,200,200,'dynamic')

--Givethetrianglesomeweight

triangle.body.setMass(triangle.body,32)

triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)

triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)

triangle.fixture:setRestitution(0.75)

--Anotherentity

localbar={}

bar.body=love.physics.newBody(world,200,450,'static')

bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)

bar.fixture=love.physics.newFixture(bar.body,bar.shape)

--Booleantokeeptrackofwhetherourgameispausedornot

localpaused=false

localkey_map={

escape=function()

love.event.quit()

end,

space=function()

paused=notpaused

end

}

love.draw=function()

love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))

love.graphics.polygon('line',bar.body:getWorldPoints(bar.shape:getPoints()))

end

love.keypressed=function(pressed_key)

--Checkinthekeymapifthereisafunction

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

love.update=function(dt)

ifnotpausedthen

world:update(dt)

end

end

Noticethe3changes:

Weaddedabooleancalled pausedandsetittofalse

2.8-Readingdocumentation

72

Weaddedanewfunctionto key_mapsothatwhen "space"ispressed,thevalueof pausedissetto notpaused. notisanoperatorforbooleanswepreviouslydidn'tdiscuss.Itsimplysays"theoppositeofthisboolean".Soif pausedis true,thensetting pausedto notpausedwillsetitto false.Lastly,inside love.updatetotoldtheworldtoupdateonlyifweare notpaused.Sothepassageoftimeinthegameworldwillceasewhenpressingthespacekey.

ExercisesNowwiththedocumentationinhand,define love.focusandmakeitsothegamepauseswhentheuserclicksawayfromthegamewindow.Bonus:makethegameprintatextsayingthatthegameispausedwhen pausedis true.Gofindthedocumentationfor love.graphics.printtoseeanexampleondisplayingtext.

2.8-Readingdocumentation

73

ModulesandorganizationEventuallywhenyoustartwritingrealprograms,yourealizeifyoukeepallthecodeinonefilethatthingscangetabitmessy.Puttingyourcodeinseparatefileshelpsyounotonlykeepyourdifferentpiecesofcodeseparatedfromeachother,butithelpsyouvisualizethestructureofyourprogram.

Let'sstartwithasinglemain.luafileandwe'llthensplititintodifferentfiles:

localmy_cool_functon=function()

love.graphics.print('Thisfunctioncamefromfunction-module.lua',100,100,0,2)

end

localmy_cool_table={}

my_cool_table.print_stuff=function()

love.graphics.print('Thisfunctioncamefromtable-module.lua',100,200,0,2)

end

print('my_cool_functionis',my_cool_function)

print('my_cool_tableis',my_cool_table)

Whenwegotorunthecode,wegetablankwindowbecausewe'renotdrawinganything.Wedoseeourprintstatementsoutputourfunctionandtabletotheconsolethough:

my_cool_functionisfunction:0x41f2abc0

my_cool_tableistable:0x41f2aa08

Modulesand requireThinkofyourLuafilesasgiantfunctionsthatgetinvokedwheneveryouloadthefile.Justlikeafunction,youcanreturnvaluesfromyourfiles.IfyouloadoneLuafilefromanother,youwillgetwhatevervalueisreturned.Let'smodifyourpreviouscode.Firstdefinethesetwonewfilesinthegamefolder:

function-module.lua

returnfunction()

love.graphics.print('Thisfunctioncamefromfunction-module.lua',100,100,0,2)

end

table-module.lua

localmy_cool_table={}

my_cool_table.print_stuff=function()

love.graphics.print('Thisfunctioncamefromtable-module.lua',100,200,0,2)

end

returnmy_cool_table

Thenupdatemain.lua:

localfunction_module=require('function-module')

localtable_module=require('table-module')

print('function_moduleis',function_module)

print('table_moduleis',table_module)

2.9-Modulesandorganization

74

Let'sstartfromthetop.Infunction-module.luawewriteareturnstatementthatreturnsafunctionwithnoname.Wedon'tinvokethefunction,wejustreturnitasavaluethesamewayafunctionmayreturnanumberorstring.Likewiseintable-module.luawedefinedatable(withafunctioninit)andreturnedthetableonthelastlineofthefile.Thefunctionnameandlocalvariablename my_cool_tableisinconsequentialandcan'tbeseenoutsidethetable-module.luafileasmoduleshavetheirownscope.

Inmain.luawearerequiringfunction-moduleusingabuilt-inLuafunction, require. requiretakesoneargument,astringthatequalsthenameofyourfileandittheninvokesthatfileandreturnsbackafunctionwhichweassigntoanewvariable function_module.Wethendothesamethingfortable-module.lua.Werequireit,whichinvokesitandreturnsbackwhateverthatfilereturns.Inthiscaseisatable.Noticethatwhenwepassinthefilenamesasargumentswejustgivethefirstpartofthefilenamewithouttheextension".lua"attheend.ThisfunctionexpectsthatanyfileitisrequiringisaLuafile.

Afterwerequiredthefiles,weprintedthevaluesofthosevariables,soyoushouldseetheresultsoftheprintstatementsappearintheconsolelikebefore:

function_moduleisfunction:0x40479548

table_moduleistable:0x40479bc8

Wepulledinthereturnvaluesfromtheothertwofilesintoourmain.luafileandprintedthevalues,butsincewedidn'tinvokethefunctionsfromthosetwofilesthenwegotablankgamewindowwhenrunningtheprogram.Let'sdefinealove.drawinmain.lualikebeforeandinvokethefunctionswegotbackfrombothmodules:

localfunction_module=require('function-module')

localtable_module=require('table-module')

print('function_moduleis',function_module)

print('table_moduleis',table_module)

love.draw=function()

function_module()

table_module.print_stuff()

end

Wewereabletoinvokethefunctionsandusethereturneddataasifitwereallinthesamefile.

OrganizingmodulesItmayhelptoseearealexampleofusingmodulesinagame,solet'stakeourpreviousgamecodefrom2.8-Readingdocumentationandseehowwecanseparateoutfunctionality.Thefirstthingwedidinourgamewasdefineaworld,solet'sstartbyputtingourworld-relatedcodeinadedicatedfilenamedworld.lua:

--world.lua

localworld=love.physics.newWorld(0,9.81*128)

returnworld

Rememberthatyouneedthereturnstatementattheendofyourfilesorelsethecodewillreturn nilwhenyougotorequireitandthiscouldcauseallkindsofunexepctederrorswhenyourunit.Nextlet'screateafoldernamedentitiesthatwecankeepallourgameentitiesin.Weplanoncreatingmoreentitiessoitwillhelptokeepthemalltogether.Intheentitiesfolder,createafileandnameittriangle.lua.We'llcutallthecodefromtheoriginalmain.luathatrelatedtoourtriangleentityandputithere:

--entities/triangle.lua

2.9-Modulesandorganization

75

localworld=require('world')

localtriangle={}

triangle.body=love.physics.newBody(world,200,200,'dynamic')

triangle.body.setMass(triangle.body,32)

triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)

triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)

triangle.fixture:setRestitution(0.75)

returntriangle

Noticehowwearerequiringthe worldtablefromworld.lua,becauseweneedtoaccessthattableinthisentity'sfilesowecanaddtheentitytotheworld.Wealsoneedtodothesamethingasabovewiththebarentity:

--entities/bar.lua

localworld=require('world')

localbar={}

bar.body=love.physics.newBody(world,200,450,'static')

bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)

bar.fixture=love.physics.newFixture(bar.body,bar.shape)

returnbar

Nowourmain.luashouldonlycontainourkeymapand lovefunctions:

--main.lua

localbar=require('entities/bar')

localtriangle=require('entities/triangle')

localworld=require('world')

--Booleantokeeptrackofwhetherourgameispausedornot

localpaused=false

localkey_map={

escape=function()

love.event.quit()

end,

space=function()

paused=notpaused

end

}

love.draw=function()

love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))

love.graphics.polygon('line',bar.body:getWorldPoints(bar.shape:getPoints()))

end

love.focus=function(focused)

ifnotfocusedthen

paused=true

end

end

love.keypressed=function(pressed_key)

--Checkinthekeymapifthereisafunction

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

2.9-Modulesandorganization

76

love.update=function(dt)

world:update(dt)

end

Ourtwoentitiesandworldgetpulledintomain.luaandeverythingshouldrunexactlyasbefore.Onethingtonoteisthateventhoughwerequireworld.lua3timesinourcode,itisthesameworldandnot3copies.ThisisbecauseLuaknowstoonlyrunamodulethefirsttimeyourequireitandnotinvokeitagain.Onceitrunsthefirsttime,thereturnedresultsarestoredinmemoryforthenexttimeyoutrytorequireit.Wecanprovethisbyaddingaprintstatementtoworld.lua:

--world.lua

print("Thisistheworld")

localworld=love.physics.newWorld(0,9.81*128)

returnworld

Howmanytimesdoes "Thisistheworld"getprintedtotheconsole?

ExercisesTrycreatingtwonewmodules;Onethatreturnsastringandonethatreturnsanumber.

2.9-Modulesandorganization

77

CollisioncallbacksWhenwritingagamesuchasaplatformeryoumaywantsomethingspecialtohappenwhentwoobjectscollide.Ifit'sapowerup,forinstance,youmaywantthepoweruptodespawn(beremovedfromtheworld)ifaplayertouchesitandthengivetheplayeraspecialability(thinkMarioandmushrooms).Ifaplayerandanenemybumpintoeachother,youmaywanttheplayer'shealthtodecrement.Theworldtablehasamethodthatallowsyoutoprograminfunctionalitylikethisforwhentwoentitiescollide.Itdoesthisbyallowingyoutocreatecallbacksaswelearnedbefore,butthesecallbacksaretriggeredbefore,during,oraftercollision.TakealookatWorld:setCallbacks.

Ifyoulookattheparametersfor World:setCallbacks,youseeitcantakefourfunctions.Thedescriptionoftheseparametershelpsexplainwhenthefunctionswillbecalled. beginContactand endContactshouldbeselfexplanatory;Theyhappenatthepointwherecontactbeginsandendsinacollision,but preSolveand postSolvemaynotbeasobvious.Nonetheless,let'seditthepreviously-createdworld.luafileandwritesomecollisioncallbackstotestthisfunctionality.

--world.lua

localbegin_contact_counter=0

localend_contact_counter=0

localpre_solve_counter=0

localpost_solve_counter=0

localbegin_contact_callback=function()

begin_contact_counter=begin_contact_counter+1

print('beginContactcalled'..begin_contact_counter..'times')

end

localend_contact_callback=function()

end_contact_counter=end_contact_counter+1

print('endContactcalled'..end_contact_counter..'times')

end

localpre_solve_callback=function()

pre_solve_counter=pre_solve_counter+1

print('preSolvecalled'..pre_solve_counter..'times')

end

localpost_solve_callback=function()

post_solve_counter=post_solve_counter+1

print('postSolvecalled'..post_solve_counter..'times')

end

localworld=love.physics.newWorld(0,9.81*128)

world:setCallbacks(

begin_contact_callback,

end_contact_callback,

pre_solve_callback,

post_solve_callback

)

returnworld

Tryitout.Everytimeoneofthecallbacksisinvoked,itwillincrementitsownnumberby1thenprintamessagetotheconsoletellingyouhowmanytimesithasbeeninvoked.It'sclearrightawaythat pre_solve_callbackandpost_solve_callbackgetinvokedmanymoretimesthan begin_contact_callbackand end_contact_callbackinthissituation.

2.10-Collisioncallbacks

78

Unlessyou'veeditedthebehaviorofthetriangleentity,itwillbounceabit(becauseofthetriangle'srestitution).Onceitbouncesandneithercornerorsideistouchingthefloorunderneath,thecontactends.Thisprocessisrepeatedeverytimeitbounces.Oncethetrianglesettlesdownitwillslideabit,maybeevenalot...likeanairhockeypuck.Thisisbecauseourtriangleandbarhavenofrictionbetweenthemtopreventthat.Anyways,thisisgoodbecauseitallowsustoseethatwhilethetriangleisslidingitisstillmakingcontact.Whilethetriangleisslidingandstillmakingcontact,the pre_solve_callbackand post_solve_callbackwillcontinuetogetcalledwitheveryframeofmovement.

Pretendourtrianglewasafuturisticracecarmovingacrossaneonstripofroadthatrechargedthevehicle.Youcouldstartincreasingtheracecar'spowermeterinside begin_contact_callbackasthecarmakescontactwiththatsectionofroadandthenstopincreasingpowerwhen end_contact_callbackisinvoked.Thiscouldworkprettywell,butthentheplayermaytryparkingforamomentonthepowerstripandcontinuetogainhealthaslongastheywant.Soanotherapproachcouldbetoonlyincreasethepowermeterastheplayercontinuestomoveandmakecontactwiththeroad,increasinghealthby1pointeverytimethe post_solve_callbackfunctionisinvoked.

Youdon'tnecessarilyneedtouseallofthesecallbacks,soyoucouldjustpassinanemptyfunctionor niltoWorld:setCallbacksfortheargumentsyoudon'tneed.

Withoutknowingwhatentitiesarecolliding,thecollisioncallbacksaren'tveryuseful.Luckily,ourcallbackshaveparametersoftheirownthatwecanaccess.Let'smodifythecodeagainandcheckoutthoseparameters:

--world.lua

--Calledatthebeginningofanycontactintheworld.Parameters:

--{fixture}fixture_a-firstfixtureobjectinthecollision.

--{fixture}fixture_b-secondfixtureobjectinthecollision.

--{contact}contact-worldobjectcreatedonandatthepointof

--contact.Whenslidingalonganobject,theremaybeseveral.

--Seefurther:https://love2d.org/wiki/Contact

localbegin_contact_callback=function(fixture_a,fixture_b,contact)

print(fixture_a,fixture_b,contact,'beginningcontact')

end

localend_contact_callback=function(fixture_a,fixture_b,contact)

print(fixture_a,fixture_b,contact,'endingcontact')

end

localpre_solve_callback=function(fixture_a,fixture_b,contact)

print(fixture_a,fixture_b,contact,'abouttoresolveacontact')

end

localpost_solve_callback=function(fixture_a,fixture_b,contact)

print(fixture_a,fixture_b,contact,'justresolvedacontact')

end

localworld=love.physics.newWorld(0,9.81*128)

world:setCallbacks(

begin_contact_callback,

end_contact_callback,

pre_solve_callback,

post_solve_callback

)

returnworld

Thisshouldprintoutsomeinformationintheconsolesimilarto:

Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480beginningcollision

Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480abouttoresolveacontact

Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480justresolvedacontact

Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480endingcollision

2.10-Collisioncallbacks

79

Fixture:0x561020bf8570isatextrepresentationofourfirstentity'sfixture.The 0x56...isthememoryaddressofthefixturetohelpidentifyit,althoughthisinformationstilldoesn'ttelluswhichentitythisfixturebelongsto.Wealsoprintedoutacontacttable,whichcontainsasetoffunctionsjustliketheentities.Thisinstanceofacontactprovidesinformationsuchaswherethecontacthappenedandhowmuchvelocitywasinvolved.

Let'sworkonmodifyingtheprintstatementssowecancollectmoreusefulinformationonthesecollisions.Thereisapairoffunctionsoneveryfixturethatlet'syousetanyarbitrarydatayouwantonthatfixtureandanotherfunctiontogetthatdatabackoutthefixture.Thesefunctionsarecalled Fixture:setUserDataand Fixture:getUserData.ThesefunctionscanbeusedtosetanameorIDonthefixturetohelpusidentifywhatentityitbelongsto.Wecanaccomplishthisbyfirstmodifyingourentityfilesandpassingsomestringsto Fixture:setUserData:

--entities/bar.lua

localworld=require('world')

localbar={}

bar.body=love.physics.newBody(world,200,450,'static')

bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)

bar.fixture=love.physics.newFixture(bar.body,bar.shape)

bar.fixture:setUserData('bar')

returnbar

--entities/triangle.lua

localworld=require('world')

localtriangle={}

triangle.body=love.physics.newBody(world,200,200,'dynamic')

triangle.body.setMass(triangle.body,32)

triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)

triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)

triangle.fixture:setRestitution(0.75)

triangle.fixture:setUserData('triangle')

returntriangle

Nowgobacktotheworld'scollisioncallbacksandyoucaneasilyextractthisinformationbackoutofthefixtures:

--world.lua

--Calledatthebeginningofanycontactintheworld.Parameters:

--{fixture}fixture_a-firstfixtureobjectinthecollision.

--{fixture}fixture_b-secondfixtureobjectinthecollision.

--{contact}contact-worldobjectcreatedonandatthepointof

--contact.Whenslidingalonganobject,theremaybeseveral.

--Seefurther:https://love2d.org/wiki/Contact

localbegin_contact_callback=function(fixture_a,fixture_b,contact)

localname_a=fixture_a:getUserData()

localname_b=fixture_b:getUserData()

print(name_a,name_b,contact,'beginningcontact')

end

localend_contact_callback=function(fixture_a,fixture_b,contact)

localname_a=fixture_a:getUserData()

localname_b=fixture_b:getUserData()

print(name_a,name_b,contact,'endingcontact')

end

localpre_solve_callback=function(fixture_a,fixture_b,contact)

localname_a=fixture_a:getUserData()

2.10-Collisioncallbacks

80

localname_b=fixture_b:getUserData()

print(name_a,name_b,contact,'abouttoresolveacontact')

end

localpost_solve_callback=function(fixture_a,fixture_b,contact)

localname_a=fixture_a:getUserData()

localname_b=fixture_b:getUserData()

print(name_a,name_b,contact,'justresolvedacontact')

end

localworld=love.physics.newWorld(0,9.81*128)

world:setCallbacks(

begin_contact_callback,

end_contact_callback,

pre_solve_callback,

post_solve_callback

)

returnworld

Ah,nowwecanseewhichfixtureiscollidingwhich!

bartriangleContact:0x55bf29c07590beginningcontact

bartriangleContact:0x55bf29c07590abouttoresolveacontact

bartriangleContact:0x55bf29c07590justresolvedacontact

bartriangleContact:0x55bf29c07590endingcontact

ExercisesModifytheprintstatementsineachcollisioncallbacktoprintoutthecoordinateswheretheentities'fixturesaremakingcontact.Youcanfindtheinformationyouneedtodothisinthedocumentationforthecontacttablementionedabove.

2.10-Collisioncallbacks

81

Breakout(part1):moreentitypracticeLet'sbringalltheseconceptstogetherbymakinganothergame,abreakoutclone.Therequirementsareprettysimple:

TheobjectiveofthegameistodestroyallthebricksonthescreenTheplayercontrolsa"paddle"entitythathitsaballTheballdestroysthebricksTheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen(belowthepaddle),thegameends

Ifyoustillhavethecodefromtheprevioussections,feelfreetocopythefoldernamingthenewone"breakout"orwhateveryouwantyourbreakoutclonetobecalled.Attheendofthissectiontherewillbealinktoallthesourcecodetouseasareferenceincaseyougetstuck.Thismaybetimeconsuming,butIencourageyoutotypeouteachsectionandstoptounderstandwhatitisyouaretyping.Ifyoucopy,paste,anddon'treadthenitwillbeeasytogetlostinthischunkofthechapterasthingswillmovefast.

Thefirstmodificationwe'llmakeistosetaspecificwindowsizesonomatterwhichversionofLÖVEyou'reonwe'reworkingwiththesamewindowproportionsandentitydimensions.Todothis,openofconf.luaorcreateitifyoudon'thaveitandputinthefollowingcode:

--conf.lua

--LÖVEconfigurationfile

love.conf=function(t)

t.console=true--EnablethedebugconsoleforWindows.

t.window.width=800--Game'sscreenwidth(numberofpixels)

t.window.height=600--Game'sscreenheight(numberofpixels)

end

Theconf,orconfigurationfileletsyoudefineacallbackinthe lovetablethatmodifiesthegameengine'sconfigurationonload.Youcanreadmoreaboutalltheinterestingthingsyoucandowithitherebutmostofitsfeatureswon'tbenecessaryforoursimplegame.

Thenextmodificationwe'llmakeisdeletingtheentitiesfromthelastsection.Let'screatenewentitiestorepresenttheballandpaddle:

--entities/ball.lua

localworld=require('world')

localentity={}

entity.body=love.physics.newBody(world,200,200,'dynamic')

entity.body:setMass(32)

entity.body:setLinearVelocity(300,300)

entity.shape=love.physics.newCircleShape(0,0,10)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setRestitution(1)

entity.fixture:setUserData(entity)

returnentity

--entities/paddle.lua

localworld=require('world')

2.11-Breakout(part1)

82

localentity={}

entity.body=love.physics.newBody(world,200,560,'static')

entity.shape=love.physics.newRectangleShape(180,20)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

returnentity

Beforewetryandrunanything,takealookatafewthingswe'vedonedifferentlyindefiningtheseentitiesthanwe'vepreviouslydone.

Inball.luawearedefiningacircleshapeinsteadofapolygon.Thismeanswehavenosidesorcornerpointswecanreferencewhenspawningortrackingthepositionofthisobject.Circleshavetobetrackedfromtheircenterpointandtheirboundariesbytheirradius.Inthisfilewe'reusing Body:setLinearVelocitytoapplymovementontheballinaspecificdirectionwhentheentityspawns.Inpaddle.luawearedefiningapolygonshape,butinsteadofspecifyingeachpointweareusingthelove.physics.newRectangleShapefunctiontodefinetheshape.Thiswillstillgenerateapolygonasbefore,butinsteadofspecifyingeachpointintheshapewearegivingaheightandwidthandallowingittofigureouttheshapewewantbasedonthosetwoparameters.Thinkofitasashortcutversionofthelove.physics.newPolygonShapefunction.Thepaddlehasastaticbodywhiletheballisdynamic.Whatthisentailsistheballwillbeaffectedbythepaddlebutthepaddlewon'tbeaffectedbytheball.Eventhoughthepaddleisstatic,itcanbemanuallyrepositionedaswe'lldolaterwithbuttons.Inbothentityfiles,wearepassingthefullentitytableasthefixtureuserdatainsteadofjustastringnamelikebefore.Thiswillallowustoeasilyaccesstheentireentityinsidethecollisioncallbackaswe'llseelater.You'llwanttogobackandcomparethatcodefromtheCollisionCallbackssectiontotheseentities,butdon'tworryifitdoesn'tmakecompletesenseyet.

Nowweneedtomodifymain.luatoloadupournewentities:

--main.lua

localpaddle=require('entities/paddle')

localball=require('entities/ball')

localworld=require('world')

--Booleantokeeptrackofwhetherourgameispausedornot

localpaused=false

localkey_map={

escape=function()

love.event.quit()

end,

space=function()

paused=notpaused

end

}

love.draw=function()

localball_x,ball_y=ball.body:getWorldCenter()

love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())

love.graphics.polygon(

'line',

paddle.body:getWorldPoints(paddle.shape:getPoints())

)

end

love.focus=function(focused)

ifnotfocusedthen

paused=true

2.11-Breakout(part1)

83

end

end

love.keypressed=function(pressed_key)

--Checkinthekeymapifthereisafunction

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

love.update=function(dt)

ifnotpausedthen

world:update(dt)

end

end

Takenoteofafewthingswe'redoinghere:

Fordrawingthecircle,weneedtoinvoke love.graphics.circle.Fordrawingthepaddle,westillinvoke love.graphics.polygonastherectangleisstillapolygonshape.

Nowlet'sremoveanyprintstatementsinworld.luajusttocleanthingsup.We'llleavethecallbackstheresincewemayusethemlaterbutwe'llleavethememptyfornow.We'llalsosetthegravityto 0becausewewanttheballtobouncefreelylikeintherealBreakoutgameandnotloseanymomentum.

--world.lua

--Calledatthebeginningofanycontactintheworld.Parameters:

--{fixture}fixture_a-firstfixtureobjectinthecollision.

--{fixture}fixture_b-secondfixtureobjectinthecollision.

--{contact}contact-worldobjectcreatedonandatthepointof

--contact.Whenslidingalonganobject,theremaybeseveral.

--Seefurther:https://love2d.org/wiki/Contact

localbegin_contact_callback=function(fixture_a,fixture_b,contact)

end

localend_contact_callback=function(fixture_a,fixture_b,contact)

end

localpre_solve_callback=function(fixture_a,fixture_b,contact)

end

localpost_solve_callback=function(fixture_a,fixture_b,contact)

end

localworld=love.physics.newWorld(0,0)

world:setCallbacks(

begin_contact_callback,

end_contact_callback,

pre_solve_callback,

post_solve_callback

)

returnworld

Whathappensifyourunthegamenow?Theballfliesrightoffthescreenwithoutconsequence.Thereareacoupledifferentwaysofpreventingtheballfrommovingoffscreen.Possiblythemostsimpleapproachistoputupsomewalls.Canyouguesswhatthecodetothosewallsmaylooklike?Yup,theywillbeentitiessimilartothepaddleexceptthattheyjustsitattheedgesofthescreen.Let'screatesomeentitiesforthatpurpose:

--entities/boundary-top.lua

2.11-Breakout(part1)

84

localworld=require('world')

localentity={}

entity.body=love.physics.newBody(world,400,5,'static')

entity.shape=love.physics.newRectangleShape(800,10)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

returnentity

Takealookatthesenumbersforaminute.Forthelocationofthebodywespecified400pixels.Sostartingfromthetopleftcornerandmovingrightalongthex-axiswe'vespecifiedtheverycenterofan800-pixel-widewindow.Thereasonwe'vedonethisisbecausewewantthetopandbottomwallboundariestostretch800pixelswide,theentirelengthofthewindow,andwhencalling newBodyandspawninganentity'sbodyitwillspawnthecenterpointoftheentityshapeatthatlocation.Notallentityshapesaresquare,orevenpolygonal,soitissimplestforthegameenginetocentertheshapeonthebody'sspawnpointratherthanusinganotherpointofreferenceontheshape,likethetopleftcorneroftheshape(notallshapeshavecorners).Infact,theballandpaddlespawnedcenteredonthelocationwegavefortheirbodies.

Sowemadethewalls800pixelswideandjusttogiveitalittlevisibilitywemadethem10pixelstall.Youwouldthinkwe'dspawnthewallattheverytopofthescreen(0pixelsonthey-axis,)butsinceourwallswillbecenteredtothespawnpointsweshouldmovedownhalftheheightofthewallifwewantitalltoappearonscreen.

Nowtheboundaryonthebottomwillhavethesamedimensions,butitwillbespawnedatthebottomofthescreen(600pixels)minushalftheheightofthewall(5pixels):

--entities/boundary-bottom.lua

localworld=require('world')

localentity={}

entity.body=love.physics.newBody(world,400,595,'static')

entity.shape=love.physics.newRectangleShape(800,10)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

returnentity

Theleftandrightboundarieswillfollowthesamepatternexcepttheywillbetheheightofthescreeninsteadofthewidthofthescreen:

--entities/boundary-left.lua

localworld=require('world')

localentity={}

entity.body=love.physics.newBody(world,5,300,'static')

entity.shape=love.physics.newRectangleShape(10,600)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

returnentity

--entities/boundary-right.lua

localworld=require('world')

localentity={}

entity.body=love.physics.newBody(world,795,300,'static')

entity.shape=love.physics.newRectangleShape(10,600)

2.11-Breakout(part1)

85

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

returnentity

Wewon'tseetheseentitiesuntilwerequirethemanddrawthemonthescreen.Somodifymain.luatorequireanddrawthemthesamewaywedotheballandpaddle:

--main.lua

localboundary_bottom=require('entities/boundary-bottom')

localboundary_left=require('entities/boundary-left')

localboundary_right=require('entities/boundary-right')

localboundary_top=require('entities/boundary-top')

localpaddle=require('entities/paddle')

localball=require('entities/ball')

localworld=require('world')

--Booleantokeeptrackofwhetherourgameispausedornot

localpaused=false

localkey_map={

escape=function()

love.event.quit()

end,

space=function()

paused=notpaused

end

}

love.draw=function()

love.graphics.polygon('line',boundary_bottom.body:getWorldPoints(boundary_bottom.shape:getPoints()))

love.graphics.polygon('line',boundary_left.body:getWorldPoints(boundary_left.shape:getPoints()))

love.graphics.polygon('line',boundary_right.body:getWorldPoints(boundary_right.shape:getPoints()))

love.graphics.polygon('line',boundary_top.body:getWorldPoints(boundary_top.shape:getPoints()))

localball_x,ball_y=ball.body:getWorldCenter()

love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())

love.graphics.polygon('line',paddle.body:getWorldPoints(paddle.shape:getPoints()))

end

love.focus=function(focused)

ifnotfocusedthen

paused=true

end

end

love.keypressed=function(pressed_key)

--Checkinthekeymapifthereisafunction

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

love.update=function(dt)

ifnotpausedthen

world:update(dt)

end

end

2.11-Breakout(part1)

86

Whenyourunthegame,youshouldseeprettymuchthesamethingasthis:

Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-1

Lookingbackatourlistofminimalrequirementswe'vealreadycompletedonethingonourlist:

Theballneedstostaywithintheboundariesofthescreen

There'sstillquiteabitmoreworktocompletethislistsolet'scontinueinthenextsection.

ExercisesMaybeitwouldbebetteriftheboundarylineswereevenwiththescreensowecouldn'tseethem.Modifytheboundarypositionssoitlooksliketheballisbouncingofftheedgeofthescreen.Whathappensifwe requiretheboundariesbutdon'tdrawthemin love.draw?Doesthegamestillwork?

2.11-Breakout(part1)

87

Breakout(part2):entitymanagement

ReviewIntheprevioussectionwemadeachecklistofrequirementsandaccomplishedoneofthem:

TheobjectiveofthegameistodestroyallthebricksonthescreenTheplayercontrolsa"paddle"entitythathitsaballTheballdestroysthebricks✔TheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen,thegameends

Inthepreviousexercise,thegoalwastomovetheboundariessotheywerejustoffscreen.Thisgivestheeffectthattheballisbouncingofftheedgesofthegamewindow.

--entities/boundary-bottom.lua

entity.body=love.physics.newBody(world,400,606,'static')

--entities/boundary-left.lua

entity.body=love.physics.newBody(world,-6,300,'static')

--entities/boundary-right.lua

entity.body=love.physics.newBody(world,806,300,'static')

--entities/boundary-top.lua

entity.body=love.physics.newBody(world,400,-6,'static')

Heretheyhavebeenmoved6pixelsoffscreenjusttouseevennumbersandmakecalculationeasier.Previouslywealsoraisedthequestionofwhetherornottheboundarieswouldworkifwestill require'dtheminmain.luabutdidn'tdrawthemin love.draw.Theansweristheystillworkbutwedon'tseethem.Sincetheyareoffscreen,thatdoesn'tmatteranywayandwecansaveourprogramfromdoingextrawork:

--main.lua

love.draw=function()

localball_x,ball_y=ball.body:getWorldCenter()

love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())

love.graphics.polygon('line',paddle.body:getWorldPoints(paddle.shape:getPoints()))

end

EntitylistLet'sthinkabouttheproblemofbrickentitiesforaminute.Wecouldcreateanentityfileforeachbrick,buttheyaremoreorlessthesameexceptthattheyspawnindifferentspots.Imaginemaking50differententityfilesandtheninside love.drawmaking50linestodraweachbrickandsoon.Whatwecaninsteaddoismakeanentityfilefor1brickthenmakealistwith50copiesofit(orhowevermanybrickcopiesweendupfittingonthescreen).Wecanthenloopoverthislisttodrawthebricks.

Let'sfirstcreatethebrickentityfile:

2.12-Breakout(part2)

88

--entities/brick.lua

localworld=require('world')

returnfunction(x_pos,y_pos)

localentity={}

entity.body=love.physics.newBody(world,x_pos,y_pos,'static')

entity.shape=love.physics.newRectangleShape(50,20)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

returnentity

end

Insteadofreturninganentityinthisfile,wereturnedafunctionthattakesanx-positionandy-positionasparameters.Whenthefunctiongetsinvokedwhereveritisrequired,itwillgenerateanewentitywiththosecoordinatesforitsspawnpoint.Here'showwecanuseit:

--main.lua

localboundary_bottom=require('entities/boundary-bottom')

localboundary_left=require('entities/boundary-left')

localboundary_right=require('entities/boundary-right')

localboundary_top=require('entities/boundary-top')

localpaddle=require('entities/paddle')

localball=require('entities/ball')

localbrick=require('entities/brick')

localentities={

brick(100,100),

brick(200,100),

brick(300,100)

}

localworld=require('world')

--Booleantokeeptrackofwhetherourgameispausedornot

localpaused=false

localkey_map={

escape=function()

love.event.quit()

end,

space=function()

paused=notpaused

end

}

love.draw=function()

localball_x,ball_y=ball.body:getWorldCenter()

love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())

love.graphics.polygon('line',paddle.body:getWorldPoints(paddle.shape:getPoints()))

for_,entityinipairs(entities)do

love.graphics.polygon('fill',entity.body:getWorldPoints(entity.shape:getPoints()))

end

end

love.focus=function(focused)

ifnotfocusedthen

paused=true

end

end

love.keypressed=function(pressed_key)

2.12-Breakout(part2)

89

--Checkinthekeymapifthereisafunction

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

love.update=function(dt)

ifnotpausedthen

world:update(dt)

end

end

Wemadeanentitytablewithalistofbrickentitiesinit,thenin love.drawwemadeaforlooptodraweachentityinthelist.Beforewechangeanythingelsetryrunningthegameandtakingalookthatthebricksappearandthateverythingworks.

RuleofsingleresponsibilityOurgoalfortherestofthissectionwillbetosimplifyentitymanagement.Onestrategywe'llhavefordoingthisistothinkofeachfileinourgameashavingasingleresponsibility.Agoodsignthatwe'redoingthisismain.luaisverysmallandeasytoscanoverwiththeeyesanddigest.

Sowhatistheresponsibilityofmain.lua?

Createthecallbackfunctionsnecessarytorunthegame.

Here'ssomethingsitisdoingthatdon'tfitthatresponsibility:

LoadandstorealltheentitiesFigureouthowtodraweachtypeofentityin love.drawStoreamapofkeypresses

Imagineourgameisanorganizationandeachfileisaroleinthecompany.Ourmainfileislikethesecretarythatknowshowtohandlerequestsfromoutsiders.Ifsomebodycalledaskingthesecretaryaboutbuilding-maintenanceissues,thesecretarywouldn'tgrabplumbingtoolsandtakecareoftheproblembutratherdispatchthepersonwhoseresponsibilityisthatexactkindofproblem.Astheownerofthisorganizationweshouldknoweveryone'srolessoit'seasytoknowwhereeachresponsibilitylies.Itwillmakeiteasierforustogrowthecompanytothesizewedesire.

Oneeasyimprovementistonotwriteoutalltheinstructionsfordrawingeachentitywithinthemainfile,butratherleteachentityfileberesponsibleforeveryfeatureofthatentity,includinghowtodrawthatentity.Wemaywanttogetfancylateranddrawbricksindifferentcolors,forinstance.Thatcouldgetcomplicatedandwedon'twantthemainfiletoretainabunchofcodeaboutbrickcolorsandsuch.

Modifyingtheentitiesisaseasyascreating drawfunctionsintheentitytables:

--entities/brick.lua

localworld=require('world')

returnfunction(x_pos,y_pos)

localentity={}

entity.body=love.physics.newBody(world,x_pos,y_pos,'static')

entity.shape=love.physics.newRectangleShape(50,20)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

entity.draw=function(self)

love.graphics.polygon('fill',self.body:getWorldPoints(self.shape:getPoints()))

end

2.12-Breakout(part2)

90

returnentity

end

--entities/paddle.lua

localworld=require('world')

returnfunction(pos_x,pos_y)

localentity={}

entity.body=love.physics.newBody(world,pos_x,pos_y,'static')

entity.shape=love.physics.newRectangleShape(180,20)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

entity.draw=function(self)

love.graphics.polygon('line',self.body:getWorldPoints(self.shape:getPoints()))

end

returnentity

end

--entities/ball.lua

localworld=require('world')

returnfunction(x_pos,y_pos)

localentity={}

entity.body=love.physics.newBody(world,x_pos,y_pos,'dynamic')

entity.body:setMass(32)

entity.body:setLinearVelocity(300,300)

entity.shape=love.physics.newCircleShape(0,0,10)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setRestitution(1)

entity.fixture:setUserData(entity)

entity.draw=function(self)

localself_x,self_y=self.body:getWorldCenter()

love.graphics.circle('fill',self_x,self_y,self.shape:getRadius())

end

returnentity

end

Goaheadandmakealltheentitiesreturnafunctionwith x_posand y_posparametersandwe'lljustaddeverythingtotheentitylistlikethebricks.Don'tforgettochangeoutthenumbersinthe love.physics.newBody(world,200,200,'dynamic')withtheargumentsbeingpassedinbythefunction: love.physics.newBody(world,x_pos,y_pos,'dynamic').Fortheboundariesentityfilesthereisnoneedfor entity.drawfunctions,butstillmakethemreturnfunctionswiththetwoparameters.Nowupdatethe entitieslistinmain.luatoincludealltheentities:

--main.lua

localboundary_bottom=require('entities/boundary-bottom')

localboundary_left=require('entities/boundary-left')

localboundary_right=require('entities/boundary-right')

localboundary_top=require('entities/boundary-top')

localpaddle=require('entities/paddle')

localball=require('entities/ball')

localbrick=require('entities/brick')

localentities={

boundary_bottom(400,606),

boundary_left(-6,300),

2.12-Breakout(part2)

91

boundary_right(806,300),

boundary_top(400,-6),

paddle(300,500),

ball(200,200),

brick(100,100),

brick(200,100),

brick(300,100)

}

localworld=require('world')

--Booleantokeeptrackofwhetherourgameispausedornot

localpaused=false

localkey_map={

escape=function()

love.event.quit()

end,

space=function()

paused=notpaused

end

}

love.draw=function()

for_,entityinipairs(entities)do

ifentity.drawthenentity:draw()end

end

end

love.focus=function(focused)

ifnotfocusedthen

paused=true

end

end

love.keypressed=function(pressed_key)

--Checkinthekeymapifthereisafunction

--thatmatchesthispressedkey'sname

ifkey_map[pressed_key]then

key_map[pressed_key]()

end

end

love.update=function(dt)

ifnotpausedthen

world:update(dt)

end

end

Takealookatour love.drawfunction.Itismuchsimplernowthatitnolongerneedstoknowhowtodraweachentity.Itjustaskstheentityifitknowshowtodrawitselfandifitdoesittellsittodoso.Rememberthatinvokingentity:draw()isjustshorthandforwriting entity.draw(entity)becauseofthe :.

Ok,butputtingtheentitiesinalistdidn'tcleanupthisfile.Nowthisfileisresponsibleforknowingwheretospawntheentitiesandhavingtheminalistjustmakesthisfilebigger.Wellyouseethereasonweputtheminalistisbecausewewanttomakeanewgamefilecalledentities.luathatwillberesponsibleforloading,spawning,andstoringalltheentitieswhenthegamestartsup.Createanewfilethencutalltheentity requirestatementsandtheentitylistandpasteitinthenewfile:

--entities.lua

localboundary_bottom=require('entities/boundary-bottom')

localboundary_left=require('entities/boundary-left')

localboundary_right=require('entities/boundary-right')

2.12-Breakout(part2)

92

localboundary_top=require('entities/boundary-top')

localpaddle=require('entities/paddle')

localball=require('entities/ball')

localbrick=require('entities/brick')

return{

boundary_bottom(400,606),

boundary_left(-6,300),

boundary_right(806,300),

boundary_top(400,-6),

paddle(300,500),

ball(200,200),

brick(100,100),

brick(200,100),

brick(300,100)

}

Andnowthetopofourmainfileonlyneedstoloadtheentitiesfileanditwillhavethelisttousein love.drawandelsewhereasneeded:

--main.lua

localentities=require('entities')

localworld=require('world')

Whenyourunthegame,youshouldbeseeingsomethingsimilartothis:

Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-2

2.12-Breakout(part2)

93

Andthat'saboutitforentitymanagement.We'llfigureouthowtohandlekeypressesforthepaddleandeverythingelseinthenextsection.We'llfinishthecleanupinourmainfilewhilewe'reatit.

ExercisesNowthatourentitieshavepassedoffknowledgeonwheretheyspawnovertoentities.lua,ourleftandrightboundariesareidenticalfiles.Replaceboundary-left.luaandboundary-right.luawithasingleboundary-vertical.luafileandspawntwocopiesofthatinentities.lua.Ifyougetstuck,checkouttheentities.luafileinthenextsectionforhowthisisdone.

2.12-Breakout(part2)

94

Breakout(part3):inputs

ReviewIntheprevioussectionwereconstructedourentitiestomakeroomforbricksandadditionalfunctionality.Wehaven'tcompletedanynewitemsonourchecklist:

TheobjectiveofthegameistodestroyallthebricksonthescreenTheplayercontrolsa"paddle"entitythathitsaballTheballdestroysthebricks✔TheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen,thegameends

Solet'scomeupwithasystemtohandleuserinputandgetthepaddlemoving.

InputserviceInsidemain.luathereissomefunctionalityforthisthatwearegoingtoremoveandrewritestartingwithanewfilethatspecificallyhandlesalltheuserinput.Thiskindoffileistypicallycalledaservicebecauseitabstractsawaytediousfunctionalityintoaneasy-to-useservice.Iencourageyoutowriteouttheserviceinsteadofcopyingandpasting.Readthrougheachfunctionandtrytounderstandwhateachonedoes.

--input.lua

--Thistableistheserviceandwillcontainsomefunctions

--thatcanbeaccessedfromentitiesorthemain.lua.

localinput={}

--Mapspecificuserinputstogameactions

localpress_functions={}

localrelease_functions={}

--Formovingpaddleleft

input.left=false

--Formovingpaddleright

input.right=false

--Keeptrackofwhethergameispause

input.paused=false

--Lookupinthemapforactionsthatcorrespondtospecifickeypresses

input.press=function(pressed_key)

ifpress_functions[pressed_key]then

press_functions[pressed_key]()

end

end

--Lookupinthemapforactionsthatcorrespondtospecifickeyreleases

input.release=function(released_key)

ifrelease_functions[released_key]then

release_functions[released_key]()

end

end

--Handlewindowfocusing/unfocusing

input.toggle_focus=function(focused)

ifnotfocusedthen

input.paused=true

end

end

2.13-Breakout(part3)

95

press_functions.left=function()

input.left=true

end

press_functions.right=function()

input.right=true

end

press_functions.escape=function()

love.event.quit()

end

press_functions.space=function()

input.paused=notinput.paused

end

release_functions.left=function()

input.left=false

end

release_functions.right=function()

input.right=false

end

returninput

Theinputtableiswhatgetsreturned,meaningwhenwe require('input')inanotherfile,wegetbackthattableanditscontents.Insidetheinputtherearethreebooleanpropertiesthatgettoggledbyuserinput: input.left,input.right,and input.paused.Alongwiththesethreeproperties,therearethreefunctionsexposedtoustomakeuseof: input.press, input.release,and input.toggle_focus,allofwhichwewillinvokefromourcallbacksinmain.lua:

--main.lua

localentities=require('entities')

localinput=require('input')

localworld=require('world')

love.draw=function()

for_,entityinipairs(entities)do

ifentity.drawthenentity:draw()end

end

end

love.focus=function(focused)

input.toggle_focus(focused)

end

love.keypressed=function(pressed_key)

input.press(pressed_key)

end

love.keyreleased=function(released_key)

input.release(released_key)

end

love.update=function(dt)

ifnotinput.pausedthen

for_,entityinipairs(entities)do

ifentity.updatethenentity:update(dt)end

end

world:update(dt)

end

end

2.13-Breakout(part3)

96

In love.updateweskipupdatesif input.pausedis true.Howeverifthegameisnotpausedthenitwillloopthroughtheentitylist,calling entity.updateiftheentityhasanupdatefunction.Withthisaddedfunctionality,wecanappendan entity.updatefunctionintoourexistingpaddlecode:

--entities/paddle.lua

entity.update=function(self)

--Don'tmoveifbothkeysarepressed.Justreturn

--insteadofgoingthroughtherestofthefunction.

ifinput.leftandinput.rightthen

return

end

localself_x,self_y=self.body:getPosition()

ifinput.leftthen

self.body:setPosition(self_x-10,self_y)

elseifinput.rightthen

self.body:setPosition(self_x+10,self_y)

end

end

Theleftandrightarrowswillnowmovethepaddle!Thereisn'tmuchelsetosayhereinthewayofinput.Abitunrelatedtotheactualinput,butmoresothepaddlefunctionalityisitmovesoffscreenanddoesn'tadheretotheboundaries?Whyisthat?

Ifyourememberwhenwecreatedthepaddle,itisastaticentity.Itdoesn'thavetheabilitytomoveonitsownorbytheeffectofotherentities.Thiswillcauseussomeproblemslater(andwe'relearningthehardway)!Ratherthanforcingthepaddlewithaninvisiblepush,weforceanewpositionforthepaddlewhenwecall body:setPositioninsidethepaddle's entity.updatefunction.It'slikewe'reteleportingitontopofwhateverspacewewantwithakeystroke,ignoringallphysicsandcollision.Thisissimplertocodeandgetsaroundthefactthepaddle'sstaticbodywon'trespondtoforce.Tofixthis,wecanartificiallysettheboundaryonthepaddlebycheckingifitisoutofboundsbeforemovingit.

--entities/paddle.lua

entity.update=function(self)

--Don'tmoveifbothkeysarepressed.Justreturn

--insteadofgoingthroughtherestofthefunction.

ifinput.leftandinput.rightthen

return

end

localself_x,self_y=self.body:getPosition()

ifinput.leftthen

localnew_x=math.max(self_x-10,108)

self.body:setPosition(new_x,self_y)

elseifinput.rightthen

localnew_x=math.min(self_x+10,700)

self.body:setPosition(new_x,self_y)

end

end

Calling math.maxmeanswewillsetthenewx-positiontoeither self_x-10or 100,whichevernumberisbigger.Thispreventsusfromgettinganumbersosmallitrunsofftoofartotheleft. math.mindoestheoppositeandtakescareoftherightsideofthescreen.

Oneissueyoumayormaynotnoticeismovementisn'talwaysauniformspeed,anddependingonthespeedofyourcomputerthepaddlemayappeartogofasterorslower.Rememberthearticleondeltatime?Weneedtoscalethedistancetravelledtomatchtheamountoftimethathaspassed.Conveniently,wearegettingthedeltatimefromlove.updatealready.Takeacloserlookatit:

--main.lua

2.13-Breakout(part3)

97

love.update=function(dt)

ifnotinput.pausedthen

for_,entityinipairs(entities)do

--Deltatimeisbeingpassed

--totheentity.updatefunctionhere

--|

--|

--V

ifentity.updatethenentity:update(dt)end

end

world:update(dt)

end

end

Whichmeanswecandothis:

--entities/paddle.lua

entity.update=function(self,dt)

--Don'tmoveifbothkeysarepressed.Justreturn

--insteadofgoingthroughtherestofthefunction.

ifinput.leftandinput.rightthen

return

end

localself_x,self_y=self.body:getPosition()

ifinput.leftthen

localnew_x=math.max(self_x-(400*dt),100)

self.body:setPosition(new_x,self_y)

elseifinput.rightthen

localnew_x=math.min(self_x+(400*dt),700)

self.body:setPosition(new_x,self_y)

end

end

Thenumber 400isarbitraryandcanbewhateverspeedyouwantthepaddletomoveat. dtisasmallnumbersoitneedstobemultipliedbyalargenumberlike400tomatchaspeedsimilartowhatwewereseeingbeforewhenwesimplywereaddingandsubtracting 10.

Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-3

Inthenextsectionwewillworkonthephysicsmoretogivetheballmovementamorerealisticfeel.Wewillalsoimplementtheabilitytodestroybricksusingtheworldcollisioncallbacks.

ExercisesDespitehavingarestitutionof1,theballislosingmomentumasitcollideswithotherobjects.Thisisduetofriction.Howcanthatbefixed?Whenthegameispaused,makeitdisplaytextonthescreensotheplayerknowsthegameisn'tjustfrozen.Hint:you'llneedoneofthedrawfunctionsfromlove.graphicstoprintthetext.

Theanswerstotheseexerciseswillbeinthenextsection'ssourcecode.

2.13-Breakout(part3)

98

Breakout(part4):physics

ReviewInthepreviousexerciseswediscussedissueswiththeballslowingdownduetofriction.Withabitofbrowsingthroughthe love.physicsdocumentationyoumighthaveseenthatfrictionisapropertyofthefixtureandcanbesetto0in fixture:setFriction.

Howaboutcreatingthepausescreentext?Wereyouabletodoitwithouttouchingmain.lua?Takealookatthisentitythatwascreatedjustforthesingleresponsibilityofdisplayingpausetext:

--entities/pause-text.lua

localinput=require('input')

returnfunction()

localwindow_width,window_height=love.window.getMode()

localentity={}

entity.draw=function(self)

ifinput.pausedthen

love.graphics.print(

{{0.2,1,0.2,1},'PAUSED'},

math.floor(window_width/2)-54,

math.floor(window_height/2),

0,

2,

2

)

end

end

returnentity

end

That'sright.Eventhepausescreenisanentity.Thefirstnaturalplacetothinktoputitwouldbethemainfilebutentityfilesareperfectbecausewecancreateasmanyasweneedforeachtaskandaddittoentities.luawhereitwillbehandledbythegameloop.Forcenteringthetextthe love.window.getModefunctionisusedtogetthefullwindowdimensionsthenthosenumbersaredividedinhalf.Thissavesusfrommanuallycodinginnumbersthatwouldneedtobereadjustedifthewindowsizechanged.Additionally, math.floorwasusedforgoodmeasuretomakesurewearereturningawholenumber.Itisrecommendedtorounddecimalsofffromnumberswhenpassingcoordinatestothedrawingfunctions.Otherwiseitmayattempttodrawthatobjectbetweenpixelsonthescreenandcausesomeblurriness.

PhysicsupdatesAnissuewehadwiththegamephysicssincewegotthepaddlemovingisthattheballdoesn'talwaysricochetoffthepaddleasyouwouldexpect.Thisisbecausewemadethepaddlestaticsotheballdoesn'tpushitaround,butthishastheeffectofthepaddlenotinteractingwiththeballcorrectly.Thisiswhere kinematicbodiescomein.Kinematicbodies,likestaticbodiesaren'taffectedbydynamicbodies.Kinematicbodies,unlikestaticbodies,canaffectdynamicbodies.

We'regoingtomake3changestopaddle.lua:

2.14-Breakout(part4)

99

Movetheboundarydimensions,paddledimensions,andpaddlespeedtoeasily-referencedvariablesatthetopofthefile.ChangethebodytypetokinematicOverhaultheupdatecodetomovethebodywithlinearvelocityratherthanmanuallysettinganewlocationonthescreenwitheveryupdate

--entities/paddle.lua

localinput=require('input')

localworld=require('world')

returnfunction(pos_x,pos_y)

localwindow_width=love.window.getMode()

--Variablestomaketheseeasiertoadjust

localentity_width=120

localentity_height=20

localentity_speed=600

--Thelimitofhowfarleft/righttheentitycanmovetowards

--theedges(withalittlebitofpaddingthrownon).

localleft_boundary=(entity_width/2)+2

localright_boundary=window_width-(entity_width/2)-2

localentity={}

entity.body=love.physics.newBody(world,pos_x,pos_y,'kinematic')

entity.shape=love.physics.newRectangleShape(entity_width,entity_height)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

entity.draw=function(self)

love.graphics.polygon('line',self.body:getWorldPoints(self.shape:getPoints()))

end

entity.update=function(self)

--Don'tmoveifbothkeysarepressed.Justreturn

--insteadofgoingthroughtherestofthefunction.

ifinput.leftandinput.rightthen

return

end

localself_x=self.body:getX()

ifinput.leftandself_x>left_boundarythen

self.body:setLinearVelocity(-entity_speed,0)

elseifinput.rightandself_x<right_boundarythen

self.body:setLinearVelocity(entity_speed,0)

else

self.body:setLinearVelocity(0,0)

end

end

returnentity

end

Itookthelibertyofadjustingthepaddlesize,butwithourniceboundary-sizecalculationsinplacethepaddledimensionscaneasilybeadjustedandtheboundarysizewilltakethosechangesintoaccount.Let'sdrillintotheentity.updatefunction.

Oncetheinputsarecheckedtobetrueorfalsethecurrentx-positionofthepaddleischeckedtoseeifitgoesoutoftheboundaries(calculatednearthetop).Noticethatthecalculationsfortheboundarylocationsaredoneatthetopinsteadofin entity.update.Thismeansthosecalculationsaren'tdoneoneveryupdatesincetheydon'tneedtobe.

Abitmorecomplexthanthepaddlearethecalculationsfortheball:

--entities/ball.lua

localworld=require('world')

2.14-Breakout(part4)

100

returnfunction(x_pos,y_pos)

localentity_max_speed=880

localentity={}

entity.body=love.physics.newBody(world,x_pos,y_pos,'dynamic')

entity.body:setLinearVelocity(300,300)

entity.shape=love.physics.newCircleShape(0,0,10)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setFriction(0)

entity.fixture:setRestitution(1)

entity.fixture:setUserData(entity)

entity.draw=function(self)

localself_x,self_y=self.body:getWorldCenter()

love.graphics.circle('fill',self_x,self_y,self.shape:getRadius())

end

entity.update=function(self)

localvel_x,vel_y=self.body:getLinearVelocity()

localspeed=math.abs(vel_x)+math.abs(vel_y)

localvel_x_is_critical=math.abs(vel_x)>entity_max_speed*2

localvel_y_is_critical=math.abs(vel_y)>entity_max_speed*2

--Ballisbouncingtoofasttoreasonablyhit.

--Cutdownitsspeedby75%ifso.

ifvel_x_is_criticalorvel_y_is_criticalthen

self.body:setLinearVelocity(vel_x*.75,vel_y*.75)

end

ifspeed>entity_max_speedthen

self.body:setLinearDamping(0.1)

else

self.body:setLinearDamping(0)

end

end

returnentity

end

Inthefirstchunkwegetthecurrentxandyvelocity,whichtellsusthexandydirectionoftheball:

localvel_x,vel_y=self.body:getLinearVelocity()

localspeed=math.abs(vel_x)+math.abs(vel_y)

Anexample vel_x/ vel_ymaybe 212/ -300,whichmeanstheballismovingupandtowardstheright.Thespeediscalculatedbyturningboththesenumbersintoabsolutenumbersandaddingthemtogether(so 512intheexample).

Inthenextchunkthereisasafetychecktomakesuretheballdidn'tricochetwithsomuchforcethatit'sgoingtoofasttopossiblyhit.Ifeitherbooleanvariableistruethenthelinearvelocityismultipliedbyafractionofitselftoquicklyslowitdown:

localvel_x_is_critical=math.abs(vel_x)>entity_max_speed*2

localvel_y_is_critical=math.abs(vel_y)>entity_max_speed*2

--Ballisbouncingtoofasttoreasonablyhit.

--Cutdownitsspeedby75%ifso.

ifvel_x_is_criticalorvel_y_is_criticalthen

self.body:setLinearVelocity(vel_x*.75,vel_y*.75)

end

Nowthereisjustachecktoeasetheballbackdowntoacomfortablemaximumspeed.Iftheball'sspeedisgreaterthan entity_max_speedadampingisappliedwhichwillreducetheballsspeedbelow880.Oncethetargetspeedisreachedthenthedampingswitchesbackto0:

2.14-Breakout(part4)

101

ifspeed>entity_max_speedthen

self.body:setLinearDamping(0.1)

else

self.body:setLinearDamping(0)

end

Tryoutthechangestofeelitinactioncomparedtothepreviousphysicsandhopefullyyouwillfindthatit'sanimprovement.It'snotaperfectreplicaofthearcadegame,butplayingaroundwiththesetricksandfeaturesyoucangetitprettydarnclosetosomethingsatisfactory.Anotherthingtotryoutifwithintheball's entity.update,addalineunderthespeedvariablethatreads print(speed)andwatchthenumberincreaseanddecreaseagainasthedampingkicksin.Prettyneatthatmostoftheheavycalculationsarehandledbythephysicsengineforus.

CollisionThereare4changesinvolvedtomakethebricksdestructible:

Updateworld.luatocheckforcollisionfunctionalityfortheentitieswhentheycollideUpdatebrick.luatoincludeacollisioncallbackAddanewattributeonthebrickentitytoletusknowitscurrentconditionandifitneedstobedestroyed.We'lljustcallit entity.health.Updatemain.luatoremove/destroyanyentitiesthathavenomorehealth

Firsttheworld:

--world.lua

--Calledattheendofanycontactintheworld.Parameters:

--{fixture}fixture_a-firstfixtureobjectinthecollision.

--{fixture}fixture_b-secondfixtureobjectinthecollision.

--{contact}contact-worldobjectcreatedonandatthepointofcontact

--Seefurther:https://love2d.org/wiki/Contact

localend_contact_callback=function(fixture_a,fixture_b,contact)

localentity_a=fixture_a:getUserData()

localentity_b=fixture_b:getUserData()

ifentity_a.end_contactthenentity_a:end_contact()end

ifentity_b.end_contactthenentity_b:end_contact()end

end

localworld=love.physics.newWorld(0,0)

world:setCallbacks(nil,end_contact_callback,nil,nil)

returnworld

Theonlycallbackwe'llbeusingforthistutorialistheend-contactcallback,sofor world:setCallbackswearegoingtoreturning nilfortheresttokeepourcodefastandclean.Takealookatwhatishappeninginsideend_contact_callback.Rememberinsideeachentitywhenweinvoked entity.fixture:setUserData(entity)?Withtheentityattachedtoeachfixture,wecangetaccesstothoseentitiesbyinvoking fixture:getUserDatainthecallbackabove.Oncewehaveaccesstoeachentity,wechecktoseeiftheentityhasany end_contactfunctions,codespecifictothatentitythatneedstorunwhenendingthecollision.

Nowwecangotobrick.luaanddefinethatfunctionality:

--entities/brick.lua

localworld=require('world')

returnfunction(x_pos,y_pos)

2.14-Breakout(part4)

102

localentity={}

entity.body=love.physics.newBody(world,x_pos,y_pos,'static')

entity.shape=love.physics.newRectangleShape(50,20)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

--Howmanytimesthebrickcanbehitbeforeitisdestroyed

entity.health=2

entity.draw=function(self)

love.graphics.polygon('fill',self.body:getWorldPoints(self.shape:getPoints()))

end

entity.end_contact=function(self)

self.health=self.health-1

end

returnentity

end

Noticethetwonewvaluesinthetable, entity.healthand entity.end_contact.Inside end_contactwearesubtracting1healthwhenthecollisionends.Healthcouldstartatanynumberandthatmeanstheballwillneedtocollidewiththebrickthatmanytimesbeforethehealthreaches0.Lastly,weneedtogointomain.luaandadjustlove.updatesoitdoessomethingwhenitseesanentitywith0health:

--main.lua

love.update=function(dt)

ifnotinput.pausedthen

localindex=1

whileindex<=#entitiesdo

localentity=entities[index]

ifentity.updatethenentity:update(dt)end

--Whenanentityhasnohealth(brickhasbeenhitenoughtimes

--thenweremoveitfromthelistofentities.Don'tincrement

--theindexnumberifdoingthatthoughbecausewehaveshrunk

--thetableandmadealltheitemsshiftdownby1intheindex.

ifentity.health==0then

table.remove(entities,index)

entity.fixture:destroy()

else

index=index+1

end

end

world:update(dt)

end

end

Theentityisremovedfrom entitiesaswellashavingitsfixturedestroyedfromtheworld.Thiswillonlyhappentobrickswith0health.Itwon'thappentoentitieswherewedidn'tdefinehealthbecause nilisnotthesamethingas0.Noticethata whileloopwasusedhere.Thisisbecausewemayremoveentitiesfromthelistweareloopingoverandthiswouldthrowofftheindexcountforaregular forloop.

Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-4

Inthenextsectionwe'llreviewthechecklistandseewhatislefttocover.

ExercisesItwouldbegreatifthecolorsofthebrickschangeddependinghowmuchhealththebrickhas.Updatethebrick'sentity.drawfunctionwithsomecolors.Hint:wecoveredcolorsin2.4-Gameloop.

2.14-Breakout(part4)

103

Addmorebrickstothescreen.What'stheeasiestwaytodothat?

2.14-Breakout(part4)

104

Breakout(part5):gamestate

ReviewWe'vegottenabitdonesolet'slookatthebasicrequirementsagain:

Theobjectiveofthegameistodestroyallthebricksonthescreen✔Theplayercontrolsa"paddle"entitythathitsaball✔Theballdestroysthebricks✔TheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen,thegameends

Inthepreviousexercisethequestionwasbroughtupwhatwouldbetheeasiestwaytodrawabunchofbricksacrossthescreen.Asimple,butverytediousanswertothatwouldbetopositionthebricksoneatatimeinentities.lualikeso:

brick(40,80),

brick(100,140)

--andsoon...

Ifyouwanttomakeyourbricksintoashapeorsculpturethenthatmightbethebestapproach.Ifyoujustwanttoarrangeyourbricksintoagrid,thentheeasiestwaywouldbetowriteanumericfor-loop.

--entities.lua

localboundary_bottom=require('entities/boundary-bottom')

localboundary_vertical=require('entities/boundary-vertical')

localboundary_top=require('entities/boundary-top')

localpaddle=require('entities/paddle')

localpause_text=require('entities/pause-text')

localball=require('entities/ball')

localbrick=require('entities/brick')

localentities={

boundary_bottom(400,606),

boundary_vertical(-6,300),

boundary_vertical(806,300),

boundary_top(400,-6),

paddle(300,500),

pause_text(),

ball(200,200)

}

localrow_width=love.window.getMode()-20

fornumber=0,38do

localbrick_x=((number*60)%row_width)+40

localbrick_y=(math.floor((number*60)/row_width)*40)+80

entities[#entities+1]=brick(brick_x,brick_y)

end

returnentities

Okthisadmittedlylooksmorecomplicatedatfirst,butifyourememberthearithmeticandordersofoperationcoveredin1.1-Interactivecodingstatementsareprocessedfromtheinnerparenthesisandworkedoutwards.Sowhythelongcalculation?Let'sstartoffwithasimplercalculation:

localbrick_x=number*60

2.15-Breakout(part5)

105

Startingwiththe number0upto38,therewillbe39loopsandtherefore39bricksdrawn.Onthefirstloop, numberis0.Sincethebricksare50pixelswidethiswoulddrawthebrickswitha10pixelspacebetweeneach.Firstbrickat60,then120,then180...Ok,butthenafteronlyadozenbrickswewouldstartrunningoffthescreen.Thisiswherethemoduluscomesinhandy:

localbrick_x=(number*60)%row_width

row_widthishowwidewewantarowofbrickstobebe.Inthiscase row_widthisthescreenwidth,800pixels,subtract20pixelsforpadding.Sodrawthebricksevery60pixels,butthenwhenyougetto780pixels,startbackat0pixelsandbegindrawinganewrow.Thanksmodulus!Nowjusttogivethebrickssomespacingontheleftsideawayfromthewall,wecangoaheadandadd40pixelstothefinalresultforthex-position:

localbrick_x=((number*60)%row_width)+40

Thebrick'sy-positioniscalculatedalittlebitdifferently.Whatweneedtofindoutiswhichrowwe'reonsoweknowwhereonthey-axistodraw.Ifwetakethe numberandmultiplyitby60thendoamodulusweknowthatgivesusthex-position.Solet'stakethatchunkofcodefromaboveandmakethatthebasisofoury-positioncalculation:

localbrick_y=(number*60)%row_width

Ratherthanusingmodulus,ifweuseregulardivisionwegetasmallremaindereverytime (number*60)exceedstherowwidth:

localbrick_y=(number*60)/row_width

Thiswillgiveusanumberwithdecimalssotokeepthingsroundedwecanuse math.floortosnapthey-positiondowntothenearestwholenumber:

localbrick_y=math.floor((number*60)/row_width)

Great!Noweverytimethex-positionexceedstherowwidth,wegetbackthenumberoftherowwe'reon...0forthefirst,1forthesecond,2andsoon.Withthisnumberwecannowspaceouteachrowby40pixels:

localbrick_y=math.floor((number*60)/row_width)*40

Thenfinallyjusttoshiftthebricksalittlefurtherdownthescreenwegiveitapaddingthatlooksright,say80:

localbrick_y=(math.floor((number*60)/row_width)*40)+80

Andthereyougo.Theentitycanjustbeaddedtotheendoftheentitieslistsoitdoesn'tgetlost:

entities[#entities+1]=brick(brick_x,brick_y)

Inthepreviousexerciseswealsotalkedaboutdrawingthebricksdifferentcolorstoindicatetheirintegrity/healthleftbeforetheywillbedestroyed.Ratherthanreviewthatnow,let'sdiveintostatemanagementandwe'llwrapcoloringupalongtheway.

Statemanagement

2.15-Breakout(part5)

106

Youraverage,every-dayprogramhasalotofinformationitneedstostoryinmemory.Forourgametofunctionwithjustthebasicfeatures,weneedtostoreinformationabouteachentity,whetherornotthegameiscurrentlypaused,orifthegameiswonorlost.Thisinformationiscalledthestate.Thestateisdatathatmaychangeduringthelifetimeoftheapplication.Thinkofthestateofyourlightsinyourroom.Aretheycurrentlyinan"on"or"off"state?Thestatecancausedifferenteffectsontheapplication,likeifthe"pause"stateofthegameis"true"thentheworldwillnolongerreceiveupdates.

Onethingwemustthinkofishowtoorganizethestateofourapplication.Thisissomethingwetakeforgrantedoftenintherealworld;Wedon'thavetofigureoutwheretostorethestateofourlights.It'sapieceofinformationintrinsictothelamp'sdesign.

Sowhydowehavetocaresomuchaboutourgame'sstate?Tobefair,ourgameissmallsoweprobablydon'tneedto.However,itiscrucialtoreconcilesuchthingswhileapplicationsaresmallbecauseitwillbeverydifficulttogobackandfixabunchofcodeoncetheapplicationisbig.Thewayyoushouldorganizethestateofyourapplicationshouldaccomplishafewthings:

Itshouldbeeasytofindandaccessthenecessarydatathatmakesupthestate.Forinstance,howeasyisitforourmainfiletoaccesstheentitiesandloopovertheminthe love.updatefunction?Thereshouldonlybeonecopyofthestate.Ifwewanttoaccessthe"paused"stateofourgameinmultipleplacesthatisfine,butweshouldn'thavemultiple"paused"variablesfloatingaroundourgame.Ifwehada"paused"variableinsideanentityfileandanotherinsidetheinputserviceupdatingindependentlythentheycouldgetoutofsyncandourgamewouldgetconfusedonwhenitshouldbepaused.Thestateshouldonlybeaccessedwhereitisneeded.Ifyouwereaccessingorstoringthe"paused"stateinsidetheballentity,thenifthatballwasdestroyedthensomethingbadwillhappenthenexttimethegamecheckstoseeifitispaused.

Whatfilescontainthestateofourgame?

entities.lua-Eachentitytableisresponsibleforitsownstate.Forinstance,eachbrickstoresthestateofitsownhealth.Alltheentitiestablesaregeneratedandstoredhere.Theentitiesarenotstoredintheentitiesfolder.Thosearejustfunctionsusedtogeneratetheentities.Theblueprints.input.lua-Thisfileisresponsibleforcapturinguserinput,butalsostoringthestateofwhatkeysarecurrentlybeingpressed.world.lua-Thisfileisnotonlytheblueprintsforthegameworld,butitalsostorestheworldinstancethatisgeneratedwhenthegamestarts.Wemadetheworldinstanceeasilyaccessibletotherestoftheapplicationbywriting returnworldattheend.Therewouldbenogameifthiswasn'teasilyaccessible.

Afewpiecesofgamestateweneedtoaddareabooleanofwhetherthegameisover,anotherforifthestageiscleared,andalsoalistofcolorstouseinourgamewhichwe'llrefertoasourpalette.Thisinformationwouldn'treallyfitinanyoftheplaceswelistedabove,andwedon'twanttoaddittomain.luabecauseofourfirstrulethatthegamestateshouldbeeasytoaccesswhereitisneeded.Besides,that'snotthemainfile'sresponsibility.We'llgoaheadandjustmakeanewfilecalledstate.luaandstoretheoverallgamestateinthisfile.Thisisalsoalittlematterofopinionbutthe"paused"andbuttonstateswe'llalsomoveinheresincetheyaffecttheoverallgame'sstate.Thiswillalsomakeitsothatinput.lua'sonlyresponsibilityistocaptureandtranslatetheuserinput,nottohandleanystatewhatsoever.

--state.lua

--Thestateofthegame.Thiswayourdataisseparatefromourfunctionality.

return{

button_left=false,

button_right=false,

game_over=false,

palette={

{1.0,0.0,0.0,1.0},--red

2.15-Breakout(part5)

107

{0.0,1.0,0.0,1.0},--green

{0.4,0.4,1.0,1.0},--blue

{0.9,1.0,0.2,1.0},--yellow

{1.0,1.0,1.0,1.0}--white

},

paused=false,

stage_cleared=false

}

It'skindofanicefeelingtokeepallthestatetogether.Wecouldevenmovetheentitieslistintostate.luaandgetridofentities.lua,butthisdoesn'tseemnecessary.Nowwiththisshiftindataweneedtoupdateinput.luaandmain.luatoreferencethenewfile:

--input.lua

localstate=require('state')

--Mapspecificuserinputstogamestates

localpress_functions={

left=function()

state.button_left=true

end,

right=function()

state.button_right=true

end,

escape=function()

love.event.quit()

end,

space=function()

ifstate.game_overorstate.stage_clearedthen

return

end

state.paused=notstate.paused

end

}

localrelease_functions={

left=function()

state.button_left=false

end,

right=function()

state.button_right=false

end

}

--Thistableistheserviceandwillcontainsomefunctions

--thatcanbeaccessedfromentitiesorthemain.lua.

return{

--Lookupinthemapforactionsthatcorrespondtospecifickeypresses

press=function(pressed_key)

ifpress_functions[pressed_key]then

press_functions[pressed_key]()

end

end,

--Lookupinthemapforactionsthatcorrespondtospecifickeyreleases

release=function(released_key)

ifrelease_functions[released_key]then

release_functions[released_key]()

end

end,

--Handlewindowfocusing/unfocusing

toggle_focus=function(focused)

ifnotfocusedthen

state.paused=true

end

end

2.15-Breakout(part5)

108

}

--main.lua

localentities=require('entities')

localinput=require('input')

localstate=require('state')

localworld=require('world')

love.draw=function()

for_,entityinipairs(entities)do

ifentity.drawthenentity:draw()end

end

end

love.focus=function(focused)

input.toggle_focus(focused)

end

love.keypressed=function(pressed_key)

input.press(pressed_key)

end

love.keyreleased=function(released_key)

input.release(released_key)

end

love.update=function(dt)

ifstate.game_overorstate.pausedorstate.stage_clearedthen

return

end

localindex=1

whileindex<=#entitiesdo

localentity=entities[index]

ifentity.updatethenentity:update(dt)end

--Whenanentityhasnohealth(brickhasbeenhitenoughtimes

--thenweremoveitfromthelistofentities.Don'tincrement

--theindexnumberifdoingthatthoughbecausewehaveshrunk

--thetableandmadealltheitemsshiftdownby1intheindex.

ifentity.healthandentity.health<1then

table.remove(entities,index)

entity.fixture:destroy()

else

index=index+1

end

end

world:update(dt)

end

Noticethechangeto love.update.Wecheckif state.game_over, state.pausedor state.stage_clearedistrueandifso,wereturnfrom love.updatewithoutdoinganyoftheupdatesasthesekindofgamestatesmeritfreezingthescreen.

Nextup,updatepaddle.luatorequire stateinsteadof input.The entity.updatefunctionnowneedstoreferencestate.button_leftand state.button_righttotelliftheplayerhaspressedanybuttons.Tryupdatingitonyourown.Ifyoudogetstuck,thesourcecodewillbeinthelinkatthebottomwaitingforyou.

Ok,nowthatwehaveastatewherewestoredthecolorsitisprobablyagoodtimetotryandupdatebrick.lua.Firstlet'slookatthosecolorsstoredinstate.lua:

palette={

{1.0,0.0,0.0,1.0},--red

2.15-Breakout(part5)

109

{0.0,1.0,0.0,1.0},--green

{0.4,0.4,1.0,1.0},--blue

{0.9,1.0,0.2,1.0},--yellow

{1.0,1.0,1.0,1.0}--white

},

The palettetableisalistofmoretables.Eachtableinthelistrepresentscolorswherethefirstnumberistheamountofred,2ndtheamountofgreen,3rdtheamountofblue,and4thnumbertheamountofopacity.Settingthelastnumberto 0meansthecoloris100%transparentand 1meansitiscompletelyopaque.Allofthesevaluesmixtogethertoformasinglecolor.Inthecaseofthefirstcolor,wehavetheredvaluesettomaximumopaqueredwithnoothercolorsmixedin.Iwouldencourageyoutogobackandeditthecolorsinthispaletteaftereverythingisworking.Now,insidebrick.lualet'supdate entity.draw:

--entities/brick.lua

localstate=require('state')

localworld=require('world')

returnfunction(x_pos,y_pos)

localentity={}

entity.body=love.physics.newBody(world,x_pos,y_pos,'static')

entity.shape=love.physics.newRectangleShape(50,20)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

--Howmanytimesthebrickcanbehitbeforeitisdestroyed

entity.health=2

--Usedtocheckduringupdateifthisentityisabrick

--Ifnobricksarefoundthenthelevelwascleared

entity.type='brick'

entity.draw=function(self)

--Drawthebrickinadifferentcolordependingonhealth

love.graphics.setColor(state.palette[self.health]orstate.palette[5])

love.graphics.polygon('fill',self.body:getWorldPoints(self.shape:getPoints()))

--Resetgraphicsdrawerbacktothedefaultcolor(white)

love.graphics.setColor(state.palette[5])

end

entity.end_contact=function(self)

self.health=self.health-1

end

returnentity

end

Beforedrawingthebrick'spolygon,wesetthegraphicsrenderertouseoneofthecolorsfrom state.palette.Thecolortousedependsonwhatthebrick'shealthis.Soifthebrickhas2healththen state.palette[self.health]willbecome state.palette[2]whichwillgrabthe2ndcolorinthelist...green.Ifthebrick'shealthwas1,thenthefirstcolorfromthepalettewouldbeselected...red.Afterthecoloredpolygonisdrawn, entity.drawfinishesupbysettingtherenderercolorbacktowhite.Ifwedidn'tdothisstep,theballandpaddlewouldgetdrawnthesamecolorasthebricks.

Onelastthingweneedtodotogetthegameworkingisupdatepause-text.luaasitisincorrectlylookingforthe"pause"stateininput.luainsteadofthenewstate.lualocation:

--entities/pause-text.lua

localstate=require('state')

returnfunction()

localwindow_width,window_height=love.window.getMode()

2.15-Breakout(part5)

110

localentity={}

entity.draw=function(self)

ifstate.pausedthen

love.graphics.print(

{state.palette[3],'PAUSED'},

math.floor(window_width/2)-54,

math.floor(window_height/2),

0,

2,

2

)

end

end

returnentity

end

FinaltouchesWeneedthegametoendwhentheplayerdestroysallthebricksorlosestheball.Justlikethepause-textentity,displaysomemessagesbasedonthegamestate.

--entities/game-over-text.lua

localstate=require('state')

returnfunction()

localwindow_width,window_height=love.window.getMode()

localentity={}

entity.draw=function(self)

ifstate.game_overthen

love.graphics.print(

{state.palette[5],'GAMEOVER'},

math.floor(window_width/2)-100,

math.floor(window_height/2),

0,

2,

2

)

end

end

returnentity

end

--entities/stage-clear-text.lua

localstate=require('state')

returnfunction()

localwindow_width,window_height=love.window.getMode()

localentity={}

entity.draw=function(self)

ifstate.stage_clearedthen

love.graphics.print(

{state.palette[4],'STAGECLEARED'},

math.floor(window_width/2)-110,

math.floor(window_height/2),

2.15-Breakout(part5)

111

0,

2,

2

)

end

end

returnentity

end

Totriggerthe"GAMEOVER"textiseasyenough.Weneedtoaddacollisioncallbacktoboundary-bottom.luatosetthegame's state.game_overtotrueonanycollision:

--entities/boundary-bottom.lua

localstate=require('state')

localworld=require('world')

returnfunction(x_pos,y_pos)

localentity={}

entity.body=love.physics.newBody(world,x_pos,y_pos,'static')

entity.shape=love.physics.newRectangleShape(800,10)

entity.fixture=love.physics.newFixture(entity.body,entity.shape)

entity.fixture:setUserData(entity)

entity.end_contact=function(self)

state.game_over=true

end

returnentity

end

Don'tforgetweneedtoupdateentities.luatoaddourtwonewentities:

--entities.lua

localboundary_bottom=require('entities/boundary-bottom')

localboundary_vertical=require('entities/boundary-vertical')

localboundary_top=require('entities/boundary-top')

localpaddle=require('entities/paddle')

localgame_over_text=require('entities/game-over-text')

localpause_text=require('entities/pause-text')

localstage_clear_text=require('entities/stage-clear-text')

localball=require('entities/ball')

localbrick=require('entities/brick')

localentities={

boundary_bottom(400,606),

boundary_vertical(-6,300),

boundary_vertical(806,300),

boundary_top(400,-6),

paddle(300,500),

game_over_text(),

pause_text(),

stage_clear_text(),

ball(200,200)

}

localrow_width=love.window.getMode()-20

fornumber=0,38do

localbrick_x=((number*60)%row_width)+40

localbrick_y=(math.floor((number*60)/row_width)*40)+80

entities[#entities+1]=brick(brick_x,brick_y)

end

2.15-Breakout(part5)

112

returnentities

Ok,testthatoutandcheckthatthe"GAMEOVER"textworks.Ifitdoes,thenlet'scontinueonandaddtheconditionsforhowtowinthegame.Thisinvolvescheckingthroughalltheentitiesin love.updatetomakesurewestillhavebricks.Ifwedon'thaveanybricksleft,thentheplayerdestroyedthemallandthestageiscleared.

--main.lua

love.update=function(dt)

ifstate.game_overorstate.pausedorstate.stage_clearedthen

return

end

--Switchtotrueifwehavebricksleft

localhave_bricks=false

localindex=1

whileindex<=#entitiesdo

localentity=entities[index]

ifentity.type=='brick'thenhave_bricks=trueend

ifentity.updatethenentity:update(dt)end

--Whenanentityhasnohealth(brickhasbeenhitenoughtimes

--thenweremoveitfromthelistofentities.Don'tincrement

--theindexnumberifdoingthatthoughbecausewehaveshrunk

--thetableandmadealltheitemsshiftdownby1intheindex.

ifentity.healthandentity.health<1then

table.remove(entities,index)

entity.fixture:destroy()

else

index=index+1

end

end

--Flagthestageclearediftherearenomorebricks

state.stage_cleared=nothave_bricks

world:update(dt)

end

Everytime love.updateisran,wesetavariable have_brickstofalse.Ifthisbooleanstays falseallthewaytothebottomofthefunctionthen state.stage_clearedgetsswitchedtotrueandthegameiswon.Insidethe whileloop,however,wecheckeveryentitytoseeifwefindan entity.typeof 'bricks'andifso, have_bricksgetsflippedtotruetostopthegamefrombeingwonyet.

Sothataboutdoesitforcompletingourchecklist.Thegamemaynotbeasfeature-completeasatruebreakoutgame,butthatroomforimprovementleavesopportunityforyoutomodifythegametoworkhowyouwantitto.It'sreallyuptoyourimagination.Tryoutafewexercisesifyoucan'tthinkupanynewfeatures.Ifyouarehavingtroublerunningthegame,besuretocheckoutthesourcecode:

https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-5

ExercisesInsteadofgettingagameoverassoonastheballtouchesthegroundonce,addanewpropertyinstate.luanamed livesandsetittoasmanylivesasyouwanttheplayertohave.Makeissothe state.livesdecreaseswhentheballhitsthegroundandmakethe game_overnottriggerunless state.lives<1.TrysettingthepaddletodifferentshapetomakethegameplaydifferentlyComeupwithnewfeaturestomakethegameplaybetterandfeelmorepolished

ChangetheballandpaddlecolorsAddabackgroundcolor

2.15-Breakout(part5)

113

FigureouthowtoplayasoundeffectwhentheballcollideswiththingsCreatesomekindofpower-upentity

2.15-Breakout(part5)

114

BinaryandbitmasksIn2.10-Collisioncallbackswesawhowtoreacttoentitiescolliding.Inthissectionwe'regoingtodiscusshowwecanbettercontrolwhencollisionshappen.Asthetitlesuggests,thiswillinvolveunderstandingsomebinary.

Let'ssaywehaveabeat-em-upgamewheretwoplayersarefightingbadguysandwedon'twantplayerstocollidewitheachotherandinsteadonlycollidewithenemies.Thecollisioncallbackcouldlooksomethinglikethis:

localbegin_contact_callback=function(fixture_a,fixture_b)

localentity_a_type=fixture_a:getUserData()

localentity_b_type=fixture_b:getUserData()

--Checkthesearen'tthesametypeofentity

ifentity_a_type~=entity_b_typethen

--Somecodetohandlethecollisiongoeshere...

end

end

Butwhatifyouhadpower-upsandyouwantplayerstocollidewiththepower-upsbutyoudon'twantenemiestouchingthepower-ups?Thingscangetcomplicatedprettyquickly:

localbegin_contact_callback=function(fixture_a,fixture_b)

locala=fixture_a:getUserData()

localb=fixture_b:getUserData()

if(a=='powerup'andb=='player')or(a=='player'andb=='powerup')then

--Somepower-upcode...

--Don'tletpower-upscollidewithotherentitytypeslikebadguys

elseifa~=banda~='powerup'andb~='powerup'then

--Codetohandletherestofthecollisions...

end

end

Let'sfindabetterway!

BinaryoperationsBackin1.0-Programmingbasicswediscussedoperations–howtooperateonstringswithequality( ==)checks,howtooperateonnumberswitharithmeticoperations,andevenhowtoperformbooleanoperationslike andandor.Binarynumbershavetheirownoperations,oftencalledbitwiseoperations.Toperformbinaryoperations,let'sfirstlookathowtorepresentbinarynumbers.Typinganumberlike 101,Luawillinterpretitasadecimalnumber(literallyone-hundredone)soweneedtorepresentitasastringandconvertittoanumber.Toconvertastringtoanumber,youpassinthenumberandthebase(base-2inthiscase)likeso:

print(tonumber('101',2))

Whichconvertsthebinarynumber 101todecimalwhenitprintsout:

5

Forcountinginbinaryandlearninghowtoreadandconvertbetweenbinaryanddecimal,therearemanyresourcesthatalreadyexplainitinmuchbetter.Learninghowtodotheconversionsisn'tnecessarytolearningthesebasicbinaryoperations,butisanessentialskilltohaveinthefieldofcomputerscience.

2.16-Binaryandbitmasks

115

Forcountinginbinaryandlearninghowtoreadandconvertbetweenbinaryanddecimal,therearemanyresourcesthatalreadyexplainitinmuchbetter.Learninghowtodotheconversionsisn'tnecessarytolearningthesebasicbinaryoperations,butisanessentialskilltohaveinthefieldofcomputerscience.

Movingon,let'stakealookatsomeofthebasicoperations.

AND

Binarynumbersaresimilartobooleansinthatbinaryonlyhas1'sand0's.TheANDoperatoralsoworkssimilarlytotheboolean and.Yougiveittwodigitsandbothmustbe 1( true)fortheoutputtobe 1.

UnfortunatelyatthetimeofwritingthistheonlineREPLhasanoutdatedversionofLuathatdoesn'tsupportbinaryoperations.Noworries,let'screateamain.luafileandtryitoutusingLÖVE.Toperformbinaryoperations,theincluded 'bit'librarymustbeloaded.Whenrequired,itwillreturnatablewithmanyfunctionsinitrelatedtobinaryoperations.Thefirstfunctionwe'lltry, bit.band()performsabinaryANDoperation.

localbit=require('bit')

print(bit.band(0,0))

print(bit.band(0,1))

print(bit.band(1,0))

print(bit.band(1,1))

Thiswillprinttothedebugconsole:

0

0

0

1

Youcanpassitthedecimals 1and 0asthosenumbersarethesameinbinaryanddecimal.

Theoperationisnotlimitedtotwoinputs:

print(bit.band(1,0,1))

Youcanalsopassitmulti-digitnumbers:

print(bit.band(

tonumber('111',2),

tonumber('101',2)

))

Notethatyouneedtoalwaysuse tonumber()toconvertyourbinarystringtoanumberasthefunctionalwaysexpectsadecimalnumber.Likewisetheoutputwillalwaysbeadecimalnumber:

5

Layitoutlikeanarithmetictableandyoucansolveitjustaseasily:

111

101

---

101-->5

11011010

2.16-Binaryandbitmasks

116

10111100

--------

10011000-->152

ORLikewiththeboolean or,thebinaryORoutputwillbe 1ifeitherthefirstorthesecondnumberis1.Youcouldsayitistheleastpickyoperatorinthatitdoesn'tcareaslongasitgetsa1somewhereatleastonce.

--main.lua

localbit=require('bit')

print(bit.bor(1,1))

print(bit.bor(1,0))

print(bit.bor(0,0))

print(bit.bor(0,0,0,1,0))

1

1

0

1

XOR

Xor(exclusiveor),returns1onlywhenitgetsone1.Let'scompareXORinatabletotheothers:

AND

inputA inputB output

0 0 0

0 1 0

1 0 0

1 1 1

OR

inputA inputB output

0 0 0

0 1 1

1 0 1

1 1 1

XOR

inputA inputB output

0 0 0

0 1 1

1 0 1

1 1 0

2.16-Binaryandbitmasks

117

Binaryoperationsaresomeofthemostfundamentalcomputeroperationsandcanbephysicallybuiltwithafewtransistors.Giventhesimplicityoftheseoperations,italsomakesforafastmethodofcalculatingcollisions.

Bitmasks

Let'stakealookatthissceneforamomentandidentifyfromthecrudelydrawnshapessomepotentialentities:

2.16-Binaryandbitmasks

118

Alltheseentitiesfallintouniquecategoriesinthatwewanteachofthemtocollidewithcertainotherentities.Ifthiswereagame,we'ddefineeachcategorywithauniquebinarydigit,orbit,solet'sfirstdothat:

entity category

sun 0000

player 0001

powerup 0010

enemy 0100

ground 1000

Let'ssetsomerulesforeachoftheseentities.Forinstance,wewanttheplayertocollidewiththepowerup(0010),enemy(0100),andofcoursetheground(1000).Totellthegameenginethis,wecreateabitmaskforthefixture.Thisisabinarynumberwithallthebitsswitchedonthatwewanttheentitytocollidewith.Inotherwords,theplayer'sbitmaskwouldbe(1110).Weleftthefirstbitblanksothattheplayercan'tcollidewithotherpotentialplayers(player2).Let'supdatethetablewiththebitmaskwewanteachentitytohave:

entity category bitmask

sun 0000 0000

player 0001 1110

powerup 0010 1001

enemy 0100 1001

ground 1000 1111

2.16-Binaryandbitmasks

119

Sohowdoesitallcometogetherandwork?Whentwoentity'sfixturescontact,abinaryANDoperationisperformedagaintheentity'sbitmaskandtheotherentity'scategory.Iftheresultingnumberisn't0000thenwehaveacollision.Taketheplayerandenemyforinstance:

0001player'scategory

1001enemy'sbitmask

----

0001wehaveacollision

Andhowabouttheenemyandthepowerup:

0100enemy'scategory

1001powerup'sbitmask

----

0000wehaveNOcollision

Armedwiththisknowledge,wecanassertthefollowinginformationfromthetableabove:

Thesuncollideswithnothing(anddoesn'tevengetacategory).It'sjustinthebackgroundandnon-interactive.Theplayercollideswitheverythingexceptotherplayers(andofcoursethesun).Thepowerupcollidesonlywiththegroundandplayers.Theenemycollidesonlywiththegroundandplayers.Thegroundcollideswitheverything.

Copyordownloadthe"collision"gamefromtheexamplecodeandrunit:

https://github.com/RVAGameJams/learn2love/tree/master/code/collision

Dotheentitiesinteractasexpected?Takealookinsidetheentitiesfoldertoseetheparticularfunctionbeingcalledtoaccomplishapplythecategoriesandbitmaskstoeachentity–Fixture:setFilterData

--square.lua

...

square.category=tonumber('0001',2)

square.mask=tonumber('1110',2)

square.group=0

...

square.fixture:setFilterData(square.category,square.mask,square.group)

Theexamplesaboveonlyuse4bitsforthecategoryandmaskbceausethat'sallweneeded,howeverLÖVEsupportsupto16bitsforthecategoryandbitmask( 0000000000000000).Thegrouppropertyisn'tusedandshouldbesetto0whenitisn't.Wehaven'tmentionedgroupsbeforebecauseifyouknowhowtousecategoriesandbitmasksthenyoudon'tneedtousegroupsascategoriesandbitmasksofferamorepowerfulwayofdoingthesamething.Thatbeingsaid,collisiongroupsshouldberelativelystraight-forwardtolearnaboutsoitwillbeleftupasanexercisetoreadandstudy.

ExercisesPlaywiththebitmasks.Canyoumaketheenemycollidewiththepowerupinsteadoftheplayer?TakealookathowgroupsworkasdescribedinFixture:setGroupIndex.Thisisasimpler,butmorelimitedmethodofdetectingcollision.Canitbeusedtoimitatethecollisionrulesabove?

2.16-Binaryandbitmasks

120

Networking(part1)Whencreatingaprogramsuchasagame,oneofthefirstthingstoconsidershouldbewhetheritisanetworkedapplication.Suchachoicewillradicallychangethestructureandcomplexityoftheapplication.Tobuildanetworked("online")multiplayergame,wemustunderstandsomenetworkingbasics.Someofthisinformationisoversimplified,butlet'sestablishabaselineofknowledge.

Internetprotocol(IP)Networksarepossiblebecausecomputersagreeonawaytocommunicatewitheachother.Likeogres,messagessentacrosstheinternethavemanylayers.Eachlayerrepresentsadifferentprotocolthatinterpretshowthemessageshouldbehandled.Theinternetprotocol(IP)tellscomputershowtorelaymessagestotheirintendeddestination.Therearetwothingsweneedtoknowaboutthisprotocol:IPaddressesandports.

EverydeviceconnectedtotheinternethasanIPaddressassignedtoitwhenitconnects.MessagessentoutfromyourdevicearesentwithadestinationIPaddressattachedsoitknowswheretogo.Messagesarerelayedfromonemachinetoanotheruntilitreachesthedestinationmachine'saddress.

Ifyouopenyourterminalorcommandpromptandtype pinggoogle.comyouwillgetaresponsebackthattellsyouthedestinationIPaddress;TheIPaddressoftheserverrunningthegoogle.comhomepageyousee.YoumayevenbeabletotypethatIPaddressintoyourwebbrowseranditwilldirectyoutothewebsiteinthesamefashiontypinggoogle.comintheaddressbarwould(althoughthiswon'tworkforallwebsitesbecauseofunrelated,complicatedreasons).Let'ssayyouconnectedtogoogle.comthroughtheIPaddress172.217.7.14.You'reactuallyconnectingtothatIPthroughaspecificport.Portsarerepresentedasnumbers,sothatIPlikemosteveryotherwebsiteontheinternetisaccessedthroughport443.

IPportsarelikethemaritimeportsthatharborships.Asingledestinationcanhavemultipleportsfordifferentpurposes.IfIwerebringinginamilitaryvesselImaygotoadifferentportthanacommercialvessel.

DependingonyourintentionsforanetworkconnectionyouwillusedifferentIPports.Forinstance,ifyouaretryingtoviewawebsitelocatedat172.217.7.14youwilluseport443foranHTTPSconnection,port80foranHTTPconnection(ifallowed),andifIamanadministratorofthemachinerunningon172.217.7.14Iwilluseacompletelydifferentporttoestablishabackdoorconnectionsuchasport22.

ForourexampleprogramwewilltryconnectingtoaspecialreservedIPaddress,127.0.0.1.ThisIPaddressisyourmachine'sownIPaddressituseswhenitwantstoconnecttoitself.Sincewe'llbetestingourprogrambyrunningbothcopiesonthesamecomputerwewon'tneedtoworryaboutmultipleIPaddressesfornow.Fortheportyouhavearangefrom0to65535anditdoesn'treallymatterwhichoneyouusesolongasit'snotalreadyinuseorbeingreservedforotherpurposes.We'llpickarandomonethatisunlikelytobeinusebyotherprograms...6789.

TransportlayerThetransportlayerdecideshowyourdatawillbepackagedandstreamed.Youhaveachoiceonafewdifferentprotocolsforthetransportlayer.Understandingthedetailsofeachprotocolinthetransportlayerisn'ttooimportantforthissectionofthebookbutlet'sdiscusswhywemaywanttouseoneortheother.

TCP-Thisprotocolprovidesdifferentfeaturestomakesuredatadoesn'tgetcorrupt.Mostnotably,itwaitsforaconfirmationresponsefromtheotherendtomakesurethemessagewasreceived.Ifaresponseisn'treceivedbyacertaintimeoutthentheconnectionisconsideredafailure.WebsitesuseTCP99%ofthetimebecauseofitsreliabilityandensuringyou'vereceivedthesite'sfullcontent.

2.17-Networking(part1)

121

UDP-Thisprotocolsendsdatatoaserverandexpectsnoresponseback.Sendingdatawithoutconfirmingitreachesthedestinationcouldleadtolessreliabledatatransportation.However,lessbackandforthcommunicationcouldmeanafasterconnection.Thisprotocolisusedbyapplicationsneedingtosendlotsofdataquickly,likeanaudiostreamoravideogame.Thisistheprotocolwe'lluse.

ImagineyouhavetwoplayersneedingtocommunicatetheirpositionwitheachothersowedecidetouseUDP.Youmaysendmessagesbackandforthseveraltimesasecondtocommunicateyourpositions.Sinceyouaresendingdatasorapidly,ifoneofthosemessagesislostthentheplayerpositioncanbere-synchronizednextmessage.Thisisfastandunlessoneoftheplayershasafaultyinternetconnectionyoutypicallywon'tnoticeasmalljitterorhiccupeverynowandthen.

Nowimagineanotherscenariowherewewanttosendamessagethataplayergainedanextralife.IfwewereusingUDPandthatmessagegotlost,wecouldhavetwoonlineplayerswithout-of-syncinformationthatwouldultimatelyjeopardizegameplay.OnesolutionaroundthiswouldbetouseTCPformission-criticalmessagesandUDPforeverythingelse.AnothersolutionistokeepallmessagesinUDP,buttowriteacallbackinLuaaroundourmission-criticalmessagestocheckthatwegetareply.Yup,youcanhaveyourapplicationsendUDPmessagesandexpectaresponsebuteventhoughUDPdoesn'thavethisfeatureaspartofitsprotocolyoucanstillprograminyourapplicationatimeoutthatexpectsaresponse.Thissoundslikealotofwork,butLuaandmanyotherlanguageshavelibrariesavailableyoucanrequireinyourprojectthatdothisforyou.We'llseehoweasythisislateron.

ApplicationlayerFinallywehavetheprotocolwecreateforeachrunningcopyofagametoknowhowtocommunicateonceaconnectionisestablished.Forinstanceifamessagewiththestring "ping"isbeingreceived,wemaywanttorespond "pong".Themorecomplicatedthegameis,themorecomplicatedtheprotocolwillbe.Let'scheckoutoneofthelibrariesLuaoffersfornetworkingandbuildatestprogramwithabasicapplicationprotocolwheretheserverrespondsto "meow"with "bark"andtheclientrespondsto "bark"with "meow".Asyoucanguessthiswillleadtoaninfiniteback-and-forthconversationbetweenthetwohostsifwearesuccessful.

ENetThereareseveralthird-partylibrariesforLuafornetworking.LÖVEincludestwoofthemostpopular,LuaSocketandlua-enet.LuaSocketisveryflexibleandallowsyoutocreateTCPandUDPconnections.Lua-enetisbuiltontopoftheENetlibrary,asimpleyethighperformancenetworkinglibrary.ItusesUDP,buthandleseverythingaroundthetransportlayerforussowecanfocusonourapplicationlayer.ItevendoesmessageconfirmationoverUDPforuswhenweneedittosowegetthebestofbothworlds.Let'screateaserverandclientprograminLÖVEandwe'llrunthemseparately,connectingthemtoeachother.

OurserverapplicationCreateafoldercalled serverandinitcreateafilenamed server.lua.We'llstartbyrequiring enet:

--server/server.lua

localenet=require('enet')

Thisfilewillreturnatableoffunctionsforstartingandstoppingtheserver.Tostarttheserver,weneedtocreateahostandtellitwhichIPaddressandportitisrunningon.Let'screatea server.startfunctionthatdoesjustthat:

--server/server.lua

localenet=require('enet')

2.17-Networking(part1)

122

localhost

localserver={}

server.start=function()

host=enet.host_create('127.0.0.1:6789')

end

returnserver

TheIPaddressis 127.0.0.1aswesaidwewoulduse.ThatistellingENetwewanttostarttheserveronourmachine'slocaladdress.TheIPaddressisfollowedbyacolon( :)thentheportnumber( 6789)whichisanarbitraryportthatshouldbefreetouse.Ifwecreateamain.luafilewecanrequireserver.luaandcreateaserverwhenLÖVEstarts.

--server/main.lua

--Ourserverapplication

localserver=require('server')

love.load=function()

server.start()

end

Ifwetryandrunthis,nothingwillhappen.Let'sdefine love.drawandprintsometexttotelluswhensomeoneconnectstoourserver:

--server/main.lua

--Ourserverapplication

localserver=require('server')

love.load=function()

--Keeptextpixelssharpandintactinsteadofblurring

--https://love2d.org/wiki/FilterMode

love.graphics.setDefaultFilter('nearest','nearest')

server.start()

end

love.draw=function()

--Scaleupthesizeofthetextbeingprinted

localtransform=love.math.newTransform(0,0,0,3)

ifserver.is_connected()then

love.graphics.print('clientconnectedtous(seeconsole)',transform)

else

love.graphics.print('serverstarted...awaitingclients',transform)

end

end

--It'sconvenienttobeabletopressescapetoclosetheprogram

love.keypressed=function(pressed_key)

ifpressed_key=='escape'then

love.event.quit()

end

end

Withthisdone,weneedtofigureouthowtheserverknowssomeoneisconnected.Wecall server.is_connected()inlove.draw,solet'sstartbydefiningthat:

--server/server.lua

localenet=require('enet')

localhost

2.17-Networking(part1)

123

localreceived_data=false

localserver={}

server.start=function()

host=enet.host_create('127.0.0.1:6789')

end

server.is_connected=function()

returnreceived_data

end

returnserver

Ok,so server.is_connected()willreturnthevalueof received_datawhichdefaultsto false.Nowthepartthatdoesalltheaction:

--server/server.lua

localenet=require('enet')

localhost

localpeer

localreceived_data=false

localserver={}

server.start=function()

host=enet.host_create('127.0.0.1:6789')

end

server.is_connected=function()

returnreceived_data

end

server.update=function()

ifnothostthenreturnend

localevent=host:service()

ifeventthen

received_data=true

peer=event.peer

print('----')

fork,vinpairs(event)do

print(k,v)

end

event.peer:send('bark')

end

end

returnserver

Let'stakeacloselookat server.updatepiecebypiece.Firstthingisan ifstatementtocheckthat hostisdefined.If server.updateiscalledbefore server.startthenitwon'tbesothereisnoserverupdatetobemade.Ifourserverhostwascreatedandwegetpasttheif-statementcheck,wecall host:service().Ifwereadthedocumentationfor host:servicewecanseethepurposeofcallingthisistocheckforanyincomingpackets(messages)andsendoutanywehavequeuedup.Ifwereceiveany,wewillgetbackan eventtable.Ifwedogetaneventtable,we'llchange received_datato true(whichinturnmeans server.is_connected()nowreturns true).Nextwewillcapturethepeer(theclient)thatsentusthisdatawhichwecanusetosendmessagestolater:

peer=event.peer

Whilewehavetheeventtable,let'sjustiterateoveritandprintitscontentstotheconsole:

fork,vinpairs(event)do

print(k,v)

2.17-Networking(part1)

124

end

Thenfinallywe'llsendtheclientamessagethatsimplyreads"bark".

event.peer:send('bark')

Wecannowcall server.updateinsideourgameloop's love.updatefunction:

love.update=function()

server.update()

end

Weneedtotestourserver,buttotestourserver,weneedaclient.

OurclientapplicationCreatea"client"folderlikethe"server"foldercreatedabove.Mostofthecodewillbeidenticaltoourserver.Themaindifferenceisthatwhenwecreateahostwewon'tpassitanIPaddressandporttoserveon,butinsteadwilltellittoconnecttotheaddressandporttheserverisrunningon.

--client/main.lua

--Ourclientapplication

localclient=require('client')

love.load=function()

--Keeptextpixelssharpandintactinsteadofblurring

--https://love2d.org/wiki/FilterMode

love.graphics.setDefaultFilter('nearest','nearest')

client.start()

end

love.draw=function()

--Scaleupthesizeofthetextbeingprinted

localtransform=love.math.newTransform(0,0,0,3)

ifclient.is_connected()then

love.graphics.print('connectedtoserver(seeconsole)',transform)

else

love.graphics.print('establishingaconnection...',transform)

end

end

love.keypressed=function(pressed_key)

ifpressed_key=='escape'then

love.event.quit()

end

end

love.update=function()

client.update()

end

--client/client.lua

localenet=require('enet')

localclient={}

localhost

localpeer

localreceived_data=false

client.start=function()

2.17-Networking(part1)

125

host=enet.host_create()

peer=host:connect('127.0.0.1:6789')

end

client.is_connected=function()

returnreceived_data

end

client.update=function()

ifhostthen

localevent=host:service()

ifeventthen

received_data=true

print('----')

fork,vinpairs(event)do

print(k,v)

end

event.peer:send('meow')

end

end

end

returnclient

Ifwereceiveamessagefromtheserverwe'll"meow"backatit.

TestingthingsoutIfyouruntheserveryouwillseeamessagesaying"serverstarted...awaitingclients".Sinceweareprintingtotheconsole,ifyouarerunningthiscodeonWindowsrememberthatyouwillneedtoenabletheconsole.Thiscanbedonebycreatingaconf.luafileinboththeclientandserverfolders.

--LÖVEconfigurationfile

love.conf=function(t)

t.console=true--EnablethedebugconsoleforWindows.

t.window.width=800--Game'sscreenwidth(numberofpixels)

t.window.height=600--Game'sscreenheight(numberofpixels)

end

Iftheserverisupandrunningwiththeconsoleenabled,goaheadandstarttheclientwithitsconsoleenabledtoo.Youshouldimmediatelyseeafloodofeventsprintingoutintheserverandclientconsoles.

Serverconsole:

----

peer127.0.0.1:58384

channel0

datameow

typereceive

Clientconsole:

----

peer127.0.0.1:6789

channel0

databark

typereceive

2.17-Networking(part1)

126

Thiswillgobackandforthuntilyoucloseeitheroneofthem.Ifyoucloseonethough,themessageswillstopanditwilljustsitthere.Ifyouclosetheserverfirst,forinstance,theclientwillsittherethenafterseveralsecondsamessagewillappear:

----

peer127.0.0.1:6789

data0

typedisconnect

Normallyadisconnectlikethiswouldn'tbedetectedwithUDP,buttheENetlibrarysends"heartbeat"messagesbackandforthtomakesurebothpeersarestillconnectedtoeachother.Thetimeoutisdefinedtobesomewherebetween5and30secondsbeforethepeerrealizesithasbeendisconnnectedfromtheotherone.Justtopolishthingsoffhere,let'smakeENetsendadisconnecteventtotheotherpeerimmediatelywhenweareclosingourapplication.Thelua-enetdocumentationlistsafunctionwecaninvoketodothat, peer:disconnect_now.LÖVEhasa love.quitcallbackthatiscalledwhenourapplicationisclosing.Wecanwritea server.disconnectfunctionandcallitfromlove.quit.

Server:

--server/main.lua

...

love.quit=function()

server.disconnect()

end

--server/server.lua

...

server.disconnect=function()

ifpeerthen

peer:disconnect_now()

peer=nil

end

host=nil

received_data=false

end

The client.disconnectcodewouldbeidentical.

ToseethisfullexampleorifyouhaveanyproblemsgettingyourcodetoruncheckoutthecodeonGitHub:https://github.com/RVAGameJams/learn2love/tree/master/code/networking-1

Inthenextpartwewilllookatnetworkarchitectureandaddentitiestothescreentoworkwith.

ExercisesWhathappensifyoutrytoconnectmultipleclientstotheserver?WhataboutrunningmultipleserversonthesameIPaddressandport?Whydoesitbehavelikeitdoes?

2.17-Networking(part1)

127

Networking(part2)Intheprevioussectionwemadetwoapplicationsthatcouldtalktoeachother.Oneapplicationwastheserverandthesecondoneconnectingtoitwastheclient.Ingamedesignthisstyleofnetworkingcanbedescribedasadirectconnection.

Directconnection

Inadirectconnectiononeoftheplayerstakesontheroleofserver,meaningtheirgameworldistheultimateauthorityifthereareanydiscrepanciesorout-of-synccommunicationbetweenthetwo.Thisalsomeanstheserverplayercanfindwaystocheatandexploitthegame.

Oneoftheadvantagestothissetupissinceyouaredirectlyconnectedtoeachother,yougetasminimallagaspossible.Thisadvantagedoesn'tholdtrueiftherearemorethan2players.Ifplayer1istheserverandplayer2and3areconnectedtoplayer1,thenplayer2and3havetorelayupdatestoeachotherthroughplayer1insteadofdirectlytoeachother.Outsideof2-playergames,thissetupisn'taspopularashavingadedicatedserver.

Dedicatedserver

2.18-Networking(part2)

128

Dedicatedserversareexactlywhattheysoundlike.Theyarehostsdedicatedtoservingplayers.Thedifferencehereisallplayersareclientsandtheserverisaneutralgroundwhereplayerscanconnectandcommunicateindirectlywitheachotherthroughit.Serverstypicallyrunamodifiedversionofthegamecodethathasnouserinterfaceandthereforecanrunonalessexpensivecomputer.Ifoneoftheplayersisdetectedcheatingtheservercandetectthatsomethingiswrongandkickthemfromthegame.Theserveristheultimateauthorityoverthestateofthegameworld.

Ournetworksetupwillsortofbeamixbetweenthetwostylesofnetworking.We'llhaveadedicatedserverthatdoesn'tparticipateinthegameplay,buttheserverwillhaveagraphicalinterfacesowecanviewwhatisgoingonduringourtesting.

ConsolidatingourcodeRatherthanmanagingtwofoldersofcodelikeintheprevioussection,we'llcombinethecodeanduseamenusystemtoselectbetweenbeingaserverandbeingaclient.Themenucodeisn'timportanttothistutorialsotrytofocusontheclientandservercodeasbefore.Therefactoredcodecanbefoundinthecoderepositoryhere.

Giventheamountoffilesitiseasiesttodownloadthezipofthewholeprojectwhereyouwillfindtherelevantfilesinside code/networking-2:https://github.com/RVAGameJams/learn2love/archive/master.zip

Oncedownloaded,whenyouruntheprogramyoushouldbegreetedwithamenuscreenlikeso:

2.18-Networking(part2)

129

Testitoutandconfirmyoucanconnectaserverandclientinstancewiththenewcode.

Ok.Themodificationsto main.luashouldbeeasyenoughtounderstand.Let'stakealookatthatandthenew"net"servicefirstbeforewebeginmakinganymodifications.Atthetopofthefilewe'reloadingthenetandmenuservicesthentellingthemenuservicewhichmenutoloadonstartup:

--main.lua

localmenu_service=require('services/menu')

localnet_service=require('services/net')

love.load=function()

--Keeptextpixelssharpandintactinsteadofblurring

--https://love2d.org/wiki/FilterMode

love.graphics.setDefaultFilter('nearest','nearest')

menu_service.load('main-menu')

end

Next,ifakeyispresseditwillpassthatpressed-keyeventtothemenuservice.Ifweareinthegameandnomenuisloaded,themenuservicewilldonothingwiththeevent.

love.keypressed=function(pressed_key)

menu_service.handle_keypress(pressed_key)

end

Inside love.drawwehaveasimilarstory.Ifwehaveanactivemenuthen menu_service.draw()willdrawitOtherwiseitwon'tdoanything.(Ifyouopen services/menu.luayouwillseethedrawfunctionwherethisallhappens.)

2.18-Networking(part2)

130

love.draw=function()

menu_service.draw()

--Scaleupthesizeofthetextbeingprinted

localtransform=love.math.newTransform(0,0,0,3)

ifnet_service.is_connected()then

love.graphics.print('peerconnected(seeconsole)',transform)

end

end

Anotherthinginside love.drawisachecktoseeifwe'vemadeaconnectioneitherasserverorclient(usingnet_service.is_connected())thendrawthe"peerconnected"textonthescreenasbefore.Weusetheword"peer"asagenerictermtorefertoeithertheclientwe'reconnectedto(ifwe'retheserver)ortheserver(ifwe'retheclient).

Inside love.updateand love.quitwehavecombinedthecodewehadbeforeandaddeda menu.update()call.Ifthereisamenu,updateit.Ifeitheraserverhostorclienthostisrunning, net.update()willupdateit.

love.update=function()

menu_service.update()

net_service.update()

end

love.quit=function()

net_service.disconnect()

end

Soeverywherewewerecalling"server"or"client"wejustcall net_serviceanditwilldoit'sthingnomatterthetypeofconnection.Let'sopenup net.luaandwe'llseesomethingveryclosetotheoriginalcode:

--net.lua

localenet=require('enet')

--Populateoneortheotherdependingifwestartaserverorclienthost

localclient_host

localserver_host

--Asaserver,wewanttokeeptrackofalltheconnectedclients

localpeers={}

localreceived_data=false

--Theservicewewillbereturning

localnet={}

Atthetopofthefilewecreatesomeemptylocalvariables.The nettableisfulloffunctionsthatarebeingusedinmain.luaandelsewhere.

net.start_server=function()

server_host=enet.host_create('localhost:6789')

end

net.start_client=function()

client_host=enet.host_create()

server_host=client_host:connect('localhost:6789')

end

Onthemenuwhenyouselect"Host"or"Join",the net.start_serverand net.start_clientfunctionsarebeingcalledrespectively.

Belowthataresomefunctionstocheckwhatkindofconnectionwehave:

2.18-Networking(part2)

131

net.is_connected=function()

returnreceived_data

end

net.is_client=function()

returnclient_hostandtrueorfalse

end

net.is_server=function()

returnserver_hostandnotclient_host

end

Thenfinallywehavethe updateand disconnectcodelikeweoriginallyhadintheserverandclientservices,butcombined.Oneadditiontothedisconnectfunctionisweareloopingoverthe peerslist.The peerslistexistsbecauseweareexpectingtohavemultipleplayersconnecttotheserverandiftheserverisrunning,itwillwanttodisconnectfromthemallwhenthegamequits.

CommunicationlayerBeforeweupdatethecode,let'sdiscussthefunctionalityanddrawoutthenetworkcommunicationforthatfunctionality.

WhenconnectingtotheserveryoushouldhaveacontrollableplayerspawnonscreenWhenyoumoveyourplayer,yourpeersshouldbeabletoseeyourplayermoveWhenotherpeersmovetheirplayers,youshouldbeabletoseethemmoveEachplayershouldlookdifferentsoyouknowwhichoneisyou

Client Server Description

(Createahost) (Createahost)Bothclientandserver'snetservicesarebooted.Nocommunicationhasbeenmadebetweenthetwoatthispoint.

Send"connect"messagetoserver

Clientsendsaconnectmessageautomatically.Thiswillbeinterpretedasarequesttojointhegame.

(Spawnentity)Servergeneratesandstoresalltheentitiesinthegame.Whenspawninganentity,associatetheclient'sconnectionIDwithit.

Respond"your-id|4177457821|100|500"

Serverrespondsbypassingtheclienta "your-id"message.Allmessagessentmustbestringssointhiscasewheremultiplevaluesneedtobeembeddedinthemessage,eachvalueisseparatedbyapipe("|")charactertohelpusre-separatethevalueswhenreceivingthemessageclientside.ThefirstvalueistheuniqueplayerIDthattheserverhasassignedtheclient,followedbytheXpositionthenYposition.

Send"peer-id|233142890|326|177"

Serversendsthenewclientotherpeersthatneedtobespawnedonscreen.IncludedaretheID,XandYpositionofthatplayer.

Send"move|233142890|327|177"

ServerislettingtheclientknowtheplayerwiththeID"233142890"haschangedposition.NoticetheupdatedXposition.

Send"move|233142890|328|177" Anothermoveupdate.

Send"move|100|502" Clientislettingtheserverknowitismovingitsplayertoo.

2.18-Networking(part2)

132

Send"peer-id|81850530|500|500" Anotherplayerhasjoinedtheserver.

AddingentitiesOnconnectionfromaclient,theserverneedstospawnentitiessolet'screateanentityserviceforhandlingallourentity-relatedneeds:

--entity.lua

localentity_service={}

--Allplayerentities

entity_service.entities={}

entity_service.spawn=function(player_id,x_pos,y_pos)

return{

--TODO:We'lladdacolorrandomizerlater

color={1,1,1,1},

id=player_id,

--TODO:We'lladdashaperandomizerlatertoo

shape=love.physics.newPolygonShape(0,0,50,0,50,50,0,50),

x_pos=x_pos,

y_pos=y_pos

}

end

entity_service.draw=function(entity)

love.graphics.setColor(entity.color)

localpoints={entity.shape:getPoints()}

foridx,pointinipairs(points)do

ifidx%2==1then

points[idx]=point+entity.x_pos

else

points[idx]=point+entity.y_pos

end

end

love.graphics.polygon('line',points)

end

returnentity_service

Theseentitieswilljustbebasicshapes.Noworld,body,orfixturestoworryaboutasdealingwithphysicsisabitoutofthescopeofthissection.

Nowwe'lldosomeheavyupgradestothenetservice.Somewherenearthetopofthefilewe'lldefinetwotableswithcallbacksthatwillgetinvokedwhenaneventcomesin.They'llbeemptyfunctionsfornowandwe'llfillthemoutaswego.

--Callbackstoinvokewhencertaineventsarereceivedfromapeer

--Defineacallbacktohandleeverytypeofmessageinourapplicationprotocol

localmessage_handlers={

['your-id']=function(message,event,is_server)

end,

['peer-id']=function(message,event,is_server)

end,

['move']=function(message,event,is_server)

end

}

--TheseeventtypesaredefinedbyLua-enet.A"receive"typeofevent

--isagenericeventthatcarriesanyofthemessagesabove.

2.18-Networking(part2)

133

localevent_handlers={

connect=function(event,is_server)

end,

disconnect=function(event,is_server)

end,

receive=function(event,is_server)

end

}

Thenwe'llmodifythe net.updatefunctionsoitcancalloneofthethreecallbacksinsidethe event_handlerstable:

net.update=function()

localhost=client_hostorserver_host

ifnothostthenreturnend

localevent=host:service()

ifeventthen

received_data=true

--event.typewillbeeither"connect","disconnect",or"receive"

event_handlers[event.type](event,net.is_server())

--Printouttheeventtablefordebugpurposes

print('----')

fork,vinpairs(event)do

print(k,v)

end

end

end

Soyousee, event_handlers.connectwillbecalledwhena"connect"typeeventcomesinandwe'llpassittheeventand net.is_server()booleanasitstwoparameters.Nowwecangobackandfilloutthe"connect"eventhandler.Readeachcodecommentasthereisalotgoingonhereinjustafewlinesofcode.

localevent_handlers={

connect=function(event,is_server)

--Onlytheserverneedstodostuffhereonconnect

ifis_serverthen

--event.peer:connect_id()providesuswithauniquenumber.

--We'llconvertthatnumbertoastringanduseitastheplayerID.

localplayer=entity_service.spawn(tostring(event.peer:connect_id()),100,100)

--StorethisplayerintheplayertablewiththeplayerIDasthekey.

entity_service.entities[player.id]=player

--Sendtheinitial"your-id"messagebacktotheconnectingclientsotheycanspawnthisentitytoo.

event.peer:send('your-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)

end

end,

disconnect=function(event,is_server)

--TODO:Addcodetoremoveentitieswhenaclientdisconnects

end,

receive=function(event,is_server)

--TODO:Addcodetoparse"receive"eventsandcalltheappropriatemessagehandlercallback

end

}

Sincewe'reusingtheentityserviceinnet.luawe'llneedtorequireitattheverytop:

localentity_service=require('services/entity')

Nowthattheplayerreceivesthemessagetospawnanentity,wecanfilloutthe"receive"eventhandler.

localevent_handlers={

connect=function(event,is_server)

--Onlytheserverneedstodostuffhereonconnect

ifis_serverthen

2.18-Networking(part2)

134

--We'llconvertthatnumbertoastringanduseitastheplayerID.

localplayer=entity_service.spawn(tostring(event.peer:connect_id()),100,100)

--StorethisplayerintheplayertablewiththeplayerIDasthekey.

entity_service.entities[player.id]=player

--Sendtheinitial"your-id"messagebacktotheconnectingclientsotheycanspawnthisentitytoo.

event.peer:send('your-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)

end

end,

disconnect=function(event,is_server)

--TODO:Addcodetoremoveentitieswhenaclientdisconnects

end,

receive=function(event,is_server)

--Extractthemessageoutfromtheeventandcalltheappropriatecallbackabove

localmessage={}

formatchin(event.data..'|'):gmatch('(.-)|')do

table.insert(message,match)

end

message_handlers[message[1]](message,event,is_server)

end

}

Don'tletthiscodefeelintimidatingasit'sonlytakingthestringmessagefromtheevent( "your-id:87335:500:500")andsplittingitintoalisttable( {"your-id","87335","500","500"})sowecanworkwiththedata.Oncewehavethemessagefragments,wecall message_handlers[message[1]](),where message[1]willbeoneofthekeysinthemessage_handlerstable: "your-id", "peer-id",or "move".Let'sfilloutthe"your-id"handlerfirst:

--Callbackstoinvokewhencertaineventsarereceivedfromapeer

--Defineacallbacktohandleeverytypeofmessageinourapplicationprotocol

localmessage_handlers={

['your-id']=function(message,event,is_server)

localplayer_id=message[2]

localx_pos=message[3]

localy_pos=message[4]

entity_service.player_id=player_id

entity_service.entities[player_id]=entity_service.spawn(player_id,x_pos,y_pos)

end,

['peer-id']=function(message,event_is_server)

end,

['move']=function(message,event,is_server)

end

}

Nowwhentheclientgetsthe"your-id"message,itcanspawnanentitytoo.Let'saddtheappropriatecodetomain.luafordrawingentitiessowecanseeifourprotocolisworkingsofar:

--main.lua

localentity_service=require('services/entity')

...

love.draw=function()

menu_service.draw()

--Scaleupthesizeofthetextbeingprinted

localtransform=love.math.newTransform(0,0,0,3)

ifnet_service.is_connected()andnet_service.is_server()then

love.graphics.setColor({1,1,1,1})

love.graphics.print('Serverpreview.Seeconsolefordetails.',transform)

end

for_,entityinpairs(entity_service.entities)do

entity_service.draw(entity)

end

end

2.18-Networking(part2)

135

Ifyou'veupdatedeverythingcorrectly,you'llseethishappenwhenaserverandclientrunside-by-side:

Ifyougetanerror,remembertocheckthelinenumberandfilenamewheretheerroroccurredandmakesurethecodelookssimilartohowitisabove.Ifyougetstuck,therewillbeafullexampleavailableatthebottomofthissection.

Ifeverythingisworkingforyouthenfantastic.Thismeanstheserverandclientaresuccessfullysyncinganentitystatewitheachother.

Nextlet'saddmovementsowecanseeliveupdateshappeningbetweenthetwogamewindows.Insidetheservicesfolder,we'llcreateaninput.luafilethatreturnsanemptytable:

--input.lua

return{}

Whyarewereturninganemptytable?Well,it'sonlyemptyrightnow,butaskeysarepressedandreleasedourtablewillgetupdated.Let'supdate love.keypressedandadda love.keyreleasedfunctiontomain.luatoseehowthatworks:

--main.lua

localinput_service=require('services/input')

...

love.keypressed=function(pressed_key)

menu_service.handle_keypress(pressed_key)

input_service[pressed_key]=true

end

love.keyreleased=function(released_key)

input_service[released_key]=false

end

Nowwhenwepresssomearrowkeysforinstance,ourtableininput.luawilllookmorelikethisinmemory:

{

left=true,

right=false,

up=true,

down=false

2.18-Networking(part2)

136

}

Nowinsideentity.luawe'lladdan entity_service.movefunctionthatchecksforinputchangesandmovestheentityifanyofthearrowkeysarepressed:

--entity.lua

localinput_service=require('services/input')

...

entity_service.move=function()

localplayer=entity_service.entities[entity_service.player_id]

--Don'tlettheplayerpressupanddownatthesametime

ifinput_service.upandnotinput_service.downthen

player.y_pos=player.y_pos-2

elseifinput_service.downandnotinput_service.upthen

player.y_pos=player.y_pos+2

end

--Don'tlettheplayerpressleftandrightatthesametime

ifinput_service.leftandnotinput_service.rightthen

player.x_pos=player.x_pos-2

elseifinput_service.rightandnotinput_service.leftthen

player.x_pos=player.x_pos+2

end

end

Thiswillcauseourentitytomoveacrosstheclient'sscreen,buttheserverwon'tgettheseupdatesunlesstheclientsendsthemover.Weneedtogobacktomain.luaandsenda"move"message.Inside love.updatechecktoseeiftheplayerpositionhaschangedandsendamovemessagetotheserverifso:

love.update=function()

menu_service.update()

--Checktoseeifaplayerhasspawnedandupdateitsmovementifanydirectionkeysarebeingpressed

ifentity_service.player_idthen

localplayer=entity_service.entities[entity_service.player_id]

localold_x=player.x_pos

localold_y=player.y_pos

entity_service.move()

ifplayer.x_pos~=old_xorplayer.y_pos~=old_ythen

net_service.send('move|'..player.id..'|'..player.x_pos..'|'..player.y_pos)

end

end

net_service.update()

end

The net_service.sendfunctionwehaven'tdefinedthatyet.Jumpbackovertonet.luaandwe'lldefinethatforsendingeitherclientorservermessagesifneeded:

net.send=function(message)

ifnet.is_client()then

server_host:send(message)

else

server_host:broadcast(message)

end

end

2.18-Networking(part2)

137

Nowwearesendingamessagetotheserverwhenwemove,butweneedtohavetheserverreadthemessageandupdatetheentitypositiononitsendtoo.Let'sfilloutthe"move"messagehandlerinnet.luatoaccomplishthis:

localmessage_handlers={

['your-id']=function(message,event,is_server)

localplayer_id=message[2]

localx_pos=message[3]

localy_pos=message[4]

entity_service.player_id=player_id

entity_service.entities[player_id]=entity_service.spawn(player_id,x_pos,y_pos)

end,

['peer-id']=function(message,event,is_server)

--TODO:handlepeer-idmessagesforwhenmoreplayersjointheserver

end,

['move']=function(message,event,is_server)

localplayer_id=message[2]

localx_pos=message[3]

localy_pos=message[4]

entity_service.entities[player_id].x_pos=x_pos

entity_service.entities[player_id].y_pos=y_pos

ifis_serverthen

--Relaythismessagetotheotherplayers

forid,peerinpairs(peers)do

ifid~=player_idthen

peer:send(event.data)

end

end

end

end

}

Noticetheextraservercheck.Iftheserverreceivedthe"move"commanditshouldrelayitovertoanyotherconnectedplayersotheycanseeyoumovingtoo.The peerstableneedstobedefinedinsidenet.luaabovethemessagehandlersoryoumaygetanerrorwhenittriestoloopoverthetableandseesanilvaluethathasn'tbeendefinedyet.

Tryitoutagainandweshouldseethesquaremovingonbothscreensnow.

Allthathardworkisstartingtopayoff!Wehaveafewmorechangestomakeforthistobefullyfunctionalthough.Ifyoutryandrunthegamenowwithmultipleclients,theplayerswon'tseeeachother.Inourapplicationprotocolwedefineda peer-idmessagesothatwhennewplayersconnectwereceiveinformationtospawnthem.

Insidenet.lua,goaheadandfilloutthe"peer-id"messagehandlersothatitspawnsanentitywheninvoked:

['peer-id']=function(message,event,is_server)

localplayer_id=message[2]

localx_pos=message[3]

localy_pos=message[4]

entity_service.entities[player_id]=entity_service.spawn(player_id,x_pos,y_pos)

end,

Nowtheserverneedstosendallthepeerstoanewplayerconnecting,butitalsoneedstosendnewplayerstopre-existingpeersduringaconnectevent.We'llupdatethe"connect"eventhandlertodothat:

connect=function(event,is_server)

--Onlytheserverneedstodostuffhereonconnect

ifis_serverthen

--event.peer:connect_id()providesuswithauniquenumber.

--We'llconvertthatnumbertoastringanduseitastheplayerID.

localplayer=entity_service.spawn(tostring(event.peer:connect_id()),100,100)

--StorethisplayerintheplayertablewiththeplayerIDasthekey.

entity_service.entities[player.id]=player

2.18-Networking(part2)

138

--Sendtheinitial"your-id"messagebacktotheconnectingclientsotheycanspawnthisentitytoo.

event.peer:send('your-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)

--Letalltheotherpeersknowaboutthisplayer

for_,peerinpairs(peers)do

localpeer_player=entity_service.entities[tostring(peer:connect_id())]

peer:send('peer-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)

event.peer:send('peer-id|'..peer_player.id..'|'..peer_player.x_pos..'|'..peer_player.y_pos)

end

--Addthispeertothepeerlist

peers[tostring(event.peer:connect_id())]=event.peer

end

end,

Aftersendingthenewclienttheiridas"your-id",wesendthatidtoeveryotherclientas"peer-id"intheforloop.Noticeweaddthenewconnectingclienttothepeertableattheveryend.Wedothisafterloopingoverthepeerlistsowedon'tsendthatclienta"peer-id"messageofthemselves.Again,whenregisteringpeersandentitieswecalltostring()onthe connect_id()toensurewearestoringIDsasstringsratherthannumbers.Storingdataintablesitmatterswhetheryoustorethemaskeysornumbers.See1.14-Tables(part2)toseewhatImean.

Anyways,tryrunningthegamenowwithmultipleclientsconnectingtotheserverandyouwillseeeachentitycanmoveseparatelyandthechangeswillbesynchronizedacrossallclients.Onechangetomaketheentitieseasiertodistinguishwouldbetorandomizetheircolorandshape.Let'smodify entity_service.spawninsideentity.lua:

entity_service.spawn=function(player_id,x_pos,y_pos)

localcolors={

{1,0,0,1},

{0,1,0,1},

{0,0,1,1},

{0,1,1,1},

{1,0,1,1},

{1,1,0,1},

{1,1,1,1}

}

localshapes={

love.physics.newPolygonShape(25,0,50,50,0,50),

love.physics.newPolygonShape(0,0,50,0,50,50,0,50),

love.physics.newPolygonShape(12,0,36,0,49,15,49,33,36,49,12,49,0,33,0,15)

}

return{

--Cyclethroughthelistofcolorsbasedonwhatevertheplayeridis

--Callingtonumber()tomakeplayer_idanumberinsteadofastringsowecandomathonit

color=colors[(tonumber(player_id)%#colors)+1],

id=player_id,

shape=shapes[(tonumber(player_id)%#shapes)+1],

x_pos=x_pos,

y_pos=y_pos

}

end

Hereweuseamodulustocyclethroughthelistofcolorsandshapesandassignonebasedonthepseudo-randomplayerIDwereceived.Thisalsomeansaplayerwilllookthesameoneveryotherplayers'client.Tryrunningitagainandyoushouldseesomethingsimilar:

2.18-Networking(part2)

139

Thefinalnetworkingcodecanbefoundhere.Again,giventheamountoffilesifyouneedthewholefolderthenitiseasiesttodownloadthezipofthewholeproject.Youwillfindtherelevantfilesinside code/networking-3:https://github.com/RVAGameJams/learn2love/archive/master.zip

ConclusionThisisaboutasbasicinfunctionalityasamultiplayergamecanget.Therearemanyfeaturesmissingfromourcode.Justtonameafewmajorones:

Sanitychecksonmessages.Youcancrashtheserverbysendingitaninvalidmessage.The"move"messageexpectsaplayeridwhentheservershouldbeabletofigurethisoutonitsown.Theservercaneasilybefooledintoaccepting"move"messagesfromaclienttomoveanotherclient.Tomakethingssimplerthereisnoworldorphysicsnoranynetcodetohandlecollisionsoranythingofthelike.Theentitiesjuststickaroundwhenyouclose/disconnectaclient.Ifyouloseconnection,thereisnoattempttorestoretheconnection.

ThereisagreatsetofarticlesbyGabrielGambettaontheproblemsfacedbymakingaaction-basedmultiplayergamesandit'sworthareadtogetahigh-leveloverviewofthechallengesandhowtoreasonaboutthem.Linktohisarticles.

ExercisesHavingalltheplayersspawnattheexactsamepointisn'tideal.Insidenet.luainthe"connect"eventhandler,makealistofspawnpointsandmaketheplayersspawnatoneofthepointsbasedontheir peer:connect_id().Hint:thecodeshouldlooksimilartothecolorcyclecodeinside entity.spawninentity.lua.Insidenet.lua,completethe"disconnect"eventhandlerandmakeissowhenaclientquitstheirentityisremovedfromthegameforallpeers.

2.18-Networking(part2)

140

Chapter3:ProgrammingindepthThegoalofthischapteristotouchonavarietyoftopicsandproblemsfacedwhileprogrammingandtounderstandandsolvethemusingLua.Awidervarietyoftopicswillbecoveredhere.Onetopicwillleadintoanotherwhichwillthenbuildontopofconceptsintroducedintheprevioustopic.

3.0-Programmingin-depth

141

PrimitivesandreferencesTakealookatthiscode:

localstring1="hello"

localstring2='hello'

print(string1==string2)

localnumber1=14

localnumber2=14

print(number1==number2)

localtable1={}

localtable2={}

print(table1==table2)

localfunction1=function()end

localfunction2=function()end

print(function1==function2)

Whatwouldhappenifyouweretorunthis?

Inchapter1welearnedaboutcomparingstringswiththe ==operatorwhenwetalkedaboutbooleans.RunthecodeaboveintheREPLandseewhatitreturns:

true

true

false

false

Thestringsequalandthenumbersequal,butwhyaren'tthetablesandfunctionsequalsincetheyarebothempty?Tryprintingthetablesandfunctionsandlookwhathappens:

localtable1={}

localtable2={}

print(table1)

print(table2)

localfunction1=function()end

localfunction2=function()end

print(function1)

print(function2)

table:0x16af270

table:0x16af220

function:0x16ae840

function:0x16aeff0

Attemptingtoprinteachvalueyouaregivenbackahexadecimalnumber,theplaceinmemorywherethosevaluesarelocated.Eachtableandfunctionresidesinadifferentplaceinmemory.Sohowisthisrelevant?

3.01-Primitivesandreferences

142

Whencheckingdatalikestringsandnumbers,the ==operatordoesindeedcheckthatthedatamatches.Thesedatatypesaresimpleandtakeverylittleeffortforacomputertocheckthattheyareequal.Booleans,strings,numbers,and nilareallprimitivetypesofdataandbehavethisway.

Whencheckingdatalikefunctionsandtables,however,the ==operatorchecksthememorylocationofthedataonbothsidesoftheoperatorandifthevariablesreferencethesamelocationthentheyareequal.Inotherwords,the==operatorchecksthesedatatypestoseeiftheyhavethesameidentity.Nomatterhowmanyemptytablesorfunctionsyouhave,eachoneiscreatedwithauniqueidentity.

localstring1='hello'

localstring2="hello"

--Anothercopyof"hello"iscreatedinmemory:

localstring3=string2

--Butthesetwocopiesareequal

print(string2==string3)

localtable1={}

localtable2=table1

print(table1==table2)

Whatistheresultof print(table1==table2)?Aha!Boththesevariablesreferencethesamedata.Quick–amagicianwavestwowandsinfrontofyourfaceandasksyoutocounthowmanywandsthereare.Howdoyouknowiftherearereallytwowandsorifthisisjustatrickwithmirrors?Whatdoyoudo?Youtakeoneofthewandsandbreakitofcourse.Iftheotherwandbreaksthentheywerethesamewandtheentiretime.Let'strythatwiththetwoobjects:

localtable1={}

localtable2=table1

table1.rabbit='white'

print(table2.rabbit)--Equals'white'too

Aslongasyourvariablesreferencethesametable,updatingthetablefromonevariableyouwillseetheresultwhencheckingtheothervariable.Thisdoesn'tworkwithprimitivedatabecauseyou'realwaysmakingacopywhenassigningittoanewvariablename:

localstring1='hello'

localstring2=string1

string1='world'

returnstring2

=>hello

Primitiveversusnon-primitivedatatypesWheneverweassignnon-primitivedatatoanewvariable,we'realwaysreferencingtheoriginaldata:

localgrocery_list={

'carrots',

'celery',

'pecans'

}

localsame_list=grocery_list

3.01-Primitivesandreferences

143

grocery_list[1]='grapes'

returnsame_list[1]

Butassigningprimitivedatatoavariable,evenprimitivedatainsidetables,we'realwaysmakingauniquecopy:

localgrocery_list={

'carrots',

'celery',

'pecans'

}

localitem_copy=grocery_list[1]

print('item_copyis'..item_copy)

grocery_list[1]='grapes'

print('item_copystillis'..item_copy)

Ifyouneedtomakeeachiteminyourtablereference-able,youneedtomakeeachitemanon-primitivedatatype:

localgrocery_list={

{name='carrots',location='produce'},

{name='celery',location='produce'},

{name='pecans',location='baking'}

}

localitem_reference=grocery_list[1]

print('item_referenceis'..item_reference.name)

grocery_list[1].name='grapes'

print('item_referenceisnow'..item_reference.name)

Soratherthanreplacingthefirstiteminthelist,thefirstitemwasretainedandonlymodified:

item_referenceiscarrots

item_referenceisnowgrapes

Cloningnon-primitivedatatypesAswearefamiliarwithatthispoint,tablesareaspecialdatatypethatcancontainotherdatatypes.Youcanbuildstructurescontainingstrings,variables,andevenothertables.Thatmakesthetableacompositedatatype,inotherwords,adatatypewithdistinguishableparts.Notalllanguageshavecompositedatatypes,butforLuathetableisoneofitsprimaryfeatures.

Onethingaprogrammermaywanttodowithatableisonceconstructed,createacopyofit.Iftherewasatableforamonsterinavideogame,youmaywanttohavemorethanonetable.Ifyoudidthis:

localenemy1={health=10,strength=12,type='orc'}

localenemy2=enemy1

Youwouldstillonlyhaveonetable.Youcouldusealooptocopyallthevaluesoutofatableandintoabrandnewtable.Afunctiontodothatmaylooklikethis,moreorless:

3.01-Primitivesandreferences

144

localcopy=function(orig_table)

localnew_table={}

forkey,valueinpairs(orig_table)do

new_table[key]=value

end

returnnew_table

end

localenemy1={health=10,strength=12,type='orc'}

localenemy2=copy(enemy1)

Thereisnothingterriblywrongwiththismethod,butamoreefficientwaytodosuchathingwouldbetoconstructeachmonstertableinsideafunctioninsteadofcopyingonefromanother.Thismethodwillbefamiliaralreadyifyoureadandfollowedthroughthebreakoutgame.

localcreate_orc=function(strength)

return{

health=10,

strength=strength,

type='orc'

}

end

localenemy1=create_orc(12)

localenemy2=create_orc(12)

Everytimethefunction create_orcisran,itconstructsanewtablefromscratch.Youdefineanorc-styletableonlyonceanddon'tneedtoreadvaluesinfromonetabletoanother.Afunctionthatconstructstablesforyouisacommonparadigminprogrammingknownasafactoryfunction.Youmadeafactorythatbuildsorcs!Ofcoursethisfactoryfunctionparadigmworkswithothernon-primitivetypesofdataaswell:

localcreate_function=function()

returnfunction()return1+1end

end

localfn1=create_function()

localfn2=create_function()

print(fn1)

print(fn2)

Afunctionthatgeneratesotherfunctions?Thismayseemlikeanoddthingtowanttodo,butthismethodofprogrammingcanbequiteusefulaswe'llseein3.02-Higher-orderfunctionsandlaterfollow-upsections.Onethingthatshouldbementionedthoughisthatfunctionscanalsobeconsideredacompositedatatypeasitcanreturnotherdatatypes,andevenotherfunctions.Compositeinthatyoucancomposehigher-orderfunctionalityinthewaytablescanbeusedtocomposehigher-orderstructures.

ConclusionWhencomparingorreferencingdata,alwayskeepinmindwhetheryouhandlingprimitiveornon-primitivedata.Ifyouaremodifyingdatainoneplace,thinkifthismightbeaffectingyousomewhereelseinyourprogram.Evenwhenwritingouta localsome_module=require('some-module')inyourcode some_moduleisjustatableandlikeeveryothertable,everyreferencetoitcanaffecteachother.Somodifying some_moduleintwodifferentfilescanhaveeitherbeneficialordisastrousconsequencesdependinghowmuchcareandregardyougiveyourcode.

3.01-Primitivesandreferences

145

Higher-orderfunctionsIn1.07Makingfunctionswelearnedabout,well,makingfunctions.Sowhatabouthigher-orderfunctions?Whataretheyandhowdowemakethem?Simplyput,higher-orderfunctionsarefunctionsbuiltontopofotherfunctions.Here'sabasicexample:

localrun_twice=function(some_function,some_data)

some_function(some_data)

some_function(some_data)

end

run_twice(print,'HelloWorld!')

Itcantakeanyfunctionandrunittwiceforyou,inthiscasethe printfunction,butitcouldbeanyfunctionyoupassit.Typicallyhigher-orderfunctionsreturndata.Here'satrickierexamplethatdoesjustthat:

localtwice=function(fn,val)

returnfn(fn(val))

end

localadd_four=function(num)

returnnum+4

end

returntwice(add_four,12)

Takealookatthebottomlineforasecond.Wearecallingthefunction twicewithtwoarguments,the add_fourfunctionandthenumber 12.Thepurposeofthe twicefunctionistotakeavalue, 12inthiscase,andrunitthroughthegivenfunction( add_four)twice.Nowtakealookinsidethe twicefunction.Insideitreturnsfn(fn(val)).Givenwhatweknowisbeingpassedtothisfunction,thiscanbereadassayingadd_four(add_four(12)).Theorderofoperationsaystostartfromtheinner-mostparenthesisandworkyourwayout:

add_four(add_four(12))

becomes

add_four(16)

whichbecomes

20

andthatiswhatisreturnedwhenyourunthecode.Thepowerofthesehigher-orderfunctionsisthattheyarere-usable.Youcangivethe twicefunctionanythingthattakesandreturnsavalue:

localtwice=function(fn,val)

returnfn(fn(val))

end

localdouble=function(number)

returnnumber*2

end

returntwice(double,3)

3.02-Higher-orderfunctions

146

...orsimilartoouroriginalexample:

localtwice=function(fn,val)

returnfn(fn(val))

end

localshout=function(message)

print(message..'!!')

returnmessage

end

returntwice(shout,'hello')

Thereareallexamplesofhigher-orderfunctionsthatacceptafunctionasanargument.Anotherkindofhigher-orderfunctionisonethatreturnsanotherfunction:

localwrapper=function()

returnfunction()

return'Youfoundthetreasure!'

end

end

localkinder_surprise=wrapper()

localsecret=kinder_surprise()

returnsecret

Whenweran wrapperitreturnedusanotherfunctionthatwehadtoinvoketogettotheinnermostvalue.Toavoidallthevariablenames,youcansavesometimeandinvokesuchkindsoffunctionslikeso:

localwrapper=function()

returnfunction()

return'Youfoundthetreasure!'

end

end

returnwrapper()()

ClosuresWhichnumberwillprintoutbyrunningthefollowingcode?

localnumber=3

localclosure=function()

localnumber=5

returnfunction()

print(number)

end

end

localprint_number=closure()

print_number()

Strange?

Ok,solet'stryathissamefunction-returning-a-functionthingbutpassinginsomedata:

3.02-Higher-orderfunctions

147

localadder=function(a)

returnfunction(b)

returna+b

end

end

localadd_three=adder(3)

returnadd_three(1)

The add_threevariableisassignedauniqueandspecialfunction.Itisassignedtheinnerfunctionwithintheadderfunction,butwiththedatawepassedinnowassignedtothe avariable.Eventhoughthefunctionwasreturnedoutsideofthescopeitwasdefinedin,thescope'sdatawasenclosedinsidethereturnedfunctionuntilthefunctionwasdiscardedandtheprogramexited.Thesetypesoffunctionsarecommoninsituationswhereafunctionneedstobegeneratedmultipletimesbutwithdifferentdatasets.

Thedataintheclosurecanalsocontinuetobeupdated,givingyoutheabilitytomakestoragecontainersforyourdata.Trythisout:

localmake_counter=function()

localnumber=0

returnfunction()

number=number+1

returnnumber

end

end

localcount=make_counter()

print(count())

print(count())

print(count())

print(count())

InprogramslikeLÖVEtherearecallbacksystemswhereasimilareffecthappens:

localentity=require('entity')

love.draw=function()

entity:draw()

end

Asseeninthepreviouschapter,the love.drawcallbackisdefinedinamain.luafileandlaterinvokedsomewherewithinthegameengine.Since love.drawwasdefinedinthescopewheretheentityvariableisdefined,theentityvariablelivesonandcanbeusedinside love.drawlongafterthemain.luafileisdonebeinginvoked.

ConclusionClosurestakesomepracticetounderstandandappreciate,butonceyouseepracticalexamplesofwhereandhowtousethemtheybecomeanindispensableitemonyourprogrammingtoolbelt.Intheprevioussectionweusedthetermcompositedatatocompareprimitiveandnon-primitivedatatypes.Inthissectionwesawhowtogoaboutcomposinghigher-orderfunctions.Inthefollowingpageswewillcoversomehigher-orderfunctionsthatarethebuildingblocksforoldandmodernsoftwarealike.

Exercises

3.02-Higher-orderfunctions

148

Inthe make_counterexampleabove,trygeneratingmultiplecounters:

--Dothenumbersineachcounterstayin

--syncoraretheytrackedindependently?

localcount_a=make_counter()

localcount_b=make_counter()

Usingthesame make_counterexample,modifyittoreturnatableinsteadofafunction.Withinthistable,definean incrementand decrementfunctionsothatyoucanmakethecounternumbergoupordown.Howwouldyouusesuchafunction?

3.02-Higher-orderfunctions

149

MapandfilterIntheprevioussectionwepracticedcreatingsomehigherorderfunctions.Inthissectionswe'llcomposetwohigher-orderfunctionscommonlyusedininternetapplicationsfortransforminglists.

We'llstartbytakingalookatourgrocerylisttoseewhatitemsweneedtopickup:

localgrocery_list={

{

name='grapes',

price='7.20',

location='produce'

},

{

name='celery',

price='5.50',

location='produce'

},

{

name='walnuts',

price='6.20',

location='baking'

},

{

name='sugar',

price='8.00',

location='baking'

},

{

name='mayonnaise',

price='3.50',

location='dressings'

},

{

name='cream',

price='3.00',

location='dairy'

}

}

Thislisthasmoreinformationthanwewanttoseeataquickglance.Ifwewantedtoonlydisplayanumberedlistofitemnames,wecoulddosobywritingafor-loopthatgeneratesanewlistforus:

localnew_grocery_list={}

forkey,valueinipairs(grocery_list)do

new_grocery_list[key]=key..'.'..value.name

end

for_,valueinipairs(new_grocery_list)do

print(value)

end

Herewegeneratedalistwithaloopthenloopedoverthelistagaintoprintourresults:

1.grapes

2.celery

3.walnuts

4.sugar

5.mayonnaise

6.cream

3.03-Mapandfilter

150

Thisworksgreatforsimplecodelikethisexample,butitcangetmessyifyouareworkingwithmanylistsorifyouwanttotransformliststodifferentformats.

MapHere'sourhigherorderfunction, map.Ittakesalistandafunctionasargumentsthenreturnsanewlist.

localmap=function(list,transform_fn)

localnew_list={}

forkey,valueinipairs(list)do

new_list[key]=transform_fn(value,key)

end

returnnew_list

end

Anewlistiscreatedbyloopingovereachitemintheoriginallist,applyingyourfunctiontotheitem,thenassigningthetransformeddatatothenewlist.Ourcodecanbere-writtentousethemapfunction:

localmap=function(list,transform_fn)

localnew_list={}

forkey,valueinipairs(list)do

new_list[key]=transform_fn(value,key)

end

returnnew_list

end

localgrocery_list={

{

name='grapes',

price='7.20',

location='produce'

},

{

name='celery',

price='5.50',

location='produce'

},

{

name='walnuts',

price='6.20',

location='baking'

},

{

name='sugar',

price='8.00',

location='baking'

},

{

name='mayonnaise',

price='3.50',

location='dressings'

},

{

name='cream',

price='3.00',

location='dairy'

}

}

localnew_grocery_list=map(grocery_list,function(item,index)

returnindex..'.'..item.name

end)

3.03-Mapandfilter

151

for_,valueinipairs(new_grocery_list)do

print(value)

end

Calling map(...)wegetbackthenewlistthenweloopoveritagainjusttoprintourresultsout.Noticehowthesecondargumentwepassedintomapisjustafunctionwithnoname.Functionswithnonamesaresometimescalledanonymousfunctions.Insomelanguagesthey'recalledlambdas,especiallywhenusedinsideahigher-orderfunctioninasituationlikethis.Thetransformfunctiontakesintheitemanditsindexandmustreturnbackanewresultformaptoputinsidethenewfunction.

Maybeafewmoreexampleswillhelpout,sowhatifwewanttoreturnanotherlistwithjustthepricessowecanadduphowmuchweneedtospend?

localprice_list=map(grocery_list,function(item)

print(item.price)

returnitem.price

end)

7.20

5.50

6.20

8.00

3.50

3.00

Herethemapfunctionispassedinatransformfunctionwithaprintstatementinsideit.Thatwayitwillprinttheitempricesasitbuildsthelistsoyoucanseewhateachvaluewillbe.

Ifyouhadotherlistsforwhichyouwantedtoprintprices,itcouldbedonequiteeasilywith map:

localtransform_fn=function(item)returnitem.priceend

map(grocery_list,transform_fn)

map(car_parts,transform_fn)

map(card_transactions,transform_fn)

FilterLet'ssaywewantedtoonlyseethethingsonourgrocerylistthatareinthebakingaisle.Wecouldwritealooptodothat:

localfiltered_list={}

for_,valueinipairs(grocery_list)do

ifvalue.location=='baking'then

filtered_list[#filtered_list+1]=value

end

end

for_,valueinipairs(filtered_list)do

print(value.name)

end

Tryrunningthatandonceitmakessense,let'sthinkabouthowtoturnthisintoare-usablehigher-orderfunctionlikemap.We'llmakeafunctioncalled filterthat,like map,takesalistandafunction.Thefunctionwillreturn trueifitwantstoputaniteminthenewlistor falseifitdoesn't.We'llcallitthepredicatefunction.

3.03-Mapandfilter

152

localfilter=function(list,predicate_fn)

localnew_list={}

forkey,valueinipairs(list)do

--Thepredicate_fnthatwaspassedinshouldreturn

--avaluethatevaluatestoeithertrueorfalse.

ifpredicate_fn(value,key)then

new_list[#new_list+1]=value

end

end

returnnew_list

end

Andwecanusethisfunctiontofilterdowntojustourbakingitemslikethis:

localfilter=function(list,predicate_fn)

localnew_list={}

forkey,valueinipairs(list)do

ifpredicate_fn(value,key)then

new_list[#new_list+1]=value

end

end

returnnew_list

end

localgrocery_list={

{

name='grapes',

price='7.20',

location='produce'

},

{

name='celery',

price='5.50',

location='produce'

},

{

name='walnuts',

price='6.20',

location='baking'

},

{

name='sugar',

price='8.00',

location='baking'

},

{

name='mayonnaise',

price='3.50',

location='dressings'

},

{

name='cream',

price='3.00',

location='dairy'

}

}

localfiltered_list=filter(grocery_list,function(item)

returnitem.location=='baking'

end)

for_,valueinipairs(filtered_list)do

print(value.name)

end

3.03-Mapandfilter

153

walnuts

sugar

Noticeourpredicatefunctionwewrote:

function(item)

returnitem.location=='baking'

end

Theoperationafterthe returnalwaysreturnsaboolean trueor false,so filterknowsexactlywhattodowiththeitembasedonthoseresults.

Youcanimaginethe filterfunctioncouldbeusefulforprocessingasearchquery.Forinstance,ifwewantedtoseeonlymedium-sizedshirtsthatfitaspecificpricerange:

filter(products,function(item)

ifitem.type=='shirt'then

ifitem.size=='M'then

returnitem.price<40

end

end

returnfalse

end)

CaveatsThefilterfunctionreturnsanewlist,buttheitemsintheliststillreferencetheoldlistiftheyaren'tprimitives.Forinstanceifwemodifiedthegrocerylist,thefilteredcopywouldbeupdated.

localfiltered_list=filter(grocery_list,function(item)

returnitem.location=='baking'

end)

grocery_list[3].name='peanuts'

print(filtered_list[1].name)

peanuts

Thisbehaviorcanbeadvantageousifit'sexpected,butit'ssomethingthatshouldbeunderstoodabouthowLuaandsimilarprogramminglanguageswork.Thisisexplainedmorein3.1-Primitivesandreferences.

Anotherthingtoconsideriswhetherornottowritethefunctionsyourselfortouseapre-writtenlibraryyoucanrequireintoyourproject.Notallimplementationsarethesameandsomemayperformbetterthanothers,orbehavedifferently.Somelanguageshavebuilt-inversionsofthesefunctionstostandardizethings.UnfortunatelyLuadoesn'tprovidethesefunctionsbuiltinorasastandardlibrary.

Atleastyounowknowhowtowritethemyourselfiftheneedarises.

ExercisesTryfilteringthegrocerylisttoonly"produce"items,thenmappingthoseresultsdowntojustthenames.Using filter,nowcanyoureturnthenumberofitemsinthegrocerylistwithapriceofmorethan5?Hint:youwillneedtousetonumber()toconverttheitempricestonumbersforcomparing.

3.03-Mapandfilter

154

StackandrecursionWhenrunningaprogram,theinterpreter(Luainthiscase)keepstrackofvariablesdefinedinascopeandwhichfunctionyouarecurrentlyin.Itorganizesthisinformationintoalistinmemorycalledthestack.Thefirstiteminthestackisthestartingpoint-therootofyourapplication.Takethefollowingexample:

localtwo=function()

print('two')

end

localone=function()

print('one')

two()

end

one()

Whenstartingtheprogram,thestartofthestackisthetoplevelofthemodule.TheLuastackcallsthisthe"mainchunk".Whenafunctionisinvoked,anotherlayerisaddedtothestack.Everytimeafunctioniscalledfromanotherfunction,thestackcontinuestobuild.Sowiththeexamplecodeabove,Thestackwillfollowtheprogression:

Stackis {"mainchunk"}.Nowstartexecuting one.Stackis {"mainchunk","one"}.Nowstartexecuting twowhilestillin one.Stackis {"mainchunk","one","two"}.twoisdoneexecuting.Stackisnow {"mainchunk","one"}.oneisdoneexecuting.Stackisnow {"mainchunk"}.Programexits.

Thiscanbevisualizedbythrowinganerroratanypointtheprogram.Theinterpreterwillgiveyoubackastacktracethatdetailswhereitwaswhentheproblemoccurred.Luaprovidesahelpful errorfunctionfordebuggingthatwecanusehere:

localthree=function()

error('Thisisanerror.')

end

localtwo=function()

print('two')

three()

end

localone=function()

print('one')

two()

end

one()

UnfortunatelytheREPLdoesn'tprovideuswithstacktraces,butifyouhaveaLuainterpreteronyourcomputer( luacommand, luajit,orLÖVE)youwillseetheerrormessageandastacktracelikethis:

lua:test.lua:2:Thisisanerror.

stacktraceback:

[C]:infunction'error'

test.lua:2:inupvalue'three'

test.lua:7:inupvalue'two'

test.lua:12:inlocal'one'

3.04-Stackandrecursion

155

test.lua:15:inmainchunk

[C]:in?

Fromthe"stacktraceback"youcanseethenewestfromthetopofthestacktotheoldestonthebottom.Incomplexprogramsiscanbeverybeneficialtoseewhichfunctioninvokedanotherfunctiontohelptracedownhowanerrorcameabout.

Understandingthestackisbeneficialformorethanjustreadingerrors.Let'sswitchtheconversationovertosomethingseeminglyunrelatedforabit.

RecursionWhenthinkingofloops,manyprogrammersfirstthinkofthe forlooporthe whileloop.Anothercommonmethodistomakeafunctioncallitself.Similartothe whileloop,youcancreateinfiniteloopslikethisone

localloop

loop=function()

print('hello!')

loop()

end

Whenafunctioninvokesitself,whetherdirectlyorindirectly,thisiscalledrecursion.Thesamefunctionwillrecuragainandagainuntilaconditionchanges.Orinthecaseabove, loop()willbecalledunconditionally.Withoutacondition,anykindofloopwillruninfinitely(orcrashtrying).Here'saloopthatisalittlesafertorun:

localcount_to_5

count_to_5=function(current_number)

print(current_number)

ifcurrent_number<5then

count_to_5(current_number+1)

end

end

count_to_5(1)

Whichprints:

1

2

3

4

5

Onequicklittleaside;Noticehowthefunctionwasdefinedinboththesesituations:

localloop

loop=function()

...

Thevariablewasdefinedbeforethefunctionwascreated.Sincethefunctionneedstoaccessthevariableinsideitself,thevariableneedstoexistatthetimethefunction'sscopeiscreated.Variablescreatedafterthefunctionareunknowntothefunction.Thisisdiscussedin1.17-ScopesandisalimitationofLua'sdesign.Fortunatelythereisshorthandsyntaxforwritingrecursivefunctions:

localfunctioncount_to_5(current_number)

print(current_number)

3.04-Stackandrecursion

156

ifcurrent_number<5then

count_to_5(current_number+1)

end

end

count_to_5(1)

isthesameaswriting:

localcount_to_5

count_to_5=function(current_number)

...

Let'stryanotherrecursiveloop:

localgrocery_list={

'pumpkin',

'pecans',

'butter',

'flour',

'sugar'

}

localfunctionprint_items(list,index)

index=indexor1

ifindex<=#listthen

print(list[index])

print_items(list,index+1)

end

end

print_items(grocery_list)

Whichprintsthegrocerylist.Don'tforgetthe localatthebeginningof localfunctionprint_items,otherwiseyouwillaccidentallygenerateglobalvariablesinyourcodewhentryingtodefinefunctions.

Wecanevenre-implementour mapfunctionfromearliertouserecursioninsteadofa forloop.

localgrocery_list={

'pumpkin',

'pecans',

'butter',

'flour',

'sugar'

}

localfunctionmap(orig_list,transform_fn,new_list)

new_list=new_listor{}

if#new_list<#orig_listthen

localindex=#new_list+1

new_list[index]=transform_fn(orig_list[index],index)

returnmap(orig_list,transform_fn,new_list)

end

returnnew_list

end

localnew_list=map(grocery_list,function(value,index)

returnindex..'.'..value

end)

map(new_list,function(value)

print(value)

returnvalue

end)

3.04-Stackandrecursion

157

Whichprints:

1.pumpkin

2.pecans

3.butter

4.flour

5.sugar

StackoverflowSowhatdoesthestacklooklikeduringrecursionwhenafunctionentersitself?Here'sascripttotest:

localfunctionrecur(n)

--assertislikeerror,buttakesanexpressiontotest.Ifthe

--expressionpassedbecomesfalsethenitthrowstheerrormessage.

assert(n<5,'Thisisaconditionalerror')

print(n)

recur(n+1)

end

recur(1)

lua:test2.lua:2:Thisisaconditionalerror

stacktraceback:

[C]:infunction'assert'

test2.lua:2:inupvalue'recur'

test2.lua:4:inupvalue'recur'

test2.lua:4:inupvalue'recur'

test2.lua:4:inupvalue'recur'

test2.lua:4:inlocal'recur'

test2.lua:7:inmainchunk

[C]:in?

Everytimethefunctionrecurswegetanotheradditiontothestack.Thiscanbeaproblemifyouareloopingoveralargesetofdatabecausethestackwillconsumemoreandmorememoryasitstacksup.Thiscanbeaccomplishedbycreatingarecursiveloopthatrunsinfinitely.Ifyouhaven'ttriedsoalready,here'saneasyexample:

localfunctionrecur()

recur()

end

recur()

Whenthestackreachesacriticalsize,yougetastackoverflowerror:

lua:test3.lua:2:stackoverflow

stacktraceback:

test3.lua:2:inupvalue'recur'

test3.lua:2:inupvalue'recur'

...

test3.lua:2:inupvalue'recur'

test3.lua:2:inupvalue'recur'

test3.lua:2:inlocal'recur'

test3.lua:5:inmainchunk

[C]:in?

Withaspecific returnstatementaddedtotheloop,however,wenolongergetastackoverflow:

3.04-Stackandrecursion

158

localfunctionrecur()

returnrecur()

end

recur()

Thiswillrununtilyoumanuallykilltheapplicationprocess.Killingitreturnsasomewhatmysteriousstacktrack:

lua:test4.lua:2:interrupted!

stacktraceback:

test4.lua:2:infunction<test4.lua:1>

(...tailcalls...)

test4.lua:2:infunction<test4.lua:1>

(...tailcalls...)

test4.lua:5:inmainchunk

[C]:in?

Sohowdidourmodificationsaveusfromoverflowingourstack?

TailcalloptimizationInsideafunctionwhenyoureturnanotherfunctioncall,theinterpreterhastheabilitytore-usethesamelayerofthestackinsteadofcreatinganotherlayer.Thisworkswithdirectrecursion(functioncallingitself)andindirect(mutual)recursionsuchastwofunctionscallingeachother:

localone

localtwo

one=function()

returntwo()

end

two=function()

returnone()

end

one()

ProgramminginLuagoesintogreaterdetailonwhenarecursionwillorwon'tbeoptimized,butthesimplethingtorememberisthatthefunction(s)mustreturnthevalueofinvokingafunctionforthistowork.Thefollowingwillbetail-calloptimized:

localone

localtwo

one=function(n)

print(n)

returntwo(n+1)

end

two=function(n)

print(n)

returnone(n+1)

end

--Countuntilwerunoutofnumbers

one(1)

Butthefollowingwon't,sinceitreturnsanoperationincludingthefunctioncallinsteadofjustthefunctioncallitself:

3.04-Stackandrecursion

159

localone

localtwo

one=function(n)

print(n)

return1+two(n)

end

two=function(n)

print(n)

return1+one(n)

end

--Thiswon'twork!

one(1)

ThecaseforrecursiveloopsSowhywouldwewanttodorecursion?Itseemstrickierthana forloopandperhapsjustaseasytomessupasawhileloop.

It'snotnecessarilyareplacementforthe forloop,butallowsyoutodocertainthingsyoucan'teasilydowithoutrecursion.TakethisexamplefromRosettaCodewhichwillflattenalistoflistsintoasingle,flatlist.Itusesa forloopandarecursiveloopinconjunctionwitheachother:

localfunctionflatten(list)

iftype(list)~="table"thenreturn{list}end

localflat_list={}

for_,eleminipairs(list)do

for_,valinipairs(flatten(elem))do

flat_list[#flat_list+1]=val

end

end

returnflat_list

end

localtest_list={

{1},

2,

{{3,4},5},

{{{}}},

{{{6}}},

7,

8,

{}

}

print(table.concat(flatten(test_list),","))

Whichprints:

1,2,3,4,5,6,7,8

Thisfunctionisn'ttail-calloptimized,butitprobablywon'tbepassedanestedlistdeepenoughtocauseastackoverflow.

Here'sjustafewofthemanysituationswhererecursionisusuallythebesttoolforthejob:

SortingdataSearchingtrees(nesteddata)inadatabaseornestedfoldersinafilesystem.

3.04-Stackandrecursion

160

FindingtheshortestpathbetweentwopointsLoopsthatincrementordecrementinirregularpatternsEvaluatingafinitesetofmovesinagamelikechess

Thepointisn'ttoreplacethe forloop,althoughyoucan.Takethefollowingexample,whichreturnsthefactorialofthegivennumber(5):

localfact=function(n)

localacc=1

foriteration=n,1,-1do

acc=acc*iteration

end

returnacc

end

print(fact(5))

Thesamefunctionalitywrittenwitharecursiveloopwouldlookverydifferent:

localfunctionfact(n,acc)

acc=accor1

ifn==0then

returnacc

end

returnfact(n-1,n*acc)

end

...butonemethodwouldn'tofferanadvantageovertheotherhere.Dependingonthelanguageyouareworkingin,onemethodmaybeeasiertoreadthantheother.Maybethelanguagesupportsonetypeofloopandnottheother.Thesearethefactorsthatwilloftendothedecidingforyou.

3.04-Stackandrecursion

161

Reduce(fold)Inprevioussectionswediscussedmanymethodsforiteratingoverdataandtransformingit.Inthissectionwe'lldiscussanotherhigherorderfunctionthatisarguablyoneofthemostpowerful.Itisaconceptrecognizedacrossenoughprogramminglanguagestogetitsownwikipediaarticle.Mostpopularlanguagescallitreduce,althoughsomelanguageswillcallitfoldorinject.Here'stheparametersittakes,andalthoughtheorderoftheparametersmaybedifferentinotherlanguagesthefunctionalityandoutputwillbethesame.

reduce(list,fn,starting_value)

Likewith map()and filter(),ittakesalistyouwanttotransformandafunction( fn)todothetransformation.Thetransformationfunctionbehaveslikearecursivelooplikeseeninthelastsection.Here'safunctionthattakesalistofnumbersandgivesyouthetotalsumofthosenumbers.

locallist={23,63,12,48,3}

localsum_fn=function(accumulator,current_number)

returnaccumulator+current_number

end

localtotal_sum=reduce(list,sum_fn,0)

Wepassreduceastartingnumberof 0.Whathappensis sum_fnisinvokedwiththefirstparameter,theaccumulatorbeingthestartingnumber0and current_numberbeingthefirstnumberinthelist.Whatevervaluethefunctionreturnsbecomesthenewvaluefor accumulatornextlooparound.

Luadoesn'thaveareducefunctionbuiltinsowe'llimplementourownherewithadetaileddescriptionofalltheparameters.Trynottogettoohungupontheactualreducefunction'simplementationatthetop,butratherfocusbelowthatonhowitworks.Therewillbeseveralmoreexamples.Onceyouunderstandhowtouseit,gobacktothetopandlookattheactual reducefunction'simplementation.CopyallthiscodeintothetexteditorwindowontheREPLandrunit:

--Appliesfnontwoargumentscumulativetotheitemsofthearrayt,

--fromlefttoright,soastoreducethearraytoasinglevalue.If

--afirstvalueisspecifiedtheaccumulatorisinitializedtothis,

--otherwisethefirstvalueinthearrayisused.

--@param{table}t-atabletoreduce

--@param{function}fn-thereducerforcomparingthetwovalues

--@param{*}acc-Theaccumulatoraccumulatesthecallback'sreturn

--values;Itistheaccumulatedvaluepreviouslyreturnedinthe

--lastinvocationofthecallback,or`first_value`,ifsupplied.

--@param{*}current_value-Thecurrentelementbeingprocessedinthelist.

--@param{number}current_index-Theindexofthecurrentelement

--beingprocessedinthelist,startingat1.

--@param{*}first_value-Theinitialvalueoftheaccumulation.Ifthearrayis

--empty,thefirst_valuewillalsobethereturnedvalue.Ifthearrayisempty

--andnofirstvalueisspecifiedanerrorisraised.

--@example

----returns'zxy'

--reduce(

--{'x','y'},

--function(a,b)returna+bend,

--'z'

--)

localfunctionreduce(t,fn,first)

localacc=first

3.05-Reduce

162

localstarting_value=first~=nil

fori,vinipairs(t)do

--Nostartingvalue,starton

--thefirstelementinthelist

ifstarting_valuethen

acc=fn(acc,v,i,t)

else

acc=v

starting_value=true

end

end

assert(

starting_value,

'Attemptedtoreduceanemptytablewithnofirstvalue.'

)

returnacc

end

locallist={23,63,12,48,3}

localsum_fn=function(accumulator,current_number)

print(accumulator)

returnaccumulator+current_number

end

localtotal_sum=reduce(list,sum_fn,0)

print('Thetotalsumis:',total_sum)

Followingthe printstatementinsideof sum_fn,wecanseethatthe accumulatorstartsoutwiththe0wepassin.Weadd current_numberto accumulatoranditbeginstoaccumulateallthevaluesasitgoes.

0

23

86

98

146

Thetotalsumis:149

Ifwedon'tpassinastartingnumber,theaccumulatorwillbeginrightawaywiththefirstnumberinthelist:

localsum_fn=function(accumulator,current_number)

print(accumulator)

returnaccumulator+current_number

end

localtotal_sum=reduce(list,sum_fn)

23

86

98

146

Thetotalsumis:149

Ifyou'veusedjavascript,youmaybestartingtoseetheuncannyresemblanceitbearstojavascript'sreducefunction.Bothlanguagesareverysimilarsyntactically,andgiventheubiquityofjavascriptthisLuaimplementationfollowsmuchofthesamebehavior.

Let'slookatsomemoreexamplestobetterunderstandhowtoreduceandwhatsituationsdoingsocouldproveuseful.Thereducefunctionisomittedinthefollowingexamples,butyoucancopyandpastethefunctionintheREPLalongsidetheexamplestorunthecodeyourself.

--Concatenatealistofwords

3.05-Reduce

163

locallist={'this','is','a','sentence'}

localsentence=reduce(list,function(acc,word,index,list)

--Addaperiodifthisisthelastword

ifindex==#listthen

word=word..'.'

end

--Otherwiseaddaspacebetweenthewords

returnacc..''..word

end)

print(sentence)

thisisasentence.

--Onlykeepoddnumbers

locallist={23,63,12,48,3}

localodd_numbers=reduce(list,function(acc,current_number)

ifcurrent_number%2==0then

returnacc

end

acc[#acc+1]=current_number

returnacc

end,{})

forkey,valueinipairs(odd_numbers)do

print(value)

end

23

63

3

Thislookssimilartowhatwemightdowiththe filterfunctionpreviouslycoveredin3.3-Mapandfilter.Infact,wecancompose filterand mapfrom reduce.Takealookatthesamecoderefactoredout:

localfilter=function(list,predicate_fn)

returnreduce(list,function(acc,val,i,t)

ifpredicate_fn(val,i,t)then

acc[#acc+1]=val

returnacc

end

returnacc

end,{})

end

--Onlykeepoddnumbers

locallist={23,63,12,48,3}

localodd_numbers=filter(list,function(current_number)

returncurrent_number%2~=0

end)

forkey,valueinipairs(odd_numbers)do

print(value)

end

Anexampleofwrapping reducewithanew mapfunctionwon'tbeexplainedhere,butratherleftuptothereaderasanexerciseattheendofthissection.

3.05-Reduce

164

Here'sonemoreexamplethatisabitmorecomplex,afunctioncalled composethatcreatesapipelineforpassingdatathrough.Itaccomplishesthisbypassinganyfunctionsyougiveitthroughto reduceasalist:

--Functionthatallowsyoutocomposeotherfunctions

--togethertoformapipeline.Theresultingpipeline

--isafunctionthatyoucanpassyourintendeddatathrough.

localcompose=function(...)

--"..."and"arg"arespecialkeywordsinLua.

--See:https://www.lua.org/pil/5.2.html

localfns=arg

returnfunction(x)

returnreduce(fns,function(acc,v)

returnv(acc)

end,x)

end

end

--Someexamplecomposablefunctions

localadd=function(x)

returnfunction(y)

returny+x

end

end

localmultiply=function(x)

returnfunction(y)

returny*x

end

end

localsubtract=function(x)

returnfunction(y)

returny-x

end

end

localnumber_pipeline=compose(add(12),multiply(2),subtract(9))

print(number_pipeline(3))

print(number_pipeline(2))

Alternativereduceimplementations

IteratingtablesLet'sgobacktotheimplementationofreduceforamoment.Takealookattheimplementationofitgivenabove.Noticetheiterationinsideisusing ipairswhichexpectsanarray/list-typetable.Ifwewantedtoreduceanon-listtablewecouldmodify reducetofirstcheckifthetableisanarrayanddoappropriateiterationoverthetablewhetherornotitis.Let'stestthat:

localfunctionreduce(t,fn,first)

localget_iterator=function(t)

iftype(t)=='table'then

--Ifpropertyof1isemptythen

--iterateasaregularkeyedtable

ift[1]==nilthen

returnpairs(t)

end

returnipairs(t)

end

error('Expectedtable,got'..tostring(t))

end

localacc=first

localstarting_value=first~=nil

--Whetherwedoipairsorpairsisconditional

3.05-Reduce

165

fori,vinget_iterator(t)do

--Nostartingvalue,starton

--thefirstelementinthelist

ifstarting_valuethen

acc=fn(acc,v,i,t)

else

acc=v

starting_value=true

end

end

assert(

starting_value,

'Attemptedtoreduceanemptytablewithnofirstvalue.'

)

returnacc

end

locallist={

monday=23,

tuesday=63,

wednesday=12,

thursday=48,

friday=3

}

localtotal_sum=reduce(list,function(acc,current_number,key)

print(key..':'..current_number)

returnacc+current_number

end)

print('totalsum:'..total_sum)

Thisshouldprintsomethinglikethis:

wednesday:12

friday:3

thursday:48

monday:23

totalsum:149

Notethattheorderthekeysareiteratedinarenotguaranteed.Also"tuesday"wasn'tprintedoutbecauseitwasthestartingnumber,butitwasstillincludedinthetotal.Passinganextraargumentof 0to reducewouldhavecausedallthedaystobepassedthroughourreducerfunctionandprintedout.

Breakearly

Ok,here'sanotherexamplethatseemstrickyatfirstglance;Let'ssayyouimplementedsomesearchfunctionalityontopofreducelikethis:

locallist={23,63,12,48,3}

localfind=function(list,predicate_fn)

returnreduce(list,function(acc,v,i,t)

ifpredicate_fn(v,a,t)then

returnv

end

returnacc

end)

end

print(find(list,function(val)

returnval>50

end))

print(find(list,function(val)

3.05-Reduce

166

returnval%8==0

end))

Whichprintsouttheexpectedresults:

63

48

Butdoyouseewhat'sproblematicaboutthis?Ifwefindtheresultswewant,thereducefunctionwillkeeprunningthroughtheentirelistunnecessarily.Typicallywhendoingasearchyouonlywantthefirstitemyoufindanyway,buttheaboveimplementationwillreturnthelastitemfoundifmorethanonematchismade.Doyourememberhowthereducefunctionpassesinthetableasthelastargumenttothereducerfunction?Wecantakecontrolofiteratorviathetableandkilltheiterationprematurely.Thisinvolvedmutatingthetable:

locallist={23,63,12,48,3}

localfind=function(list,predicate_fn)

returnreduce(list,function(acc,v,i,t)

ifpredicate_fn(v,a,t)then

--Ifaresultwasfound,destroythenextiteminthelist

--topreventtheiterationfromgoinganyfurther.

t[i+1]=nil

returnv

end

returnacc

end)

end

print(find(list,function(val)

returnval>1

end))

Thisreturnsthecorrectresult:

23

Butifweloopoverthetableafterwardswecanseewe'vemessedwiththeoriginaldatawhichcanleadtounexpectedconsequencesinarealapplication.Ifyourdataiscomingfromanimmutablesource,meaningsomethingisgeneratinganewcopyeachtimeyouuseitthenthiswouldn'tbeaproblem:

localgenerate_list=function()

return{23,63,12,48,3}

end

reduce(generate_list(),function()

...

...

Howeverwecouldfixallofthisifwearewillingtoaddanotherparametertoourreduceimplementation.

localfunctionreduce(t,fn,first)

localget_iterator=function(t)

iftype(t)=='table'then

--Ifpropertyof1isemptythen

--iterateasaregularkeyedtable

ift[1]==nilthen

returnpairs(t)

end

returnipairs(t)

3.05-Reduce

167

end

error('Expectedtable,got'..tostring(t))

end

localacc=first

localstarting_value=first~=nil

fori,vinget_iterator(t)do

--Exittheloopwhentrue

localshould_break=false

--Nostartingvalue,starton

--thefirstelementinthelist

ifstarting_valuethen

acc,should_break=fn(acc,v,i,t)

ifshould_breakthen

break

end

else

acc=v

starting_value=true

end

end

assert(

starting_value,

'Attemptedtoreduceanemptytablewithnofirstvalue.'

)

returnacc

end

Nowifwepass trueasasecondreturnparameterthenwewillgetthefirstnumberwearelookingforinsteadofthelast.Loopthroughandprintoutthelistafterwardtomakesurewehaven'tmutateditunexpectedly.

locallist={23,63,12,48,3}

localfind=function(list,predicate_fn)

returnreduce(list,function(acc,v,i,t)

ifpredicate_fn(v,a,t)then

returnv,true

end

returnacc

end,false)

end

print(find(list,function(val)

returnval>1

end))

foridx,valinipairs(list)do

print(idx,val)

end

reduce_right

Anotherpossiblechangeyouwouldwanttomakeistoreplacetheiteratorwithacustom-madeonetotransformdatainaspecificorderorpattern.Takenfromlua-users.org'sIterationTutorialisthisreverse-ipairs( ripairs)implementationthatallowsyoutoiterateoveratablefromrighttoleft.Thismodifiedversionof reduceistypicallycalled reduce_right.

localfunctionreduce_right(t,fn,first)

localripairs=function(t)

localmax=1

whilet[max]~=nildo

max=max+1

end

localfunctionripairs_it(t,i)

i=i-1

3.05-Reduce

168

localv=t[i]

ifv~=nilthen

returni,v

else

returnnil

end

end

returnripairs_it,t,max

end

localacc=first

localstarting_value=first~=nil

fori,vinripairs(t)do

--Exittheloopwhentrue

localshould_break=false

--Nostartingvalue,starton

--thefirstelementinthelist

ifstarting_valuethen

acc,should_break=fn(acc,v,i,t)

ifshould_breakthen

break

end

else

acc=v

starting_value=true

end

end

assert(

starting_value,

'Attemptedtoreduceanemptytablewithnofirstvalue.'

)

returnacc

end

Thenswapout reducefor reduce_rightintheplacesyouwanttouseit:

locallist={23,63,12,48,3}

localfind=function(list,predicate_fn)

returnreduce_right(list,function(acc,v,i,t)

ifpredicate_fn(v,a,t)then

returnv,true

end

returnacc

end,false)

end

print(find(list,function(val)

returnval>1

end))

Recursive

Sincewetalkedaboutrecursioninthelastsection,let'stryarecursiveimplementationof reduce.AlthoughwithLuathere'snopracticalreasontochoosearecursiveimplementationoverafor-looporwhile-loopimplementation,doingrecursionisfun.

localfunctionreduce(t,fn,acc,key)

--Checkforstartingvalue

ifkey==nilandacc==nilthen

key=next(t,key)

acc=t[key]

end

--Beginnextiteration.NextisaLuabuilt-infunction

--thatfetchesthenextkeyinatableafterthegivenkey.

3.05-Reduce

169

--See:https://www.lua.org/pil/7.3.html

key=next(t,key)

--Returnaccifwe'veiteratedallkeys

ifkey==nilthen

returnacc

end

localbreak_early=false

--Collectnewaccumulatorfrompredicatefunction

acc,break_early=fn(acc,t[key],key,t)

--Checktoseeifthepredicatewantstoendearly

ifbreak_earlythen

returnacc

end

--Recur

returnreduce(t,fn,acc,key,acc)

end

--Testitbygettingthetotalsumfromatablelikebefore

locallist={

monday=23,

tuesday=63,

wednesday=12,

thursday=48,

friday=3

}

localtotal_sum=reduce(list,function(acc,current_number,key)

print(key..':'..current_number)

returnacc+current_number

end,0)

print('totalsum:'..total_sum)

Thissupportsbreakingearlylikethetwopreviousimplementations.

ExercisesCreatea countfunctionthatcountsupthenumberofitemsinalistthatmatchthepredicateandreturnsthetotal.Itshouldworklikethis:

localcount=function(list,predicate_fn)

????

end

locallist={23,63,12,48,3}

--Printnumberofitemsevenlydivisibleby3(shouldreturn4)

print(count(list,function(v)

returnv%3==0

end))

Gobacktothemapsectionin3.3andseeifyoucanreimplementthe mapfunctionontopof reduce.

3.05-Reduce

170