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