table of contents - github€¦ · [gcc 4.2.1 (llvm, emscripten 1.5)] on linux2 this is just...
TRANSCRIPT
1.1
1.2
1.2.1
1.2.2
1.2.3
1.2.4
1.2.5
1.2.6
1.2.7
1.2.8
1.2.9
1.2.10
1.2.11
1.2.12
1.2.13
1.2.14
1.2.15
1.2.16
1.2.17
1.2.18
1.3
1.3.1
1.3.2
1.3.3
1.3.4
1.3.5
1.3.6
1.3.7
1.3.8
1.3.9
1.3.10
1.3.11
1.3.12
1.3.13
1.3.14
1.3.15
1.3.16
TableofContentsIntroduction
1.0-Programmingbasics
1.1-Interactivecoding
1.2-Strings
1.3-Nilandvariables
1.4-Usingfunctions
1.5-Commentsincode
1.6-Scriptingandprinting
1.7-Makingfunctions
1.8-Booleans
1.9-Flowcontrol
1.10-While
1.11-Typechecking
1.12-Firstgame
1.13-Tables(part1)
1.14-Tables(part2)
1.15-Forloops(part1)
1.16-Forloops(part2)
1.17-Scopes
1.18-Chapterreview
2.0-IntroducingLÖVE
2.1-Upandrunning
2.2-LÖVEstructure
2.3-Geometry
2.4-Gameloop
2.5-Deltatime
2.6-Mapping
2.7-Theworld
2.8-Readingdocumentation
2.9-Modulesandorganization
2.10-Collisioncallbacks
2.11-Breakout(part1)
2.12-Breakout(part2)
2.13-Breakout(part3)
2.14-Breakout(part4)
2.15-Breakout(part5)
2.16-Binaryandbitmasks
1
1.3.181.3.17
1.4
1.4.1
1.4.2
1.4.3
1.4.4
1.4.5
2.17-Networking(part1)
2.18-Networking(part2)
3.0-Programmingin-depth
3.01-Primitivesandreferences
3.02-Higher-orderfunctions
3.03-Mapandfilter
3.04-Stackandrecursion
3.05-Reduce
2
learn2loveCurrentprogress:
Chapter1-Programmingbasics✔Chapter2-IntroducingLÖVE✔Chapter3-Programmingindepth(inprogress)Chapter4-LÖVEindepth(todo)
Viewasawebpage:link
Downloadinebookformat:pdf-epub
Whatisthisbook?ThisbookteachesprogrammingfromthegroundupinthecontextofLuaandLÖVE.Itteachesbasiccomputerscienceandsoftwarebuildingskillsalongtheway,butmoreimportantly,teachesyouhowtoteachyourselfandfindouthowtogoaboutsolvingaproblemorbuildingasolution.Toolscomeandgo,sothegoalistoteachthingsofvaluewithlessfocusontheprogramminglanguageandothertoolsusedtobuildthesoftware.Ihavebeenprogrammingsince2007,focusingonteachingmyselfbestpractices.AlongthewayIhavefoundalotofgoodandbadtutorialsontherightandwrongwaytobuildthingsandIwanttohelpothersavoidgettingstucklikeIdid.
Whoisthisfor?Anyagegroup.Kidstoo,withabitofdemonstration,helpandencouragement!Anybodythatwantstolearnbasiccomputerscience.Thisbookwilltouchonseveralcomputersciencesubjectsinordertobuildprograms.Anybodythatwantstolearntoprogram.Nopriorskillsorknowledgerequired.Anybodythatwantstolearntomakeagame.Makinggamesarefunandrequirelearningmanythingsalongtheway.We'llbuildafewthroughthisbook.AnybodythatwantstolearnLua.Althoughwewon'tdiveintotheadvancedfeaturesofthelanguage,wewillgainalargeunderstandingonhowthelanguageworksinordertoactuallybuildsomethings.Therearealreadyonlineguidesandreferencescoveringsomeofthemoreadvancedtopics.ForexperiencedprogrammerswantingtolearnLua,theProgramminginLuabookmaybesufficient.
Authorandcontributorsjaythomas:OriginalauthorJimmyStevens:Editsandsuggestionsinchapter1&2rm-code:Chapter2gettingstartedValentinChCloud:Chapter3primitivesandreferences
ContributingIssues,comments,andsuggestionscanbemadeusingtheGitHubissuespage.Todownload,build,andrunthebookoranycodeexamplesusethe"Cloneordownload"buttononthemainrepositorypage.
Introduction
3
Fordevelopersandthecurious
Feelfreetosubmitapullrequest.ThedocumentationisbuiltusingNodeJS.Ifyouwishtorunthedocumentationforlocaldevelopmentpurposes,installnodejsthenrunthesecommandsfromwithinthe learn2lovedirectoryyoudownloaded:
npminstall#Downloadsbuildtoolstothea"node_modules"folderinsidethedirectory
npmstart#Createsalocalwebservertowhereyoucanvisitthelinkhttp://localhost:4000
Oncethelocalwebserverisrunning,anyeditsyoumaketothepageswillrebuildthebookandreloadthepageyou'reviewing.
Introduction
4
Chapter1:ProgrammingBasicsThegoalofthischapteristoteachthemostnecessarybuildingblocksofprogramming.Bytheendofthechapteryouwillbebeabletobuildbasicprogramswhichwewillapplywithexercisesinthefollowingchapters.
1.0-Programmingbasics
5
Interactivecoding
What'saREPL?Programmingdoesn'ttakemucheffortbeyondloadingupaREPLandjusttyping.WhatisaREPL?It'saninteractivewindowyoucantypecodeintoanditspitsouttheresultsonscreenwhenyouhitenter.ItstandsforRead-Evaluate-Print-Loop.Thesearethe4thingstheREPLdoes:
1. Readthecodethatwasjusttyped2. Evaluate,orprocessthecodedownintoaresult3. Print,orspitouttheresult4. Loop...doeverythingagainandagainuntiltheprogrammerisdone
It'sactuallysimplerthanitsounds.Let'sgotoawebsitewithaREPLandtryitout:https://repl.it/languages/Lua
Youwillseetwowindowpanesonthewebsite:alightsideontheleftanddarksideontheright.Theright-sideistheREPLandiswhatwe'reinterestedinfornow.Ithasalotofinformationthatisn'tnecessarilyusefultousatthemoment.Somethingsimilartothis:
Lua5.1Copyright(C)1994-2006Lua.org,PUC-Rio
[GCC4.2.1(LLVM,Emscripten1.5)]onlinux2
ThisisjusttellingyouwhatprogramminglanguagethisREPLisloading,inthiscase,Lua.Ifyouclickinsidethewindowpaneandstarttypingyouwillseeyourtextappear.
Let'strytypingsomecodefortheREPLtoRead.Youalreadyknowsomecodeifyouknowarithmetic.Type:
2+2
ThenhitENTERandimmediatelytheREPLwillPrintout:
=>4
Alothappenedveryquickly.AfterhittingENTER,theREPL,Readtheline 2+2,itEvaluatedthevalueofthatstatementtobe 4,itPrinted4onthescreenforyou,thenLoopedbacktoanewlinetoawaityournextcommand.Tryoutsomemorearithmetic.Multiplication:
2*3
Subtraction:
2+2-4
Division:
6/2
Youcanuseparenthesistotellitwhichordertodotheoperations:
(2+2)*(3+1)
1.1-Interactivecoding
6
Whichgivesdifferentresultsthan:
2+2*3+1
IfyougivetheREPLasinglenumber:
12
Itwillgiveyouback 12,becausethiscan'tbesimplifieddownanyfurther.
Youcanalsodoexponentsusingthe ̂ (caret)symbol:
2^4
Numbersareatypeofdata,and +, -, /, *, ̂ , %areoperators.Statementssuchas 2-2and 23*19arealloperations.
Onelastarithmeticoperationwe'llcoverismodulo,whichisdonewiththemodulusoperator.Themodulusoperatorisrepresentedinmostlanguagesasa %(percent)symbol:
8%3
Modulusoperationsaren'tseeningradeschoolclassroomsasoftenastherest,butarequitecommoninsoftwareandcomputersciences.Thewayitworksisyoutakethe2ndnumberandsubtractitfromthebiggernumberasmanytimesaspossibleuntilthe2ndnumberisbiggerthanthe1st.Theresultiswhat'sleftofthe1stnumber.With 8%3,ifyoukeepsubtracting 3from 8thenyouendupwith 2left.
Arealworldexampleistimeelapsingonananalogclock.Imaginethefaceofaclockwiththehourhandonnoon.If25hourspassthenthehourhandgoesallthewayaroundtwiceandendson1.Thatwouldbeequivalenttowriting:
25%12
=>1
Thehourhandresetseverytimeitpasses12,so 13%12, 25%12,and 37%12wouldallequal 1.Likewise, 10%4resultsin 2because4goesinto10twice,andleavesaremainderof2.
ExercisesTrytypingdifferentmodulooperationsinandguessingwhattheanswerwillbe.Tryusingnegativenumbers( -3+-2).Tryusingasetofparenthesisinsideanothersetofparenthesis.Doesitbehaveasyouexpect?Afterrunningthroughalltheexercisespressthe'up'keyintheREPL.Whathappensandhowcanthisspeedupyourwork?
1.1-Interactivecoding
7
StringsNumbersareonetypeofdatathatcanbeoperatedon.Let'sexploreanotherdatatypewithintheREPL.TakeasetofquotesandputsometextinitandhitENTER:
"hello"
TheREPLwillprint hellobacktoyou.Thisisastring.Astringisasetofcharacters(lettersandsymbols)stringedtogetherasonesinglepieceofdata.Thisstringismadeof9characters:
"H-E-L-L-O"
Likenumbers,thereareoperatorstomakestringsplaywitheachother.Theconcatenateoperator( ..)combinesstringstogether:
"hello".."world"
What'stheresult?Noticethattheresultingstringhasnospacebetweenthetwowords.Ifyouwantedaspace,youwouldhavetoputoneinthequotestobepartoftheoperation:
"hello".."world"
Youcouldevenmakeaseparatestringwiththespaceinit:
"hello".."".."world"
Stringscanhaveanycharactersinthemthatyouwant.
"abc".."123"
"Япо́нский".."ロシア語!!"
ExercisesTryusinganarithmeticoperatoronstrings "hello"/"world".Whathappens?Tryusingtheconcatenateoperator( ..)onnumbers( 1..1).
1.2-Strings
8
Nilandvariables
Data,orthelackthereofHumanshavedifferentwaysofrepresentingalackofdata.Iftherearenosheeptocountthenwehavezerosheep.Iftherearenowordsonapagethenthepageisblank.Inacomputerwemayrepresentthenumberofsheepas 0orthemissingwordsonapageasanempty "".Thesearestilldatathough...anumberandastring.Insoftwarewhenyouwanttorepresentalackofdatawehave:
nil
Sometimescalled nullor undefineddatainotherlanguages.It'sseeminglyuseless.Youcan'tuseoperatorsonnil.
nil+nil
Thiswillprintanerrorlikeitdidwhenyoutrieddoingarithmeticonstrings.Let'stakealookatvariablesandwe'lldiscoverthepurposeof nil.
VariablesSometimesyouwanttowriteoutdata,butyouwantthatdatatobeeasytochange.Variablesletyougivedataanametoreference.Here'sanexampletotry:
name="Mandy"
"hellomynameis"..name
Sinceyoutolditwhat nameis,itknowswhatvaluetoaddtothestring "hellomynameis".Ifyoutype:
name
...andhitENTER,itwillprintoutthevaluethatbelongstothisvariabletoremindyou.The =(equal)signtellsLuathatyouwanttoassignavaluetothegivenname/variable.Youcanchangethevalueofavariableandgetdifferentresults:
name="Jeff"
"hellomynameis"..name
Assignmentisn'tthesameasitisinAlgebra.Youcanchangethevalueofavariablemultipletimes.Wecantell namethatitequalsitselfwithsomeadditionalinformationconcatenatedtoit:
name="abc"
name=name.."def"
name
Youcanassignanytypeofdatatoavariable,includingnumbers:
name="Jeff"
1.3-Nilandvariables
9
age=16
"hellomynameis"..name.."andIam"..age.."."
Youcanchangenumbersafterassignmenttoo:
age=16
age=age*2
"myagedoubledis"..age
So,whatifyoutypeinamadeupvariablename?
noname
Youwillseeithas nil,ornodatayet.Ifyoutrytouse nilinyourstringoperationyouwillgetanerror:
"hellomynameis"..nil
[string"return"hellomynameis"..nil"]:1:attempttoconcatenateanilvalue
"hellomynameis"..noname
[string"return"hellomynameis"..noname"]:1:attempttoconcatenateglobal'noname'(anilvalue)
Tryassigningavaluetoavariablename:
best_color="purple"
thenassigningthatvariabledatatoanother:
worst_color=best_color
worst_color
You'llseethatbothvariablesnowhavethevalue "purple".
Variablescanhavenamesmadeupofletters,numbersandunderscores( _).Variablenamescannotbeginwithanumberthough,otherwiseitwillthinkyou'retryingtotypeinnumberdata.Here'ssomeexamplesofvalidvariables:
my_dog="Poe"
myDog="Zia"
DOG3="Ember"
ExercisesTryoutdifferentvariablenames.Tryafewinvalidvariablesnamestoojusttoseewhattheerrormessagelookslike.It'simportanttoseeerrormessagesandunderstandthem.Theyhelpyouunderstandhowaprogrambreakssoyoucanfixit.
1.3-Nilandvariables
10
UsingfunctionsMostprogramminglanguagescomewithsomevariablesalreadydefinedforus.Luahasmany,solet'stypeoneinandhitENTERtoseewhatthevalueis:
string.reverse
=>function:0x2381b60
Ohmy.So"function"isanotherdatatype,butwhatis 0x2381b60?It'sjusttellingyouwhereinthecomputer'smemorythatfunctionexists,justincaseyouwantedtoknow.Functionsworkverydifferentlythannumbersinstrings.Essentiallyfunctionsarepre-definedinstructionsthattelltheprogramhowtododifferentthings.Theytakedataandreturnbackdifferentdata.Let'sseehowtogivethisfunctiondata:
string.reverse("hello")
=>olleh
Attheendofthefunction'svariablename, string.reverse,wetypeasetofparenthesis, string.reverse(),andputinsidetheparenthesissomedatawewantchanged( string.reverse("hello")).Makingthefunctionrunisoftencalledinvokingthefunction.Havingafunctionthatreversestextinastringforuscanbeuseful,andwecancapturethereturnvalue(theresults)ofthefunctionusingavariable.Tryitout:
greeting="hello,howareyou?"
backwards_greeting=string.reverse(greeting)
backwards_greeting
=>?uoyerawoh,olleh
Itshouldbeobviousfromthenamewhatthatfunction'spurposeis.Howaboutthisone?
string.upper("hello,howareyou?")
Nowtrycapturingthatvaluebyassigningittoavariable:
greeting="hello,howareyou?"
shouting_greeting=string.upper(greeting)
crazy_greeting=string.reverse(shouting_greeting)
Wecangetcrazier.Howaboutinvokingafunctionwheninvokinganotherfunction??
string.reverse(string.upper("hey"))
What'shappeninghereisthestringisbeinguppercasedby string.upperbutthenthevaluefrom string.upperisbeingreversedby string.reverseassoonasitisdone.It'sjustlikeinarithmeticwhenyouhavenestedparenthesis.Theinner-mostparenthesisareresolvedbeforedoingtheouter-mostparenthesis.
Let'stryonemorefunction.Thisfunctionhastwoparameters,meaningitacceptstwopiecesofdatawhichitrequirestoworkproperly.
1.4-Usingfunctions
11
Let'stryonemorefunction.Thisfunctionhastwoparameters,meaningitacceptstwopiecesofdatawhichitrequirestoworkproperly.
math.max(7,10)
Whengivingmorethanonepieceofdatatoafunction,youneedtoputacomma( ,)betweentheparameters
Thesearegreatfunctions,butwouldn'titbegreatifwecouldmakeourown?We'llgiveitashotinjustafewpages.
ExercisesSeeifyoucanfigureoutwhat math.maxdoes.Giveitdifferentnumbersandexaminetheresult.Thereisanotherfunctioncalled math.minthatalsotakestwonumbers.Whatdoesitreturn?
1.4-Usingfunctions
12
CommentsincodeSometimeswemightwanttowriteacommentinourcode–anexplanationtoafriendorourfutureselvesonwhatthepurposeofsomecodeis.Perhapswewanttowriteanotetoourselvestochangesomethinglater.Commentsworkverysimilarlyindifferentlanguagessothey'reprettyeasytoreadevenifyoudon'tunderstandtheprogramminglanguageorthecodeitself.Luadenotesacommentas --andanytextthatfollowsit:
1+1
--Thisisacodecomment
1+2
--Thisisanotherlineofcomments
3+4
Thesecommentswillbecompletelyignoredbythecomputerandaremeantforthehumantoread.Commentscanalsobeonthesamelineascode.Thecomputerwilljustignoretherestofthelinewhenitseesacommentstarting.
1+1--Thisismycomment.Thiscodeaddssomenumberstogetherincaseyoudidn'tknow!
Youwillseecommentsappearinfutureexamplecode,sodon'tletitsurpriseyou!
1.5-Commentsincode
13
ScriptingandprintingLookingbackatthewebsite,(youbookmarkedit,right?)wehavebeenusingtheREPLwindowpaneontheright,buthaven'ttalkedaboutthepaneontheleft.Thiswindowisjustatexteditor.Insteadofrunningtheprogramwitheachlineyoutype,itallowsyoutowritemultiplelinesofcodebeforeexecutingitall.Let'strytypingsomethinginit.Onceyouaredonetypingallthecodeyoucanclickthe"Run"button.
number=4
number=number+1
Butwhenyouclickrun,nothinghappens.Okwellnowthatyouarewritingaprogram,youneedtoreturndata.Solet'sprovideanotherstatementtoourprogram.
number=4
number=number+1
returnnumber
NowwhenyouclickRun,thetext =>5appearsintheconsole.Whenyoutoldittorun,itreadandevaluatedeachlineofthecode,thenwhenitgottothelinewith returnonit,theprogramstoppedandreturnedthevalueyouaskedittoreturn(inthiscase 5).Ifyouwriteanycodeafterthereturnstatement,itwillcauseanerrorwhenwetrytoruntheprogram,sothereturnstatementshouldbethelastthinginourfile.
number=4
number=number+1
returnnumber
--Thislinewillcauseanerror:
number=10
Youcanreturnanytypeofdata,notjustnumbers:
return"hello"
Rememberthoseotherfunctionsweusedbefore?Youcanwritethoseaspartofthereturnstatement.
returnstring.reverse("hello")
Sometimeswhenwritingprograms,wewanttopokearoundandseevalueswhiletheprogramisrunningandnotwaituntilitisdone.Thisissocommonthatthereisafunctionthatprovidesthisforus.
print("hello")
return"world"
The printfunctiontakesanydataandprintsthevalueinthewindowpaneontheright.
>
"hello"
=>"world"
So "hello"isbeingprintedand =>"world"isbeingreturned.Youcaninvokefunctionsonthesamelinewhenprintingifyoureallywanttogetcrazy:
1.6-Scriptingandprinting
14
print(string.reverse("hello"))
return"world"
The printfunctionand returnstatementwillbothcomeinhandywhentestingthefunctionswe'regoingtowrite.
ExercisesWhenwepassdataintoafunction,itiscalledanargument.Wepassed1argumentinto printbutitcanpassintwo,orthree,ormore.Whatdoesitlooklikewhenyouprintmultiplearguments?Funtip,whenusingatexteditoralong-sidetheREPLyoucanrunthecodewithoutthemousebypressing'command+enter'onMacand'Alt+enter'onWindows.Doesthisspeedupyourlearning?
1.6-Scriptingandprinting
15
MakingfunctionsFunctionsarethethirddatatypewe'veseen.We'veaccessedsomevariableswherefunctionsweredefinedforusandhadablastusingthem(IknowIdid).Functionsarethebuildingblocksofsoftware.YoucancomposethemthensnapthemtogetherlikeDanishplasticblocks.Ittakestimetounderstandhowtheyworkandmuchlongertomastertheirinnerpower.Sowithoutfurtherado,let'sseewhattheyactuallylooklike:
function()
return4+4
end
Typeitoutinthetexteditorwindowandletusbreakthisdownlinebylineandwordforword.Wheneverwetypefunction()wearebeginninganewfunction.The2ndlineisthebodyofourfunctionwherethingshappen.Thebodyofthefunctioncanbemanylineslong.Thebodyofthefunctioncouldalsobeempty(butthat'snotveryuseful).Onthelastlineofthefunctionbodywereturndata.Thisreturnstatementwon'tendourentireprogramthough!Itwillonlytellthecomputerwe'refinishingupwithourfunction.Thenonthethirdline,we'retellingthecomputerwe'redonewritingourfunction.Inordertousethisexamplefunction,weshouldprobablyuseavariabletogiveitaname:
add=function()
return4+4
end
Thefirstbitshouldbeunderstandable.Wedeclaredavariablecalled add,thenweassignedsomedatatoitontherightoftheequalsign.Inthiscase,ourfunction.Nowitisreadytouse.
add=function()
return4+4
end
result=add()
returnresult
=>8
We'vemadeourveryownfunctionwithourveryownnameforitandeveninvokeditandgotbackdata!Ifyouinsteadgotanerrormessage,doublecheckwhatyoutypedthatnothingismissing.Errormessagesgiveyoualinenumberofwheretofindtheerrorthatcrashedtheprogram.
Takealookforaminuteathowweinvokedourfunction:
add()
Wetypedoutthevariablenamethatourfunctionisassignedto,followedbysomeparenthesis.Inthoseparenthesisisthedatathatwepassedintoourfunction...waitaminutetheparenthesisareempty.Wedidn'tpassanydataintoourfunction.Wheneverwecalledthoseotherfunctionswepassedindata,likewhenwepassed "hello"intostring.reverse("hello").Whatifwemodifyourlinewhereweinvokeourfunctionandgiveitsomedata?
add=function()
return4+4
end
result=add(16)
1.7-Makingfunctions
16
returnresult
Itseemsitalwaysreturns =>8nomatterwhatargumentswetrytopassin.Weneedtorewindtothefirstlineofourfunctionandtakeacloselookatthisbit:
add=function()
The ()attheendof function()iswherewetellourprogramhowmanyargumentsweareaccepting.Iftheparenthesisareempty,thenourfunctionisignoringallargumentsandwilllikelyalwaysreturnthesameresult.Let'stweakthefunctionslightlyandgiveitoneparameterwiththename a.Let'salsotweakthesecondlinewhilewe'reatit:
add=function(a)
returna+4
end
result=add(16)
returnresult
=>20
Nowwhenwepassindifferentnumbers,wegetdifferentresults:
add=function(a)
returna+4
end
print(add(16))
print(add(12))
Tocompletethisfunction,let'sgiveitasecondparameterof bandmodifythereturnstatementinthefunctionbody:
add=function(a,b)
returna+b
end
print(add(16))
print(add(12))
Ifwetryandrunthecodenow,we'llgetanothererror:
[string"add=function(a,b)..."]:2:attempttoperformarithmeticonlocal'b'(anilvalue)
Let'sreadthiserrorcarefully.Itissayinginsidethesquarebracketsthatanerroroccurredwhenusingthefunctionwedefined( add=function(a,b)...).Totherightofthesquarebracketsitissayingline2( :2)ofourtextistheparticularlocationofthecrash.Totherightofthelinenumberiswhathappenedthatmadeitcrash.Ittriedtoperformadditionwith a+bbutthevalueof bwasnil.Westatedthatourfunctionrequirestwoparametersnow, aandb,andourprogramwillcrashifwetryandinvokethefunctionwithonlyoneparameter.Let'smodifythelineswhereweinvokethefunctiontogiveittwoargumentseachtimeweinvokeit:
add=function(a,b)
returna+b
end
print(add(16,10))
1.7-Makingfunctions
17
print(add(12,2))
Great,everythingisworkingagain!Withtheexperienceofourfirst,fully-functionalfunction,wecannowstarttreadingthewatersofthisgreatworld.
ExercisesTogetusedtowritingfunctions,trywritingsomecomplimentaryfunctionsnamed subtract, multiply, divide,or modulate(modulus).Makea concatenatefunctionthataccepts2stringsandreturns1combinedstring.Trymakingafunctionthattakes3ormoreparameters.
1.7-Makingfunctions
18
BooleansDatatypesarelikeelementsontheperiodictable.Themoreelementsyouhavethemorechemicalscancreate.Luckilytherearen'tasmanydatatypesasthereareelements.Infactwe'velearnedalmostallofthem.Thereareonlytwopossiblebooleans:
true
and
false
That'sright.Andyoucanassignthemtovariablesjustlikenumbers,strings,nil,andfunctions:
myboolean=true
print(myboolean)
Thecoolthingwithnumbersandstringsisyoucanusethemtocreatestatementsthatcanbeevaluatedas trueorfalse.Letmegiveanexamplebyintroducingsomenewoperators.TrytheseoutintheREPL:
5>3
=>true
5<3
=>false
"5isgreaterthan3"isatruestatementsoitreturnsa trueboolean.Naturally,"5islessthan3"isafalsestatementandreturns false.Wecanchecktoseeiftwonumbersareequalinvalue:
number=5
number==5
=>true
Byusingadoubleequal( ==)wecancomparetheequalityoftwonumbers.Thisalsoworksforstrings:
"hello"=="hello"
=>true
"hello"=="HELLO"
=>false
1.8-Booleans
19
Forstrings,oftentimeyouwillseesinglequotes ''(apostrophe)usedinsteadofregularquotes(sometimescalleddoublequotes)wrapperaroundthetext.Luadoesn'tcareaslongasthetextinsidebothstringsareidentical.Wecanprovethatwithanequalitycheck:
'hello'=="hello"
=>true
Anyways,youcanalsodotheinverseofanequalitycheckandcheckforinequality(iftwothingsarenotequal):
5~=3
=>true
"HELLO"~=string.upper("hello")
=>false
Nowlet'sdiginalittledeeperwithtwomoreoperators.Firstisthe andoperator:
3<4and4<5
=>true
ThisreadsoutalmostasplainEnglish.3islessthan4and4islessthan5.Thisisalogicallysoundstatementsoitevaluatestotrue.Justtobeclearonwhat'sactuallygoingonherethough,let'sbreakitdown.Whatwesaidisbeinggroupedinto3separateoperations:
(3<4)and(4<5)
Thetwosetsofparenthesisareevaluatedfirstandinternallythecomputerbreaksthesetwooperationsdownto:
(true)and(true)
Trueandtruearebothtrue.Thissoundssilly,butitisindeedlogicallysound.Let'stryonemorejusttogetthehangofit:
"hello"=="hello"and6>10
Finally,let'stryonemoreoperatortoputabowonthings.Sometimeswedon'tcarethatbothoperationsarecorrect.Weonlycareifone ortheotheriscorrect.
4==10or4~=10
=>true
1.8-Booleans
20
1>100or12==12or"hello"=="bananas"
=>true
Aslongasoneoftheoperationsiscorrect,theentirestatementislogicallytrue.Withtheintroductionof trueandfalsewe'vebroughtinalotofnewoperators:"greaterthan"( >),"lessthan"( <),"equal"( ==),"notequal"( ~=),"and"( and),and"or"( or).
TriviaBooleansgettheirnamefromGeorgeBoolewhoinventedbooleanalgebra,whichwe'vejustseenalittlebitof.
ExercisesTrywritingdifferentstatementswithallthenewoperators.Tryusingtwo andoperatorsinthesamestatementandseeifyoucanmakeitevaluateto true.Tryoutthesetwobonusoperatorswithsomenumbers:"greaterthanorequalto"( >=),and"lessthanorequalto"( <=).
1.8-Booleans
21
FlowcontrolTypicallythecomputerstartsatthetopofourscriptandreadseachlinedowninasequence.WemaketheprogramsjumparoundwithfunctionsinthemixTrythisoutinthetexteditor:
print("I'mcalled1st")
add=function(a,b)
print("I'mcalled5th")
returna+b
end
subtract=function(a,b)
print("I'mcalled3rd")
returna-b
end
print("I'mcalled2nd")
subtract(16,10)
print("I'mcalled4th")
add(12,2)
Wehaveafunctionthatissavedtothevariable addbutitisn'tinvokeduntilfurtherdowninthecode.Soinasenseourprogramhasworkeditswaydownthepagethenjumpedbackuptothefunctionandworkeditswaythroughthebodyofthefunctionthenpickedbackupwhereitwasbefore.Inasimilarfashion,wecanmakeourprogramtakeonepathoranotherdependingifthedatais trueor false.
noise=function(animal)
if(animal=="dog")thenreturn"woof"end
return""
end
print(noise("dog"))
print(noise("rabbit"))
Let'sanalyzethisfunctionlinebyline.Thefunctioniscallednoiseandtakesananimalname(string)asaparameter.Onthenextlineitsaysif"animalisdog"istruethenreturnsomethingspecial.Weputan endattheendofourstatementtomakeitobvioustothecomputer.Ifthestatementwasfalse,then "woof"doesnotgetreturned.Insteadanemptystring( "")getsreturned.Whenweinvokethefunctionwiththeargument"dog"thenwegetback"woof!".With"rabbit"wegetbacksilence.Maybetherabbitdoesn'twantthedogtohearwheresheis.Let'smakeourfunctionmoreversatilebyaddingmoreanimals:
noise=function(animal)
ifanimal=="dog"oranimal=="wolf"thenreturn"woof"end
ifanimal=="cat"thenreturn"meow"end
return""
end
print(noise("dog"))
print(noise("cat"))
print(noise("rabbit"))
print(noise("wolf"))
1.9-Flowcontrol
22
Wehavebranchingpathshappeningwithinourfunction.Ifweweretomapoutthesebranchesitmaylooksomethinglike:
|
+-->"woof"
+-->"meow"
|
+-->""
There'snorequirementthatastatementhastobeallwrittenoutononeline.Sometimeswhendoingmultiplethingsinsideanifstatementwemaywanttoputitonmultiplelines:
ifmy_age>17then
print("You'reanadult!")
print("Getajob!")
end
Similartofunctionshavingbodies,everythingbetween thenand endisconsideredthebodyoftheifstatement.Sometimesitisnecessaryforourbranchestohaveforkswithinthem.Let'ssayourfunctiontakesalanguageasasecond,optionalparameter:
noise=function(animal,language)
ifanimal=="dog"oranimal=="wolf"thenreturn"woof"end
ifanimal=="cat"thenreturn"meow"end
ifanimal=="bird"then
iflanguage=="spanish"thenreturn"pío"end
return"tweet"
end
return""
end
print(noise("dog"))
print(noise("rabbit"))
print(noise("bird"))
print(noise("bird","spanish"))
Theifstatementforcheckingiftheanimalisabirdis4lineslong.Oncewefindoutthattheanimalisabird,whilestillinthebodyoftheifstatement,westoptocheckandseeifthelanguageissettoSpanish.Ifitis,weendupinsideanifstatementwithinanifstatement!Otherwisewe'llreturn "tweet"ifthelanguageisn'tSpanish.Maybemappingoutthepathswillclearthingsup:
|
+-->"woof"
+-->"meow"
+----->"pío"
||
|+-->"tweet"
|
+-->""
Ourcodecangetunreadableveryquicklyifwestartnestingifstatementsinsideeachother.Fortunatelydoingsoisn'tusuallynecessary.
Let'stalkaboutanotheraspectofifstatements.SupposeIhavetwobranchesofcodethatareoppositeofeachother:
ifdaytime==truethen
thermostat=71
end
ifdaytime==falsethen
1.9-Flowcontrol
23
thermostat=68
end
Ratherthanwritingthisoutastwoifstatementsandcheckingthevalueofdaytimetwice,Icantakeadvantageofthekeyword else:
ifdaytime==truethen
thermostat=71
else
thermostat=68
end
Thatwayif daytimeisnot true,itwilldefaulttothesecondbranch.Youcouldreadthisoffalmostlikeasentence:"Ifdaytimeistruethensetthethermostatto71,otherwisesetthethermostatto68."Nothavingtocheckthingstwicewhendoingcomputationssavesustimeandmakesourprogramrunmoreefficiently.Since daytimeisabooleaninthiscase,wedon'tneedtocheckifitistrueorfalse.Wecanjustpassittotheifstatementtobecheckedfortrue/ falseandmakeouroperationevensimpler.
ifdaytimethen
thermostat=71
else
thermostat=68
end
Better."Ifdaytimethensetthermostatto71,otherwisesetthermostatto68."There'sonemorefeatureofifstatementsweshoulddiscuss.Ifthereisanotherconditionyouneedtocheck,maybeseveralmore,youcanusetheelseifkeyword.Itlookssomethinglikethis:
color="green"
ifcolor=="blue"then
print("That'smyfavoritecolor!")
elseifcolor=="green"then
print("Verysubtlechoice.Ilikeit.")
elseifcolor=="pink"then
print("Nice,boldchoice.")
else
print("Idon'tthinkthatcolorwouldmatchyourshoes.")
end
Tryitout!
Thebeginningoftheifstatement... ifcolor=="blue"then...isfalse.Thiscodegetsskippedover.Thenthenextpartoftheifstatement... elseifcolor=="green"then...istruesothatsectionofcodeunderneathit... print("Verysubtlechoice.Ilikeit.")isran.Therestoftheifstatementisskippedwithoutcheckingifitstrueornot.So elseifcolor=="pink"then/ elseareneverprocessed.
ExercisesWriteoutafunctionthattakes1parameternamed"sides".Makethefunctionreturnthenameoftheshapedependingonthenumberofsides(forinstance,"triangle").Trytomaketheifstatementincludean elseattheendtoaccountforeverythingelsethattheifdoesn't.
1.9-Flowcontrol
24
WhileAnotherwaytocheckconditionsiswiththe whilekeyword.
while1+1==2do
print("Mymathiscorrect!")
end
Whileaconditionistrue,thebody(everythingbetweenthe doand end)willberunrepeatedlyandnotstop.Soifyoutriedtorunthatbitofcode,yourscreenprobablywentcrazyprintingoverandoverinanever-endingloop.Weneedtomakesuretheconditioncangetchangedsowe'renotstuckinanever-endingloop.Let'swritealoopwecanescapeoutof.
boolean=true
--Thisconditionwillgetcheckedtwice.Thefirsttimeit
--ischeckeditwillbetrueandthebodyofthewhile-loop
--willberun.Thesecondtimetheconditionischecked,
--ourbooleanwillbefalseandthewhile-loopwon'tberunagain!
whilebooleando
print("Switchingbooleantofalse.")
boolean=false
print("Booleanhasbeensettofalse.")
end
print("Wemadeitoutoftheloop!"
Understandingthatwecanchangethewhileconditionfrominsidethebodyoftheloop,wehavethepowertowriteprogramsthatendexactlywhenwewantthemto.Canyouguesswhatthiswilldowhenwerunit?
countdown=10
whilecountdown>1do
print(countdown.."...")
--Thislineiscriticaltomakeournumbershrink.
countdown=countdown-1
end
print("Blastoff!")
...Andremembertousea >andnota <,oryourloopmayneverrun.
ExerciseComeupwithyourownideaforawhileloop.
1.10-While
25
TypecheckingLuadoesn'tcarewhattypeofdataavariablehas.
data=12
data="hello"
data=true
Tothisend,wecanusethe typefunctiontocheckwhatkindofdataavariableisholding.
type(data)
=>boolean
Wecancheckthetypeoffunction:
type(string.reverse)
type(type)
Wecanalsouseittocheckwhattypeofdataafunctionisreturningbacktous:
type(string.reverse("hello"))
=>string
type(type(12))
=>string
ConvertingdatatypesWe'vealreadyseendatatypeconversionpreviouslywhenwetooknumbersinandoperationandtransformedthatintoatrueorfalsestatement.
type(12>3)
=>boolean
Therearealsowaystoconvertbetweennumbersandstringsusing tonumberand tostring.
number=tonumber("24")
print(type(number))
string=tostring(number)
print(type(string))
number
1.11-Typechecking
26
string
Interestingbutmaybelessuseful,youcanconvertotherdatatypestostring:
print(tostring("alreadyastring"))
print(tostring(true))
print(tostring(nil))
print(tostring(tostring))
ExercisesWhichofthesestringscanbeconvertedtoanumbersuccessfully? "001", "7.12000", "5", "1,943"
1.11-Typechecking
27
FirstgameLet'slearnaboutafewnewfunctionsandthenwe'llbeabletowriteourfirstgame!
ReadinginputNotonlycanourprogramprintoutdata,butusingthefunction io.readitcantakedatatoo.Thisfunctiondoesn'tneedanyargumentsbecauseitwillpromptusinthewindowontherightforustotypeindata.
print("Enteryourname:")
name=io.read()
print("Yournameis"..name..".")
Afteryouclick"Run",theprogramwillpausewhenitruns io.read().TypeyournameandhitENTERandlook,theprogramprintsbackoutthenameyougaveit.Noticethelastprintstatement.Wecombinedthenamewithtwootherstringstoformasentence.Youcanprompttheusermultipletimesifyouneedtogetadditionalinformation:
print("Enteryourname:")
name=io.read()
print("What'syourfavoritefood?")
food=io.read()
print("Yournameis"..name.."andyourfavoritefoodis"..food..".")
Onelimitationwithdoingthisisthedatawillalwayscomeinasastring:
print("What'syourfavoritenumber?")
data=io.read()
print(type(data))
string
Inthelastsectionwetalkedaboutconvertingdatabetweendifferenttypes.Ifwewantedtofindoutwhetheryourfavoritenumberisoddoreven,wewouldneedtoconvertittoanactualnumbertoperformoperationsonit.Typethisinyourtexteditorandrunit:
print("What'syourfavoritenumber?")
data=io.read()
number=tonumber(data)
--Iftheusergaveusananswerthatisn'ta
--number,thenthevalueof"number"isnil.
ifnumber==nilthenreturn"Invalidnumber."end
ifnumber%2==0thenreturn"Yournumberiseven."end
return"Yournumberisodd."
Randomnumber
1.12-Firstgame
28
Manylanguagesgiveusaccesstoarandomnumbergenerator.Randomnessishowwegeneratesecurepasswordsandkeysintherealworld.TogeneratearandomnumberinLua,weuse math.random:
math.random(100)
=>63
Thisgeneratesarandomnumberbetween1and100.Except,ifyouruntheprogramrepeatedlyyoumaynoticethatitspitsoutthesamenumber.That'sbecausenothinginthecomputerworldisrandom.Ifwefedinrandomnoisesthroughaspeakerorwhitenoisefromanoldtelevisionsetthenourcomputercouldusethistogeneraterandomnumbers.Sincewedon'teasilyhaveaccesstothosethings,weneedtoseedLuawithsomeperceivedrandomness.
Ifwerun os.timewewillgetthecomputer'scurrenttimeinintegerform:
os.time()
=>1.529098167e+09
Thisnumberishardenoughtoguessthatitwillworkasaseedforourprogram.Let'stakethesystemtimeandfeeditinusing math.randomseedthenfromthere,Luawillbeabletogeneratea"random"numberintherangewewant(1-100).
seed_number=os.time()
math.randomseed(seed_number)
returnmath.random(100)
=>19
Success!Itisgenerateddifferentnumberseachtimewerunit,withnopattern.
PuttingitalltogetherIshouldprobablyexplainwhatthisgameis.It'squitesimple.Wewantthecomputertomakeupanumberandtheuserhastoguesswhatthenumberis.Ifthey'rewrong,thenweshouldgivethemahintandmakethemguessagain.Wecantakeadvantageofthewhilelooptomakethemcontinueguessingwhiletheirguessisincorrect.
--Thecomputer'ssecretnumber
math.randomseed(os.time())
number=math.random(100)
print("Guessmysecretnumber.Itisbetween1and100.")
guess=tonumber(io.read())
--Whiletheuser'sguessisnotequalto
--thenumber,repeatthebodyoftheloop.
whileguess~=numberdo
--Givethemsomehints
ifguess>numberthen
print("Yourguessistoohigh.")
end
ifguess<numberthen
print("Yourguessistoolow.")
end
1.12-Firstgame
29
--Makethemguessagainandagainuntiltheygetit
print("Guessagain:")
guess=tonumber(io.read())
end
--Winningmessage
print("Youguessedcorrectly!Thenumberwas"..number..".")
Let'sre-factoronebitofthiscodetomakeiteasiertoread.Whenwetalkedaboutifstatements,rememberthekeyword else?
--Thecomputer'ssecretnumber
math.randomseed(os.time())
number=math.random(100)
print("Guessmysecretnumber.Itisbetween1and100.")
guess=tonumber(io.read())
--Whiletheuser'sguessisnotequalto
--thenumber,repeatthebodyoftheloop.
whileguess~=numberdo
--Givethemsomehints
ifguess>numberthen
print("Yourguessistoohigh.")
else
print("Yourguessistoolow.")
end
--Makethemguessagainandagainuntiltheygetit
print("Guessagain:")
guess=tonumber(io.read())
end
--Winningmessage
print("Youguessedcorrectly!Thenumberwas"..number..".")
Nowthatthingsarecleaner,let'saddonefeaturetoourprogram.Itwouldbemorefunifthegamekepttrackofhowmanyguesseswemadesowecouldgivethemaspecialmessage.Let'screateavariablecalled guess_countthatwillstartat 1andincrementeverytimetheusermakesanotherguess.We'llalsogoaheadandaddsomemessagestothebottomtopraisetheuseriftheydiditinareasonablenumberofguesses.
--Thecomputer'ssecretnumber
math.randomseed(os.time())
number=math.random(100)
--Ourstartingnumberofguesses
guess_counter=1
print("Guessmysecretnumber.Itisbetween1and100.")
guess=tonumber(io.read())
--Whiletheuser'sguessisnotequalto
--thenumber,repeatthebodyoftheloop.
whileguess~=numberdo
--Incrementtheguesscounterby1
guess_counter=guess_counter+1
--Givethemsomehints
ifguess>numberthen
print("Yourguessistoohigh.")
else
print("Yourguessistoolow.")
1.12-Firstgame
30
end
--Makethemguessagainandagainuntiltheygetit
print("Guessagain:")
guess=tonumber(io.read())
end
--Winningmessages
print("Youguessedcorrectly!Thenumberwas"..number..".")
ifguess_counter<=5then
print("Amazing!Itonlytookyou"..guess_counter.."tries.")
else
print("Ittookyou"..guess_counter.."tries.Notbad.")
end
ExercisesTryaddingmoremessagestothe guess_counterfordifferentscores.Tryaddingamessagetotheifstatementwiththehintsforwhentheuserguessesaninvalidnumber.Howwouldyoudothat?Maketheconditionexitif guess_countergoesabove10andtelltheusertheylostthegamebutshouldtryagain.
1.12-Firstgame
31
Tables(part1)Tablesarethelastdatatypewe'lldiscussinthischapter.Otherlanguageshavedifferentnamesforthisdatatypelike"object","hash","map"and"dictionary",andthefeaturesmayvaryfromoneprogramminglanguagetoanother.Tablesareusedtobuildcompositedatatypeslikelists,trees,orabiggreenorcrunningacrossthescreen.Compositedatatypesarehigherorderdatastructurescreatedfrommoreprimitivedatatypeslikenumbersandstrings.Thenumberofdatastructuresyoucancreateareendless.Weneedtolearnaboutafewtonotonlyunderstandhowtableswork,buttobeabletobuildanymodernsoftware.
Thebasicsyntaxfortablesistomakeacurlybrace {(samekeyasthesquarebraceonmostkeyboards)tostartthetable,writesomedatainthetable,thenputaclosingcurlybrace }toendthetable.Soanemptytablewouldlooklikethis:
my_cool_table={}
ListsListsareusuallystartedbywritingthefirstitem,thenthesecond,andsoon.Ifwewantedtomakeagrocerylistinsoftware,itmaylooklikethis:
groceries={
[1]="beans",
[2]="bananas",
[3]="buns"
}
Okmaybeyourtypicalgrocerylistlooksdifferent.Whatdowedowiththisdatanowthatwegotit?Wecanaccessandmodifythedataasiftheywerestoredintheirownvariables.
returngroceries[1]
=>beans
Firstwespecifythevariablenameofthetable,theninsquarebracketsweputthenumberwewant.Youcanaccesstheminanyorderandmodifythemasneeded:
print(groceries[3])
groceries[1]="coffeebeans"
print(groceries[1])
buns
coffeebeans
Theorderyoudefinethemindoesn'tmatter:
groceries={
[3]="beans",
[1]="bananas",
[2]="buns"
}
1.13-Tables(part1)
32
Thenumberinsquarebracketsisthekey.Akeythatispartofanumericsequenceofkeyssuchasthislistisoftencalledanindex.So "bananas"hasanindexof 1.Thepluralofindexisindices.
Don'tforgetthecommasbetweeneachiteminyourlistoryouwillgetquitetheerrormessage:
[string"groceries={..."]:3:'}'expected(toclose'{'atline1)near'['
Whenyouaremissingacommabetweenitems,itthinksithasreachedtheendofthetablebutthenerrorsoutwhenitgoestoclosethetablebutseesanotheriteminsteadoftheclosecurlybracket }.
Anotherissueyoumayrunintoisifyoutrytoaccessakeythathasnodata.Thereisno4thiteminourtablesoifwetrytoaccessit:
print(groceries[4])
Wegetback nil,thesamewaywewouldifwetriedtoaccessavariablenamethathasnodataassignedtoit.
Writingoutlargelistscanbecomeaheadachewhenwehavetomanuallynumbereachiteminalist:
groceries={
[1]="beans",
[2]="bananas",
[3]="buns",
[4]="blueberries",
[5]="butter",
[6]="broccoli",
[7]="basil"
}
Whatifweremoveanitemorwewanttomovesomethingtoadifferentpositioninthelist?Whatapaintohavetore-indexeverything.Thankfullythereisshorthandwayofwritinglists:
groceries={
"beans",
"bananas",
"buns",
"blueberries",
"butter",
"broccoli",
"basil"
}
Thisisidenticaltothecodewrittenabove,exceptnowtheindicesareauto-generatedforme. "basil"hasanindexof 7sinceitisthe7thiteminthelist,butifIcutandpastedittothetopofmylist,it'sindexwouldbe 1andeverythingbelowitwouldberenumberedaccordingly.
LoopingoverlistsIfwewantedtoprintourgrocerylist,wecouldsaysomethinglike:
print(groceries[1])
print(groceries[2])
print(groceries[3])
print(groceries[4])
--andsoon...
1.13-Tables(part1)
33
Butthatisquiterepetitiousandrequiresupdatingifthesizeofourlistchanges.Luckilywealreadyknowaboutwhileloops.
index=1
whilegroceries[index]~=nildo
print(index,groceries[index])
--Gotothenextindexinthelist
index=index+1
end
Seehowinsteadofaccessingeachitemas groceries[1], groceries[2]...wecanjustuseavariableinthesquarebracketsinsteadofanumber.Theninsidetheloopwebumpthenumberupandaccessthenextiteminthelist.Theloopstopswhentheindexgoesbeyondthelastiteminthelistandthereisnothingthere.Sowhenindex8isread,groceries[8]isnilandthewhileconditionisnolongertrue.Whileconditionsdon'tevenneedabooleanexpression.Itcanknowwhetherornottocontinuesimplyifthegivenitemhasdataorisnil.Itcanbesimplifiedtoread:
index=1
whilegroceries[index]do
print(index,groceries[index])
--Gotothenextindexinthelist
index=index+1
end
Again,itknowstoexitwhenitsees falseor nil.Thecaveattothiswouldbeifyoumakeaspeciallistwith falseinit:
groceries={
"beans",
"bananas",
false,
"blueberries",
"butter",
"broccoli",
"basil"
}
Whenthewhileloopgetstothethirditeminthelistandsees false,itwouldstoploopingbeforeitreadstherest.It'snottypicallyagoodideatomixandmatchdifferentdatatypesinalistbecauseofissueslikethis,however,wecouldworkaroundthisifweneededto.Thereisaspecialoperatorfortablestogetthesizeofthelist.
print(#groceries)
7
Aneasywaytorememberthe #operatoristorememberthatitreturnsthe#ofitemsinalist.Usingthisoperatorwecouldwriteourwhileloopinadifferentway.
index=1
whileindex<=#groceriesdo
print(index,groceries[index])
--Gotothenextindexinthelist
index=index+1
end
1.13-Tables(part1)
34
Youcouldeventweakthisslightlytoreadthelistbackwardsifyouwantedto:
index=#groceries
whileindex>0do
print(index,groceries[index])
--Gotothenextindexinthelist
index=index-1
end
Noticewearesubtractingfromtheindexwitheachloopinordertoaccomplishthis.
ExercisesTrytomodifythewhilelooptoonlyprinteveryotheriteminthegrocerylist.(Hint:insteadofincrementingby1oneachread,youwanttoincrementmore.)Writeawhileloopthatcountsto10andpopulatesanemptytablewiththesameitem10times.(Hint:youassigntoindicesjustlikevariables, list[index]="hi".)
1.13-Tables(part1)
35
Tables(part2)Inthelastsectionwesawhowsimpleitwastomakealist.Workingwiththelistwasalittletrickyatfirstbuthopefullynottoobad.Ifwerewindback,wecanrememberthatwecreatedatablebyassigningsomekeysvalues.
boxes={
[1]="JohnDoe",
[2]="AmandaParker",
[3]="TylerReese"
}
Thinkofitlikepostofficeboxesandwelabeleachboxwithauniquenumber.Wheneverwereferenceapostalbox,wedosobyreferencingthenumberwithinthearray(list)ofboxes: boxes[2].Thelabel,orkey,isultimatelyarbitrarythough.Formakingalist,welabelthingsinanincrementalordertomakethemeasiertoloopoverandtogiveusasenseoflinearsequence.Keysdon'tneedtobenumbers.Theycouldjustaswellbestrings:
coins={
["half"]="50cents",
["quarter"]="25cents",
["dime"]="10cents",
["nickel"]="5cents",
["penny"]="1cent"
}
Whichwouldbeaccessedjustthesameway:
print(coins["nickel"])
5cents
Thiscanbereallyusefulfordoingalookupifweinsteaduseavariableforthekey.Trythisoneout:
coins={
["half"]="50cents",
["quarter"]="25cents",
["dime"]="10cents",
["nickel"]="5cents",
["penny"]="1cent"
}
print("Whichcoindoyouhave?")
response=io.read()
print("Yourcoinisworth"..coins[response]..".")
Thisisn'tfarofffromhowcertaindatabasesanddigitalserviceswork.Itemsarestoredinauniquekeythatcanbereferencedforgettingadefinitionoutoflater.That'swhythisdatastructureissometimescalledadictionary.Remember,wecanadditemstoatableafteritisdefined:
coins["silverdollar"]="1dollar"
AnothershortcutLuagivesusiswedon'tneedtousethesquarebracesorquoteswhenaddingkeysthatarestrings.
1.14-Tables(part2)
36
coins.nickel="5cents"
Thelimitationwithdoingthisisthekeysdefinedthiswaycan'thavespacesorspecialcharacters.Theymustbevalidinthesamewayvariablenamesarevalid.
coins.silverdollar="1dollar"--INVALID
coins.silver_dollar="1dollar"--Valid
coins.100="1dollar"--INVALID
Youcanusevariablenamesforkeyswhencreatingthetabletoo:
color="purple"
description="thebestcolor"
colors={
[color]=description
}
print(colors.purple)
print(colors[color])--printsthesamething
Byconvention,stringsaretypicallyusedfordictionary-liketableswhilelistsarenumbers.Don'tmakethemistakeofthinkingthesearethesame:
list={
1="someitem",
["1"]="auniqueitem"
}
Youcoulduseotherdatatypesaskeys,butyoumightfindyourresultstobeveryunexpected:
crazy_list={
[true]="works",
[false]="works",
["true"]="notthesame",
["false"]="notthesame"
}
print(crazy_list[true])
print(crazy_list[false])
print(crazy_list["true"])
print(crazy_list["false"])
crazy_key={}
crazy_list={
[crazy_key]="works"
}
print(crazy_list[crazy_key])
crazy_list={
[nil]="doesn'twork!"
}
print(crazy_list[nil])
Throwsanerror:
[string"crazy_list={..."]:2:tableindexisnil
1.14-Tables(part2)
37
Valuesinatablecanbeanytypeofdata,includingfunctions:
cat={
color="gray",
smelly=true,
make_sound=function()
print("meyuow!")
end
}
cat.make_sound()
ExercisesRemembertheearlyfunctionwemadethatreturnedtheanimalsounds?Makeafunctionwithatableinit,whereeachkeyinthetableisananimalname.Giveeachkeyavalueequaltothesoundtheanimalmakesandreturntheanimalsound.Tryinvokingthefunctionandseeifyougetbackthecorrectsound.
1.14-Tables(part2)
38
Forloops(part1)Wesawpreviouslythatwecouldusewhileloopsformanythings,butwealsosawhoweasyitwastomakeawhileloopthatdidn'trunproperly.Theprogrammerhastomakeavariabletopasstothecondition,makesuretheconditioniswrittenoutcorrectly,andthenmakesuretheconditioncanbechangedsotheloopcaneventuallyend.Thismanystepseachtimewewanttowriteasimpleloopleavesuspronetoerrorsandwastingourtime.Withforloops,wecantellaloopexactlyhowmanytimeswewantittorunandskipallthesesteps.
Numericforloops
fornumber=1,10do
print(number)
end
Onthefirstlinewearesaying"For[startingnumber]through[endingnumber]dothefollowing".The number=isavariableyouareassigningthestartingnumberto.Thevariablenamecanbewhateveryouwant.Thesecondlineisthebodyoftheloopandthethirdlineendstheloop.Ifyourunthisprogram,itwillprintthenumbers 1, 2, 3...through 10as numberisbeingincrementedby1witheachloop.Thisvariableisabitpeculiarthough,notonlybecausewedefineditinthemiddleofastatementbutbecauseitdisappearsafterwearedonewiththeloop.
fornumber=1,10do
print(number)
end
returnnumber
=>nil
Thisiscalledalocalvariable,becauseitonlyexistslocallywithintheforloop.
Forloopsactuallyhave3parameters:
startnumber-weassignthevariabletoitandthevariablewillincrementwitheachloopstopnumber-thelastnumbertoincrementtobeforestoppingtheloopstep-howmuchtoincrementbywitheachloop.Ifwedon'tspecifyastepsizeitwilldefaultto1.
Let'ssaywewantedtoprintoutonlyevennumbers.Wecouldchangethestartingnumberto2andsetthesizeofthestep(3rdparameter)to2:
fornumber=2,10,2do
print(number)
end
Ifwewantedtoiterate,orloopoverandreadeachiteminalist,itwouldlooksimilartoawhileloop.Let'slookatthewhileloopexampleagainjustforcomparison:
items={'a','b','c','d'}
index=1
whileindex<=#itemsdo
print(items[index])
index=index+1
1.15-Forloops(part1)
39
end
items={'a','b','c','d'}
forindex=1,#itemsdo
print(items[index])
end
Wecouldalsocountdownbychangingtheparametersaroundandsettingthesteptoanegative1.
items={'a','b','c','d'}
forindex=#items,1,-1do
print(items[index])
end
Inthiscasetheindexstartsatthepositionofthelastitemandstopswhenitgetstothestopnumber,1.
ExercisesModifythepreviousloopsothatitonlyprintseveryotheriteminthelist.
1.15-Forloops(part1)
40
Forloops(part2)Wecancreateadifferentstyleofforloopusingfunctions,butinordertodothat,weneedtounderstandanotheraspectoffunctionswehaven'tyetcovered.Functionscanreturnmultiplevalues.
sort_numbers=function(a,b)
ifa>bthen
returna,b
end
returnb,a
end
bigger,smaller=sort_numbers(12,18)
print(bigger)
print(smaller)
Thisfunctiontakestwonumbers,checkstoseewhichisbigger,thenreturnsboththebiggernumberfirstthenthesmallernumbersecond.Noticewedidthisbyputtingacommainthereturnstatementthenprovidingasecondvalueafterthecomma.Likewise,wewereabletocapturebothvaluesintovariablesbyputtingthefirstvariablename,acomma,thenthesecondvariable( bigger,smaller=).Wedon'tneedtocaptureeverythingreturnedfromafunction.Wecouldhavejustaseasilycalledthefunctionandonlycapturedthebiggernumberifthat'sallwewantedfromit.
bigger=sort_numbers(12,18)
GenericforloopsLet'stakealookatthesiblingtothenumericforloopcalledthegenericforloop.It'scalledgenericforloopbecauseittakesafunctionthatmakesitbehaveindifferentwaysfordifferentsituations.Itdoesn'tdoanythingonitsown.Itreliesonthefunctiontotellithowtobehave.
ipairsHere'swhatgenericforloopslooklike:
list={'dog','cat','mouse'}
forindexinipairs(list)do
print(index,list[index])
end
ipairstakesourforloopandmakesititerateovereachiteminthelistandgivesusan indexvariabletoworkwithinsidetheloop.Butwait,there'smore! ipairsprovidesuswithanothervariablethatholdsthevalueoftheitematthatindex.Tryitoutyourself:
list={'dog','cat','mouse'}
forindex,valueinipairs(list)do
print(index,value)
end
Ahyes,soconvenient!Thereisonegotchawithdoingthis.Ifyouwantedtoeditthetablefrominsidetheloop,youneedtoaccessthetabledirectly:
1.16-Forloops(part2)
41
list={'dog','cat','mouse'}
forindex,valueinipairs(list)do
list[index]=string.upper(value)
end
print(list[1])
Ifyoutrytojusteditthevalue:
list={'dog','cat','mouse'}
forindex,valueinipairs(list)do
value=string.upper(value)
end
print(list[1])
thelistwon'tbemodified,because valueisjustacopyofthedatathat'sactuallyinthelist.You'reeditingatemporarycopy.
pairsAnotherfunctionforprogrammingforloopswithspecialfunctionalityis pairs.Thiswilliterateovereverykeyinatable:
table={
cat='meow',
dog='bark'
}
forkey,valueinpairs(table)do
print(key,value)
end
Evenindices:
table={
'a',
'b',
'c',
cat='meow',
dog='bark'
}
forkey,valueinpairs(table)do
print(key,value)
end
Nosneakingpast pairsforanyofthesekeyseither:
table={
[1]='a',
[2]='b',
[3]='c',
cat='meow',
dog='bark',
[true]=false,
[{}]='what?'
}
forkey,valueinpairs(table)do
1.16-Forloops(part2)
42
print(key,value)
end
Aneasywaytorememberthedifferencebetween ipairsand pairsisthe"i"in ipairsstandsforindex.Surethere'sadifferencewhenworkingwithweirdtablesliketheoneabove,butwhycan'twejustuse pairsforregularlist-styletables?
table={
[2]='b',
[3]='c',
[1]='a'
}
forkey,valueinpairs(table)do
print(key,value)
end
3c
2b
1a
Asyoucansee,theorderoftheitemsisn'tguaranteedwith pairs. ipairsisalsooptimizedtohandlenumerickeysandwillgenerallyperformfaster,soit'sgoodtoknowthedifference.
Underthegeneric-for-loophood
ipairsand pairsarejustregularfunctionsthatweinvoke.Theyreturnafunction(yes,afunctionthatreturnsafunction!)andthisreturnedfunctionprogramsourlooptobehavehowwewant.
forkey,valueiniterator,list,start_numberdo
print(index)
end
Sothisiswhatagenericforloopreallylookslikewithoutthehelpof ipairsor pairs.Itrequires3parametersthatipairs/ pairsprovidesdatabacktothekeyandvaluevariablesthatwecanuseinsidetheloop. iterator, list,start_numberareallvariableswewouldotherwisehavetodefinewithouttheirhelp.
iteratorwouldbeafunctionweprovidetothelooplistwouldbewhatwewanttoiterateoverstart_numberwouldbethestartingindexinthelist
list={'a','b','c'}
iterator,list,start_number=ipairs(list)
forindex,valueiniterator,list,start_numberdo
print(index)
end
ipairsgivesusaniteratortopasstotheforloop,aswellasourlistwealreadyhad,andastartingnumber.Wecanprinttheresultsofipairsandseethe3thingsitgivesus:
print(ipairs(list))
function:0x156a3f0table:0x1572aa00
1.16-Forloops(part2)
43
Sotosayitagain,genericforloopsrequire3things:aniteratorfunction,ourlist,andanumber.Inordertonothavetowritethemourselves,wegeneratedthose3thingsbyinvoking ipairsthenpassingthemintotheforloopparameters.Don'tfrettoomuchifthisseemsconfusingrightnowbecausewe'renotgoingtoneedtowritecustomforloopsorcustomiterators.
Numericversusgeneric:whichtouse?
Numericforloopsaregoodforsimplecountingbutperformjustaswellormaybeevenbetterthangenericforloops.Genericforloopsaremoreadaptable.Ifyouhaveasituationwhereeitherwouldwork,justusewhicheveryouwant.Itreallywon'tmakeanydifference.
ExercisesMakealistandthenwritebothanumericforloopandgenericforloopthatiterateoverthelistandprinteachitem.Comparethetwoapproaches.Makeatablewithanimalsforkeysandthesoundstheymakeforthekeyvalues.Makeaforloopthatusespairstoiterateovereachandchangethenoisestoallcapitalletters.
1.16-Forloops(part2)
44
ScopesWhendefiningfunctions,wedefineparametersforthosefunctionswhichworklikeregularvariables.Ifwetrytoaccessaparameteroutsideafunctionwewillseethatitis nil.
addition=function(a,b)
print(a,b)
returna+b
end
addition(1,2)
print(a,b)
Theparameters aand barelocalvariables.We'veseenlocalvariableswithforloops,wherethevariablecounting_numbercouldn'tbeaccessedoutsidetheforloop:
forcounting_number=1,4do
print(counting_number)
end
print(counting_number)
Functions,forloops,andwhileloopscreateascopeeachtimetheyareran.Thingscreatedinthescope,includinglocalvariables,aredestroyedwhenthatlooporfunctioninvokeisdone.Thisishowtheprogramtidiesupafteritselfandkeepsthecomputerfromrunningoutofmemory.Theprocessofremovingunuseddatafrommemoryandreleasingcontrolofthatmemoryiscalledgarbagecollection.Luadoesthisforussowedon'thavetothinkaboutit.Variableswecreatenormallydon'tfollowthesamerules.Theywillcontinuetoexistafterthescopetheywerecreatedinhasbeendestroyed.
addition=function(a,b)
text="I'mnotgoingaway."
returna+b
end
addition(1,2)
print(text)
Eventuallyallthesevariableswemakewillfillupmemoryunnecessarily.Thiscanalsobeproblematicifweaccidentallymaketwovariablesbutusethesamename.
x=2
addition=function(a,b)
--Thismodifiesthexatthetop!
x=9
returna+b
end
print(x)
result=addition(x,y)
print(x)
1.17-Scopes
45
Whenyouwritealargeprogram,you'llinevitablymaketwovariableswiththesamename,sothiscouldbeahugeissue.Thesolutionistomakeourvariableslocalvariablesbyputtingthekeyword localbeforeallourvariableswhenwecreatethem.
addition=function(a,b)
localtext="I'monlyaccessibleinsidethefunction."
returna+b
end
addition(1,2)
print(text)
Now textisonlyinthescopeofthefunctionandnotgettingintootherpeople'sbusiness.Ifyoudon'twrite localbeforeavariable,thenwhatyouarecreatingisaglobalvariable.It'sashamethatvariablesareglobalunlessweexplicitlytellthemnottobe.Thereisneverareasontocreateglobalvariablesifyouhaveenoughknowledgetoknownotto.Soasabestpractice,allcodeexamplesgoingforward,onlylocalvariableswillbecreated.Let'sseeafewmoreexamples:
localnumber=12
--Thisfunctionhasnoparameters
localprint_numbers=function()
--Thisworks.Youcanseevariablesoutsidethefunction
print('number:',number)
--Thisdoesn'twork.Thevariabledidn'texist
--atthetime"print_numbers"wascreated.
print('number2:',number2)
end
localnumber2=18
print_numbers()
--Wealready"declared"number.Wedon'twrite"local"again.
number=13
print_numbers()
Noticewhenitprintedthatitknew numberwasupdatedto13butcouldn'ttrack number2.Aslongasavariablewascreatedbeforethescope(function'sscopeinthiscase)wascreatedthenitwillalwaystrackthelatestvalue.
Asareminder,wealreadysawwiththeforloopandwhileloopthatyoucanmodifyvariablesintheouter,orparent,scope:
localnumber=1
whilenumber<10do
number=number+1
end
Thisalsoworkswithfunctions:
localnumber=1
localmutate_number=function()
number=7
end
print(number)
mutate_number()
1.17-Scopes
46
print(number)
Whatifyoumaketwovariableswiththesamenameintwodifferentscopes?Tryrunningthisone:
localnumber=18
localshadowing=function()
localnumber=6
print(number)
end
print(number)
shadowing()
Theinner numberdoesnotaffecttheouter numberinanyway.Theouter numberisnotaccessibleinsidethefunctionaslongastheinner numberexists.Ifavariablehasthesamenameasanothervariableinaparentscopethentheparentscopevariablebecomesinaccessible:thisiscalledshadowing.Typicallyyouwouldwanttoavoidshadowingifatleastforthereasonthatusingthesamevariablenametwiceinthesamefilecanmakethecodehardertoreadandmorepronetoerrorsbeingintroduced.
Onemoreinterestingthingswithscopes.Normallyafunctioncannotseeitself:
localself_reference=function()
print(self_reference)--Thiswillbenil!
end
self_reference()
Itdoesn'tseeitselfbecausethevariableisstillbeingcreatedwhenthefunctionisbeingcreated.Butrememberthatifavariableexistsbeforethefunctiondoes,itcanseethelatestup-to-datecontentofthatvariable.Sohere'sthetricktomakethatwork:
localself_reference=nil
self_reference=function()
print(self_reference)
end
self_reference()
Thevariableisdeclared,eventhoughweassigned niltoit.Assigningniltogetavariabledeclaredisprettycommon,soLuaincludesashorthandwayofdeclaringemptyvariables:
localself_reference
self_reference=function()
print(self_reference)
end
self_reference()
Thismayseemsillythatafunctionwouldneedtoaccessitself,buttherearesomeverypowerfulapplicationsforthisthatwewillseelateron.
ExercisesDeclareaglobalvariableinsideafunction, x=5(nolocalkeyword)thentrytoprintthevariablefromoutsidethefunction.Canitbeprinted?How?
1.17-Scopes
47
1.17-Scopes
48
Chapterreview
TerminologyOperatorandOperation-Operatorsaresymbolsthatcauseanoperation,orinteractiontohappenbetweentwopiecesofdata.Anexampleoperationwouldbe 5+5. (5+5)*2wouldbetwooperations.
ModuloandModulus-Moduloisaspecialtypeofarithmeticoperationbetweentwonumbersusingamodulusoperator.Themodulusisrepresentedbya %(percentsymbol).Example: 24%2==0
VariableandValue-Variablesarenamesthatreferenceacertainpieceofdata.Thevalueiswhatisstoredinsidethevariable: variable="value"
Statement-Thisiswhenyoudosomething,likeanoperation(orgroupofoperations),declareavariable,orinvokeafunction.Forinstance,thisisaprintstatement: print("hello")
Invoke-Run/callafunction.
ParameterandArgument-Functionstellyouwhatandhowmanyparameterstheyhave.Argumentsarethedatathatgetspassedintothoseparameters.
Boolean- trueor false
Equality-Whetherornottwothingsareequal.Thisisusuallydonewithanequal( ==)comparison.
Loop-Codethatrepeats.
KeyandIndex-Keyisthenamedreferenceinatablewheredatacanbefound.Itissimilartoavariable.Indexisakeythatcomesinanorderedsequence,suchasnumberedkeysinalist.Thepluralofindexisindices.
Iterate-Loopoveralistanddosomethingwithit.
Scope-Anareawherevariablescanbecreatedthataren'taccessiblefromtheoutside.Scopesarecreatedbyfunctionsandloops.
LocalandGlobal-Localdescribesthingsaccessibleonlyinthecurrentscope,suchaslocalvariables.Globalthingsareaccessiblefromanywhereintheprogram.
Shadowing-Whenalocalvariablehasthesamenameasavariableinaparentscopeandpreventsyoufromaccessingtheparentscopevariable.
1.18-Chapterreview
49
Chapter2:IntroducingLÖVEThegoalofthischapteristoapplyallthebuildingblockswelearnedinthefirstchapterandmakethemconcretethroughpractice.Bytheendofthechapteryouwilllearnreal-worldskillssuchashowtointeractwithotherpeople'ssoftwareandbasicprinciplesfordesigningandbuildingyourownprograms.
2.0-IntroducingLÖVE
50
UpandrunningLearningbymakingisfunandeffective.Learningtointerfacewithotherpeople'ssoftwareispartofbeingaprogrammerandisanecessaryskilltohaveasone.LÖVEisaframeworkformakinggames.Aframeworkisjustasetoftoolsorfunctionalitycombinedtogethertoservealargerpurpose.InthecaseofLÖVEthisincludes,butisnotlimitedto:
Functionsforloadingimages,audio,andtextFunctionsforcreatingandmovingobjectsonscreenParametersformakingtheobjectsinteract
InstallingyourdevelopmentenvironmentTheLÖVEwebsitehaslinkstoinstallthesoftwareonyoursystem.IfyouhaveLÖVEinstalledalready,makesurethatyouatleastversion11(mysteriousmysteries)assomefunctionalitywe'llcoverheredoesn'texistinolderversions.Formobiledevicesyoucanfindacopyintheappstore.
AlongwithinstallingLÖVE,youwillneedatexteditorforcreatingLuafilesonyoursystem.I'mnotgoingtomakeanyrecommendationshere,becauseintheenditallcomesdowntopersonalpreference,butyoucancheckthislistbytheLÖVEcommunityifyouneedastartingpoint.Itfeaturesdifferenteditors(andrecommendedplugins)forLÖVEandLuadevelopment.Simplypickone.
TestthatLÖVErunsWhenyoulaunchLÖVE,(seeinstructionsbelowonhowtodothat)youwillbegreetedwithafriendlygraphicandthetext"NOGAME",meaningyouarerunningtheenginebutdidn'tgiveitagametoload.
macOSOnceyouhavedownloadedtheLÖVEbinaryformacOS(64-bitzipped),proceedtothe"Downloads"folderandunzipthearchive.Youshouldnowseeanapplicationcalled"love".
macOSmightshowyouawarningmodal,becauseyouaretryingtoopenanapplicationbyanunverifieddeveloper.Ifso,rightclickontheapplicationandchoose"open"and"open"againinthefollowingdialog.Youshouldnowbegreetedbytheno-gamescreen.
Addendum:Homebrew
IfyouarefamiliarwithdevelopmentonamacOSmachine,youmighthaveheardofHomebrew.Itisapackagemanagerwhichallowsyoutoinstallalotofprograms,librariesandsoondirectlythroughyourTerminal.
Ionlyrecommendthisapproachforadvanceddeveloperswhoknowwhattheyaredoing.ForcompletenesssakeherearethestepstoinstallLÖVEviaHomebrew.
/usr/bin/ruby-e"$(curl-fsSLhttps://raw.githubusercontent.com/Homebrew/install/master/install)"
brewtapcaskroom/cask
brewcaskinstalllove
2.1-Upandrunning
51
Oneofthebenefitsofthisapproachis,thatyoudon'thavetosetupyourownterminalalias,becauseHomebrewalsotakescareofthat.
Windows
IfashortcutforLÖVEdidn'tappearinthestartmenu,youshouldbeabletotype"love"inthesearchandseeit.
Ubuntu
Openthe"UbuntuSoftware"applicationandsearch"love2d".Clickonthetopresultandyoushouldseeafamiliarapplicationdescription:
Clickthe"Install"buttontoinstallit.Onceinstalled,youcansearchforthe"terminal"application.Oncethatisopen,type lovetolaunchtheapplication.
OtherGNU/LinuxoperatingsystemsMostdistroshaveLÖVEintheirrespectiverepositories:
Archlinux-basedsystems- sudopacman-SyloveFedora-basedsystems- yuminstalllove
Onceinstalledfromyourpackagemanager,openaterminalandtype lovetotestthatitruns.
Ifyourdistrodoesn'thaveLÖVEinthepackagemanageranalternativewaytogetitistodownloadedtheAppImageversionfromthehomepage(https://love2d.org/).AppImagefilesarelikeauniversalexecutablethatworksacrossLinuxsystemssimilartothewayan"exe"fileworksonWindows.Oncedownloaded,opentheterminal,changetothedirectorywhereyoudownloadedtheAppImageandtypethecommands:
chmoda+xlove-11.1-linux-x86_64.AppImage#Marksthefileasasafeexecutable
2.1-Upandrunning
52
./love-11.1-linux-x86_64.AppImage
love-11.1-linux-x86_64.AppImageshouldbechangedtomatchthenameofthedownloadedAppImagefile.
CreateaprojectfolderFindasafeplacetocreateafolderandgiveitthename"hello".Withinthefolder,createanewtextfilenamed"main.lua".Thiswillbewhereourgame'scodegoes.
NoteforWindows:Inordertocreateafilewiththename"main.lua",youmayneedtofirstcreateanew"TextDocument",right-clickonit,click"Properties"thenfromthepropertiesmenuchangethefileextensionfromreading"main.lua.txt"tojust"main.lua".ToavoidhavingtodothisforeveryLuafileyoucreateinthefutureyoucantellWindowstoalwaysshowthefullnameoffiles,includingtheirextension.Toenablethis,type"ControlPanel"intheprogramsearchandopenthe"ControlPanel"result.Withinthecontrolpanel,select"FileExplorerOptions".Clickthe"View"tab.Removethecheckmarkfromthe"Hideextensionsforknownfiletypes"andpressApply/OK.
CreateatestgameWithin"main.lua",writeoutthefollowingfunction:
love.draw=function()
love.graphics.print('HelloWorld!',400,300)
end
Nowlet'sfigureouthowtorunitandseewhatitdoes.
RunthegameThiswillbedifferentfordifferentoperatingsystems.
macOSStartingyourgame
ThesimplestwaytostartaLÖVEgameistodragthewholefoldercontainingthegame'ssourcefiles(notjustthemain.luafile!)ontotheapplicationfile.
2.1-Upandrunning
53
Thisalsoworkswith.lovefiles.
Usingtheterminal
IfyouarefamiliarwiththeTerminal,youcanuseitasamoreconvenientmethodofstartinggames.
Assumingthedownloaded"love"applicationisstillinyour"Downloads"folder,openanewTerminalandtypethefollowinglines(youneedtopressreturnaftereachline):
#SwitchestotheDownloadsfolder
cd~/Downloads/
#StarttheLÖVEapp
openlove.app
ThisobviouslystartsLÖVEwithano-gamescreensincewedidn'tspecifywhichfoldertoload.Let'sfixthisbytypingthefollowingcommand:
#Inmycasethefullcommandtostartthegalaxydemowouldbe:
#open-alove.app~/Downloads/galaxy
open-alove.app<path-to-your-game>
Usingaterminalalias
Wecanstillimproveonthepreviousmethodbyusinganalias.Beforewedothis,wemovethe"love"applicationbundletotheApplicationfolder.
#MovetheappfromDownloadstoApplications.
~mv~/Downloads/love.app/~/Applications/love.app
2.1-Upandrunning
54
Nowtrythefollowingcommand:
#StartLÖVEbyusingthescriptinsideoftheapplicationbundle.
~/Applications/love.app/Contents/MacOS/love<path-to-your-game>
AsyoucanseewenowcanrunLÖVEwithoutusingthe opencommand,whichalsohastheaddedbenefitofshowingthegame'sconsoleoutputdirectlyinourterminal.
Ofcourseitwouldberatherinconvenientifwehadtospecifythefullpatheachtimewewanttorunourgame,sowe'llnowsetupanaliasinyour.bash_profile(whichbasicallyactsasaconfigurationfileforyourbashsessions).
Sinceitisahiddenfileyoumightnotbeabletospotitinyourfinder,butwecansimplyedititthroughourTerminal.
#Appendsthealiasdefinitiontoanexisting.bash_profile
#orcreatesanewone.
echo"aliaslove='~/Applications/love.app/Contents/MacOS/love'">>~/.bash_profile
#Usetheupdated.bash_profileforthecurrentsession.
source~/.bash_profile
#Startyourgamethroughthealias.
love<path-to-your-game>
Andthat'sit:Youcannowquicklyrunyourgameswiththe lovealias.Thisisespeciallyhandyifyouareinsideofthegame'sdirectory,becauseallittakesnowisaquick love.tostartthegame.
Windows
FindtheshortcuttoLÖVEanddraganddropitinthegamefolderlikeso:
Thenright-clickontheLÖVEshortcutandyouwillseea"Properties"dialogwindowsimilartothis:
2.1-Upandrunning
55
The"Target"fieldmaybethesameorslightlydifferentdependingonyoursystemversion.Withoutdeletingthetextstringcurrentlyinthe"Target"field,appendthepathtoyourgamefolderinquotestotheend.Youcancopyandpastethispathfromthefolder'saddressbar.Forinstancethetargetpathinthepictureshouldgofromreading:
"C:\ProgramFiles\LOVE\love.exe"
to
"C:\ProgramFiles\LOVE\love.exe""C:\Users\IEUser\Desktop\hello"
Nowpress"OK"toclosethePropertiesdialogandclickingtheshortcutwilllaunchthegame.Ifthegameransuccessfully,youwillseeablackwindowwiththetext"Helloworld!"insmallprint.
GNU/LinuxIfyouknowthelocationofyourfolder,youcanopenaterminalandtypethecommand:
love<path-to-your-game>
Where <path-to-your-game>hasbeenchangedtotheactualfolderpathwhereyourgameresides.
Ifyouarealreadynavigatedintothegamefolder,youcanrunaterminalcommandwithinthatdirectory:
2.1-Upandrunning
56
love.
The"."simplymeans"thisfolderthatIamcurrentlyin".
Congratulations!You'vesetupyourdevelopmentenvironmentforwritingagameinLua.Ifyouhadissuesgettingthroughthis,reachouttomeeitherthroughaGitHubissueormycontactinformationandIwillupdatethisguidetoincludinganyadditionaltroubleshootingstepsforfutureusers.
Nowthatourdevelopmentenvironmentissetupandourfirstgameisrunning,trymodifyingthecodesothestring"HelloWorld!"readssomethingdifferent.It'sprettyapparentthatrunningthisfunctionprintstothescreenwhateverstringwegiveit.Butwhatarethe2ndand3rdparametersfor?
love.graphics.print('HelloWorld!',400,300)
Trymodifyingthosenumbersandseewhatitdoestothetext.
2.1-Upandrunning
57
LÖVEstructureOpenup"main.lua"andtakealookatourfirstline.Wedefinedafunctioncalled love.draw,whichimpliesthereisatablecalled loveandwecreatedakeyinitcalled draw.Indeedthisisthecase,butsomehowthefunctionwasinvokedwithoutusinghavingtowrite love.draw()andinvokeitourselves.Thisrequiresahigh-levelexplanationofwhattheengineisdoingwithourfile.
WhenLÖVEisrun,beforeourmain.luafileisran,atablecalled loveisdefinedasaglobalvariable.Wecanassignfunctionstothistable( love.draw)andaccessfunctionsalreadydefinedinit( love.graphics.print).love.graphics.printhastwodotsinit,sothatmeansthelovetableprobablylookssomethinglike:
love={
draw=nil,
graphics={
print=function()...end
}
}
The lovetablehasaplentyofothertablesnestedinit,anditputssimilarfunctionsintablestogether.Soallthefunctionsrelatingtographicsareinsidethe love.graphicstable.
Once"main.lua"isdonerunning,we'veaccessedandmodifiedthe lovetableandaddedsomenewfunctionalitytoit,tellingithowtodrawtothescreenbydefiningour love.drawfunction.Ifwedefineafunctionwiththisname,thegameenginewillseeitandinvokeit.Infact,itcontinuouslyinvokes love.drawmanytimesasecond.Toprovemypoint,let'smodifymain.luaandmakeitprintanumber.
localnumber=0
love.draw=function()
number=number+1
love.graphics.print(number,400,300)
end
Eachtimewegotoprintthenumber,weincreaseitby1.Runthisprogramandseehowquicklythenumberclimbs.
The lovetableisaseeminglycomplexstructureoftablesinsidetablesandfunctionsinsidethose,butwe'llgraduallylearnthestructureandpurposeofeachthingoverthecourseofthischapter.Inthenextsection,let'stakealookatthe2ndand3rdparametersin love.graphics.printandseehowtheywork.
2.2-LÖVEstructure
58
GeometryIfyoumodifiedthenumbers 400and 300inmain.luayouwillhaveseenthattheymovethetext.Realizingthatthey'resomekindofcoordinates,let'stalkaboutgraphs.
Whenlearningaboutgraphsingeometryclass,welearnedaboutanx-axisandy-axisandlabeledplottedpointsalongthegraph.Ifyouwantedtomark(-2,-4)thenyouwouldfindwhere-2onthex-axisintersectswith-4onthey-axis.KnowingthatXishorizontalandYisvertical,ifwehad(-2,-4)and(1,2)wecoulddrawitoutlikethis:
Thesetwopointscouldevenbeconnectedtoforma2-dimensionalline:
Beforewegettoofarondrawingpointsandlines,let'slookbackatourfunction:
love.graphics.print("HelloWorld!",400,300)
The 400istheXpositionandincreasingitwillmovethetextfurthertotheright.DecreasingtheXpositionwillmovethetextfurthertotheleft.The 300istheYposition,onedifferencebetweencomputersandgeometryclassisdataiscalculatedfromtoptobottom,soincreasingtheYpositionmovesthetextdownanddecreasingitmovesthetextup.Let'stakealookatwhatourgame'sgraphlookslikewiththepoint(5,3)highlighted:
Noticethatthetop-leftcornerofourscreenis(0,0),soifyoutriedtodrawanypointswithnegativenumberstheywouldbedrawnoffscreenwherewecan'tseethem.Anotherthingtonoteisinthegame,thecoordinatesrepresenthowmanypixelsdownandtotherightwewanttodraw.Sincecomputerscreensaremadeofsomanypixels,youneedtouselargenumberstomakeanoticeabledifference.
Ifwewantedtodrawapolygon(shape)suchasatriangleonthisgraph,wewouldhavetogivethreepoints:
Inthesameway,wecanplotoutpointsinourcodeandtellittodrawalinetoconnectthedots.Let'suselargernumbersthough.Rewritemain.luatolooklikethis:
love.draw=function()
love.graphics.polygon('line',50,0,0,100,100,100)
end
Thenumbersinthiscodecanbereadoffinpairstoidentifythecoordinates:(50,0),(0,100),(100,100)LÖVE'sphysicsenginetakesthecoordinates,startingatthefirstpointandconnectsthemwithalinesequentially.Onceitreachesthelastpoint,itdrawsalinefromthelastpointbacktothefirsttoclosetheshape.
Let'stryarectangletogetsomemorepracticein:
love.draw=function()
localrectangle={100,100,100,200,200,200,200,100}
love.graphics.polygon('line',rectangle)
end
Noticethistimeinsteadofpassingthenumbersdirectlyinto love.graphics.polygon,weputthemintoalistandpassedthelistin.Passingincoordinatesbothwayshasthesameeffect.
Anotherimportantthingtothinkaboutisifyoudrawapolygonwith4ormoresides,youneedtomakesurethepointsarelistedinthecorrectorder.Considerthefollowingexample:
Ifwefedthepointsintothefunctioninaclockwiseorcounter-clockwise/anti-clockwiseorder,therectanglewouldbedrawnthesameeitherway.Ifwefedthepointsinfromcrossdirections,wemayaccidentallydrawabowtie:
Creatingshapeswithunclosedsidesdon'tplaywellwithLÖVE'sphysicsengineassuchshapesarenotphysicallypossible.Ifyoutrytodothis,youmaynotseetheshapeyouexpect,andperhapsnothingwillbedrawn.
2.3-Geometry
59
Creatingshapeswithunclosedsidesdon'tplaywellwithLÖVE'sphysicsengineassuchshapesarenotphysicallypossible.Ifyoutrytodothis,youmaynotseetheshapeyouexpect,andperhapsnothingwillbedrawn.
ExercisesTakealookatthedocumentationforthefunctionlove.graphics.polygon.Theexampleshowstheargument'line'isastringandrepresentsthe"drawMode".Trychangingthe"drawMode"from 'line'tooneoftheotheravailableoptions(seetheexamplesorclickthedrawModelinkonthewikipage).Whatotheroptionisthere?Howdoesitwork?Tryitoutandseehowitworks!Trymakingapolygonwith5sides/points.Hint:usethesquareexampleaboveasareference.
2.3-Geometry
60
GameloopAnotheraspectcommonwithgameenginesisthatthereisaloop(likeawhileloop)thatcontinuouslyrunsandkeepsthegamegoing.Theorderthatthingshappeninvaries,butthecontentsmoreoflesslooklike:
1. Gameisstarted.Loadgamefiles.2. Beginloop.3. Checkforinputfromkeyboard,joystick,orotherperipherals.4. Ticktimeingame.5. Redrawthescreen.6. Gobacktostep2.
Duringstepsinthegameloop,LÖVEinvokescertainfunctionsinsidethe lovetable.Forinstance,everytimethescreenneedstobere-drawn,thegameloopinvokes love.draw()ifyoudefinedit.InthestepwhereLÖVEchecksforuserinput,ifthereisuserinput,itinvokes love.keypressed(PRESSED_KEY)ifwedefinedit.The PRESSED_KEYthatispassesinofcoursedependsonwhatkeytheuserpressed.Whendefining love.keypressed,itmaylooksomethinglikethis:
love.keypressed=function(pressed_key)
print('keywaspressed:',pressed_key)
end
Let'smodifymain.luatohaveacontrivedexample:
localcurrent_color={1,1,1,1}
love.draw=function()
localsquare={100,100,200,200,100,200,100,200}
--Initializethesquarewiththedefaultcolor(white)
love.graphics.setColor(current_color)
--Drawthesquare
love.graphics.polygon('fill',square)
end
love.keypressed=function(pressed_key)
ifpressed_key=='b'then
--Blue
current_color={0,0,1,1}
elseifpressed_key=='g'then
--Green
current_color={0,1,0,1}
elseifpressed_key=='r'then
--Red
current_color={1,0,0,1}
elseifpressed_key=='w'then
--White
current_color={1,1,1,1}
end
end
Whenyoupressanyofthekeys,"b","g","r",or"w",ourfunction love.keypressedwillbeinvokedandthevariablepressed_keywillbeastringmatchingoneofourletters.Thischanges current_color,whichischangingthecolorbeingdrawnin love.draw.
Inthenextsection,let'sseehowLÖVEhandlesthe"4.Ticktimeingame."stepofthegameloop.
2.4-Gameloop
61
ExercisesTryaddingafewmorecolorstotheprogram.Tounderstandhow love.graphics.setColorworks,seethedocumentation.Makeissothatiftheescapekeyispressed,thefunction love.event.quitisinvokedandthegameexits.Thestringtousefortheescapekeycanbefoundonthewiki'sKeyConstantpage.Spoilers:thesolutioncanbeseeninthenextsection.
2.4-Gameloop
62
DeltatimeHere'swhatwe'velearnedaboutthegameloopsofar:
1. Gameisstarted.Loadgamefiles.main.luaisloadedandthe lovetableisupdatedwithourmodifications.
2. Beginloop.3. Checkforinputfromkeyboard,joystick,orotherperipherals.
Iftherewaskeyboardinputandwedefined love.keypressed,invokeit,passingitinformationaboutthepressedkey.
4. Ticktimeingame.???
5. Redrawthescreen.Invoke love.drawifwedefinedit.
6. Gobacktostep2.
Let'stakealookatthefunction love.update:
love.update=function(dt)
print(dt)
end
NoteforWindows:UnlessyouarerunningLÖVEfromtheconsole,youwon'tseeanythingprintedout.Putthisfileinthegamefoldernexttomain.luaunderthename"conf.lua":
--LÖVEconfigurationfile
love.conf=function(t)
t.console=true
end
Thisconfigurationfilewillletussetspecialparametersforourgame.Don'tworrytoomuchaboutwhatalltheoptionsare,butifyou'recuriousthenyoucanfindthedocumentationhere.EssentiallythiswillopenaconsolewindowonWindowstoseeprintedvalues.
Nowrunthegameandifyouweren'tseeingthe print(dt)messagedisplayanythingyoushouldnowseeitbeinginvokedmanytimesasecond,printingoutadecimalnumber. dtstandsfordeltatimeanditrepresentstheamountofsecondsthathaspassedsincethelastgameloop.Ifthegameloops4timesasecond,thatmeans love.updateand love.drawgetinvoked4timeseachsecondaswell.Thedeltatimeinthiscasewouldberoughly 0.25asroughly1/4asecondhaspassedbetweeneachtime love.updatewascalled.Somecomputersarefasterthanotherssothenumberofgameloopspersecondwillbedifferent.Youarelikelyseeingnumbersaround 0.01orsmaller,meaningthegameisrunningroughly100framesasecond.Let'saddacountertothescreenlikebefore,butnowusingdeltatime.
localcurrent_color={1,1,1,1}
localseconds=0
love.draw=function()
localsquare={100,100,100,200,200,200,200,100}
--Printacounterclock
localclock_display='Seconds:'..seconds
love.graphics.print(clock_display,0,0,0,2,2)
2.5-Deltatime
63
--Initializethesquarewiththedefaultcolor(white)
love.graphics.setColor(current_color)
love.graphics.polygon('fill',square)
end
love.keypressed=function(pressed_key)
ifpressed_key=='b'then
--Blue
current_color={0,0,1,1}
elseifpressed_key=='g'then
--Green
current_color={0,1,0,1}
elseifpressed_key=='r'then
--Red
current_color={1,0,0,1}
elseifpressed_key=='w'then
--White
current_color={1,1,1,1}
end
ifpressed_key=='escape'then
love.event.quit()
end
end
love.update=function(dt)
--Addupallthedeltatimeaswegetit
seconds=seconds+dt
end
Imagineifwewantedtomoveacharacteracrossthescreenbutwedidn'tusedeltatime.Thecharacterwouldrunfasteronsomecomputersandsloweronothers.Computerswouldkeepgettingfasterandthegamewouldrunsofastitwouldnolongerbeplayable.Deltatimesolvesthisissueandwe'llbetakingadvantageofitforeverythingtime-basedinourgame.
ExercisesChangetheline localclock_display='Seconds:'..secondssothat secondsisformattedtodisplaywholenumbers.Hint:youwillneedtouseLua'sbuilt-in math.floorfunctiontoformat seconds.Changethexpositionoftheleftsideofthesquarefrom 100to (seconds*10)andwatchwhatthesquaredoes.
2.5-Deltatime
64
MappingLet'ssidetrackfromLÖVEforaminutetolearnaboutaconceptcalledmaps.Nottobeconfusedwithoverheadmapsaplayerwouldwalkaroundoninagame,butdatamaps.Weactuallydidmappingbackinchapter1whenwelearnedabouttables.
coins={
["half"]="50cents",
["quarter"]="25cents",
["dime"]="10cents",
["nickel"]="5cents",
["penny"]="1cent"
}
print("Whichcoindoyouhave?")
response=io.read()
print("Yourcoinisworth"..coins[response]..".")
Whenevertheusertypedinacoin,wemappedthecoinnametoavaluebylookingupthecoinnameinthetable,ordictionary.Sowhat'sthedifferencebetweentables,dictionaries,andmaps?
tablesarejustadatatypeinLuathatcanbeusedtobuilddatastructureslikelistsanddictionariesdictionariesarekey-valuestoragesusedtocentralizesimilar-purposedatainoneplaceandmakeiteasiertolookthedataupmapsaredatastructuresusedtotranslateonetypeofinformationtoanother,andadictionaryisonetypeofmap
Dictionariesaretheonlytypesofmapwe'llbeconcernedabouthere,butknowthatmapsgenerallyrefertoinstancesofdatastructuresthatdomapping.Thereareoftendiscrepanciesinterminologybetweenmathematicsandthevariousfieldsincomputerscience.Don'tbesurprisedifyouseedictionariesandmapsbeingusedsynonymouslyinothercontextslaterinlife.
Let'sdosomemappingonourcodewepreviouslywrotetogetabetterfeelforthem.Rememberallthoseif/elseifstatementsinmain.lua?
ifpressed_key=='b'then
--Blue
current_color={0,0,1,1}
elseifpressed_key=='g'then
--Green
current_color={0,1,0,1}
elseifpressed_key=='r'then
--Red
current_color={1,0,0,1}
elseifpressed_key=='w'then
--White
current_color={1,1,1,1}
end
ifpressed_key=='escape'then
love.event.quit()
end
Wecanputallthatfunctionalityinamaplikethis:
localkey_map={
b=function()
current_color={0,0,1,1}--Blue
end,
2.6-Mapping
65
g=function()
current_color={0,1,0,1}--Green
end,
r=function()
current_color={1,0,0,1}--Red
end,
w=function()
current_color={1,1,1,1}--White
end,
--Closethegame
escape=function()
love.event.quit()
end
}
Thisdoesn'tlookanymoreconcisethanourpreviouscode,butourgoalistokeepthe love.keypressedfunctioncleaninthiscase.Whenakeyispresseditwillbemappedtoakeyfunctionwedefinein key_map.Anotherimportantthingisthesefunctionscouldbemodularandmovedanywhereweneedthemtobe,andevenre-used.Let'snotgotoocrazyrightnowthough.We'llkeepthekeymapsomewherenearthetop.
localcurrent_color={1,1,1,1}
localseconds=0
localkey_map={
b=function()
current_color={0,0,1,1}--Blue
end,
g=function()
current_color={0,1,0,1}--Green
end,
r=function()
current_color={1,0,0,1}--Red
end,
w=function()
current_color={1,1,1,1}--White
end,
escape=function()
love.event.quit()
end
}
love.draw=function()
localsquare={100,100,100,200,200,200,200,100}
--Printacounterclock
localclock_display='Seconds:'..math.floor(seconds)
love.graphics.print(clock_display,0,0,0,2,2)
--Initializethesquarewiththedefaultcolor(white)
love.graphics.setColor(current_color)
love.graphics.polygon('fill',square)
end
love.keypressed=function(pressed_key)
--Checkinthekeymapifthereisafunction
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
love.update=function(dt)
--Addupallthedeltatimeaswegetit
seconds=seconds+dt
end
2.6-Mapping
66
Ifyoupressakeythatisn'tpartofthemapthenthenewifstatement( ifkey_map[pressed_key]...)willseethatkeydoesn'texistinthemapandnotdoanything. key_map[pressed_key]()isthesameassaying key_map['b'](),key_map['escape']()orwhateverthevalueof pressed_keywasatthetime.
2.6-Mapping
67
TheworldAworldisaphysicalspacewhereobjectscanbecreated(spawned)andinteract.Shapesandotherthingsdrawnonthescreenarenotimplicitlypartofaworldandwon'tinteractwitheachotherunlesstheyare.Multipleworldscanco-exist,buttheobjectsineachworldwon'tinteract.GoingforwardIwillrefertotheseobjectsasentities.
EntitiesEntitiesaremadeupofdifferentcomponentsthatallowthemtointeract.Thesearethe3fundamentalphysicalcomponents:
shape-somesortofpolygontogiveourentityaphysicalshapethatdeterminestheboundariesoftheentitybody-holdsphysicalpropertiessuchasmassfixture-attachesashapetoabody
Let'swriteanewmain.luafromscratchandseehowtheseareallwiredup.First,aworldneedstobedefined:
localworld=love.physics.newWorld(0,100)
love.physics.newWorldreturnsatable,aninstanceofaworld.Thetableholdsfunctionsthatallowustoapplyattributestotheworld.Italsoholdsalltheentitiesinourworld,whichiscurrentlynoneoninitialization.Accordingtothedocumentationon love.physics.newWorld,our1stand2ndparameterssettheXandYgravityonourworld.Wedon'twantanysidewaysgravity,butwe'llgoaheadandsetanarbitrarynumberfortheverticalgravity.
Whilefocusingontheworld,weshouldallowtheworldtoknowwheneverwegetanupdatetothedeltatime.Aworldwithouttimewouldbefrozen;Bylettingtheworldknowaboutthepassageoftime,itcanknowwhetheritneedstomakeanentityfallanothermeterortwometers…
love.update=function(dt)
world.update(world,dt)
end
Actually,let'sdoonetrickhere.WhencallingafunctioninLuaandthefirstparameterofthefunctionisthetablethefunctionisstoredin,youcanuseashortcutnotation:
love.update=function(dt)
world:update(dt)
end
Asidefrombeingeasiertowrite,you'llseethiswayofinvokingfunctionsusedallovertheplaceintheLÖVEdocumentation.
Finally,we'lladdanentitytothegamein4steps:
1. Createatabletostoreallthepiecesofourentitytogether.Notentirelynecessarybutwe'lllearnlaterwhythisstepmakesthingseasier.
2. Createabody.Thiswillbeaddedtotheentitytableandtheworld.3. Createtheshapewewanttheentitytohave.4. Createafixturetoattachthebodyandshapetogether.
--Triangleisthenameofourfirstentity
localtriangle={}
2.7-Theworld
68
triangle.body=love.physics.newBody(world,200,200,'dynamic')
--Givethetrianglesomeweight
triangle.body:setMass(32)
triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)
triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)
love.draw=function()
love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))
end
Aftercreatingthe bodytableinside triangle,wecalled triangle.body.setMasstosetaweightpropertyonourtrianglesoitcanfall.Noticewewrote triangle.body:setMass(32),whichisthesameassayingtriangle.body.setMass(triangle.body,32)butshorterandmoreconventionaltothewaytheLÖVEdocumentationwrites.
What'sgoingoninside love.drawlooksprettycrazysolet'sbreakthelonglineup.
love.graphics.polygon(
'line',
triangle.body:getWorldPoints(triangle.shape:getPoints())
)
We'veused love.graphics.polygonpreviouslysoitspurposeshouldalreadybefamiliar.Thefirstparameter 'line'istellingitthatwewantanoutlineofashapedrawn.Thesecondparameterisatablecontainingthepointsthatneedtobeoutlined.Togetthetriangle'spointswecall triangle.shape:getPoints(),butthisonlyreturnstheshapeofthetriangleandtherelativepositionofthepoints.Bythencallingtriangle.body:getWorldPoints(triangle.shape:getPoints())weconvertthoserelativepointstotheirabsolutepositionasthat'swhatthepolygondrawingfunctionneedstoknowsoitcandrawthetriangleexactlywhereitissupposedtobeonthescreen.
Let'sputitalltogetherandaddonemoreentityintothemixsothetwocaninteract:
localworld=love.physics.newWorld(0,100)
--Triangleisthenameofourfirstentity
localtriangle={}
triangle.body=love.physics.newBody(world,200,200,'dynamic')
--Givethetrianglesomeweight
triangle.body.setMass(triangle.body,32)
triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)
triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)
--Anotherentity
localbar={}
bar.body=love.physics.newBody(world,200,450,'static')
bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)
bar.fixture=love.physics.newFixture(bar.body,bar.shape)
localkey_map={
escape=function()
love.event.quit()
end
}
love.draw=function()
love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))
love.graphics.polygon('line',bar.body:getWorldPoints(bar.shape:getPoints()))
end
love.keypressed=function(pressed_key)
--Checkinthekeymapifthereisafunction
2.7-Theworld
69
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
love.update=function(dt)
world:update(dt)
end
Thisisalottodigestsodon'thesitatetore-readthroughthiscodeseveraltimesifnecessary.Therewerealotofnewfunctionsintroducedinthissection,sointhenextsectionwe'lltakeadeeperlookatthedocumentationandreadmoreaboutthemandtheircomponents.
ExercisesTrychangingthemass( triangle.body:setMass)andgravity( love.physics.newWorld)andseewhathappens.
2.7-Theworld
70
ReadingdocumentationYoutypicallyrunintotwotypesofdocumentationforsoftware:guidesandAPIdocumentation.Guideswouldbeinformationongettingstarted,tutorials,andbooks.AnAPI(applicationprogramminginterface)isaportionofsoftwarethataprogrammerwritesforhis/herprogramtoallowfellowprogrammerstointeractwiththeirapplication.AsforLÖVE'sprogramminginterface,mostofyourinteractionswiththeframeworkaredonethroughthe loveglobalvariablethattheframeworkpurposelyexposes.APIdocumentationisthemostfundamentalformofsoftwaredocumentationbecausewithoutit,youwouldnotknowwhatallthefunctionsintheprogramdounlessyouwereresourcefulenoughtogoinandstudyallthesourcecodeandfigureeachfunctionoutonyourown.
ThedocumentationforLÖVE(bookmarkthis!)iswritteninawikistylewhereeachtableandfunctionhasanarticlethatdescribeshowtouseit.Fromhereyoushouldseemanymoduleslisted.Clickon love.physics.Again,love.physicsisjustatablewithfunctionsinit.Sowithinthearticleweseeeachofthefunctionsstoredinitincludingthefunctions love.physics.newBodywhichweusedtocreateourentities'bodies, love.physics.newPolygonShapewhichweusedtocreatetheirshapes,and love.physics.newFixturewhichweusedtocreatetheirfixtures.Wealsosee love.physics.newWorldwhichcreatedourworldtable.Let'slookatthefirstfunction'sarticle.
Clickingonthearticlefor love.physics.newBodywegetasynopsisshowinghowthefunctionmightbeused,alongwithadescriptionofitsparametersandwhatthefunctionreturns.Overthecourseofdifferentversionsoftheframework,functionsmaybemodifiedsointhecaseofthisfunctionyoucanseeexamplesofhowtouseitdifferentlyindifferentversionsofLÖVE.Sincethefunctionliststhatitreturnsabody,clickthelinktogoovertothearticleonthebodytable'sdocumentation.
There'salotoffunctionsherethatsetpropertiesonthebodyandgetpropertiesfromthebody.Oneofthemisthebody:setMassfunctionweusedtogiveourentityweight.Wecanseethatittakesoneparameterandthattheparameterismeanttosimulatehowmanykilogramsofmassthebodyhas.Weoriginallytolditthatourtriangleinthelastsectionweighed32kilograms,whichifyouthinktheobjectfelltoofastortooslowthenyoumayneedtoadjustyourworld'sgravitytomatchyourexpectation.
Nowlet'sgobackto love.physicsforamomentandtakealookatoneoftheothercomponentsweaddedtoourpreviouscode,thefixture.Wepreviouslycreatedafixturetablebycalling triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape).However,wehaven'tseenanyofthesefunctionsinthefixturetablethatcouldcomeinhandy.Forinstance,wecouldgiveourtrianglebouncinessbyinvokingfixture:setRestitution.Ourtrianglefixtureisnamed triangle.fixturethough,not fixture.If0isnobounciness(default)and1is100%,trymodifyingthegamecodeandaddingarestitutionof75%:
triangle.fixture:setRestitution(0.75)
Tryrunningthegameandseehowthatworks.Ifyousettherestitutionto 1orhigherthenthetrianglewon'tstopbouncingandwillbounceitselfrightoffthescreen.
CallbacksLet'sbacktracknowtothemainarticleaboutthelovetable.Ifyouscrolldownalittleonthepage,you'llseeasectiontitled"Callbacks"thatcontainssomefunctionswe'vebecomefamiliarwithsuchas love.drawand love.update.Thisisalistofallthefunctionsinthegameloopthatwehaveandhaven'ttalkedaboutyet.A"callback"isafunctionyoucreateandgivetoanAPI(the lovetableinthiscase)thatwilllatergetinvokedasneeded.Creatingfunctionswiththesenamesallowyoutotapintospecificportionsofthegameloopandtriggeryourownevents.
2.8-Readingdocumentation
71
Let'stakealookatthe love.keypressedcallbackforinstance.Inthesynopsisyouseethatithas3parameters(thedocumentationmistakenlycallsparameters"arguments").Weusedthefirstparameter keytoseewhatkeywaspressed.Ifyoueverneedtoknowwhatkeysareavailable,youcanclickonthelinkprovidednexttotheparametername, KeyConstanttoseeawell-definedlistofalltheavailablekeystringspassedintothisparameter.Thesecondparameter scancodewedidn'ttalkabout,butithasawell-defined Scancodearticleexplainingwhatitis.Ifyouarenotfamiliarwithscancodes,takeaminutetoreaditandperhapsyou'lllearnaboutafeatureyoumaywanttouseinyourgame.
Onemorecallbackwe'lllookatwhilewe'rehereis love.focus.Takeamomenttostophereandreadwhatitdoesandwhatparametersittakesbeforecontinuing.Nowitwouldbereallycoolifweweremakingagameandwhentheuserswitchedtoanotherapplicationwindow,thegameautomaticallypausedfortheuser.Sofirstlet'sstartbyimplementingapausefeatureinourearliergamecode:
localworld=love.physics.newWorld(0,9.81*128)
--Triangleisthenameofourfirstentity
localtriangle={}
triangle.body=love.physics.newBody(world,200,200,'dynamic')
--Givethetrianglesomeweight
triangle.body.setMass(triangle.body,32)
triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)
triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)
triangle.fixture:setRestitution(0.75)
--Anotherentity
localbar={}
bar.body=love.physics.newBody(world,200,450,'static')
bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)
bar.fixture=love.physics.newFixture(bar.body,bar.shape)
--Booleantokeeptrackofwhetherourgameispausedornot
localpaused=false
localkey_map={
escape=function()
love.event.quit()
end,
space=function()
paused=notpaused
end
}
love.draw=function()
love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))
love.graphics.polygon('line',bar.body:getWorldPoints(bar.shape:getPoints()))
end
love.keypressed=function(pressed_key)
--Checkinthekeymapifthereisafunction
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
love.update=function(dt)
ifnotpausedthen
world:update(dt)
end
end
Noticethe3changes:
Weaddedabooleancalled pausedandsetittofalse
2.8-Readingdocumentation
72
Weaddedanewfunctionto key_mapsothatwhen "space"ispressed,thevalueof pausedissetto notpaused. notisanoperatorforbooleanswepreviouslydidn'tdiscuss.Itsimplysays"theoppositeofthisboolean".Soif pausedis true,thensetting pausedto notpausedwillsetitto false.Lastly,inside love.updatetotoldtheworldtoupdateonlyifweare notpaused.Sothepassageoftimeinthegameworldwillceasewhenpressingthespacekey.
ExercisesNowwiththedocumentationinhand,define love.focusandmakeitsothegamepauseswhentheuserclicksawayfromthegamewindow.Bonus:makethegameprintatextsayingthatthegameispausedwhen pausedis true.Gofindthedocumentationfor love.graphics.printtoseeanexampleondisplayingtext.
2.8-Readingdocumentation
73
ModulesandorganizationEventuallywhenyoustartwritingrealprograms,yourealizeifyoukeepallthecodeinonefilethatthingscangetabitmessy.Puttingyourcodeinseparatefileshelpsyounotonlykeepyourdifferentpiecesofcodeseparatedfromeachother,butithelpsyouvisualizethestructureofyourprogram.
Let'sstartwithasinglemain.luafileandwe'llthensplititintodifferentfiles:
localmy_cool_functon=function()
love.graphics.print('Thisfunctioncamefromfunction-module.lua',100,100,0,2)
end
localmy_cool_table={}
my_cool_table.print_stuff=function()
love.graphics.print('Thisfunctioncamefromtable-module.lua',100,200,0,2)
end
print('my_cool_functionis',my_cool_function)
print('my_cool_tableis',my_cool_table)
Whenwegotorunthecode,wegetablankwindowbecausewe'renotdrawinganything.Wedoseeourprintstatementsoutputourfunctionandtabletotheconsolethough:
my_cool_functionisfunction:0x41f2abc0
my_cool_tableistable:0x41f2aa08
Modulesand requireThinkofyourLuafilesasgiantfunctionsthatgetinvokedwheneveryouloadthefile.Justlikeafunction,youcanreturnvaluesfromyourfiles.IfyouloadoneLuafilefromanother,youwillgetwhatevervalueisreturned.Let'smodifyourpreviouscode.Firstdefinethesetwonewfilesinthegamefolder:
function-module.lua
returnfunction()
love.graphics.print('Thisfunctioncamefromfunction-module.lua',100,100,0,2)
end
table-module.lua
localmy_cool_table={}
my_cool_table.print_stuff=function()
love.graphics.print('Thisfunctioncamefromtable-module.lua',100,200,0,2)
end
returnmy_cool_table
Thenupdatemain.lua:
localfunction_module=require('function-module')
localtable_module=require('table-module')
print('function_moduleis',function_module)
print('table_moduleis',table_module)
2.9-Modulesandorganization
74
Let'sstartfromthetop.Infunction-module.luawewriteareturnstatementthatreturnsafunctionwithnoname.Wedon'tinvokethefunction,wejustreturnitasavaluethesamewayafunctionmayreturnanumberorstring.Likewiseintable-module.luawedefinedatable(withafunctioninit)andreturnedthetableonthelastlineofthefile.Thefunctionnameandlocalvariablename my_cool_tableisinconsequentialandcan'tbeseenoutsidethetable-module.luafileasmoduleshavetheirownscope.
Inmain.luawearerequiringfunction-moduleusingabuilt-inLuafunction, require. requiretakesoneargument,astringthatequalsthenameofyourfileandittheninvokesthatfileandreturnsbackafunctionwhichweassigntoanewvariable function_module.Wethendothesamethingfortable-module.lua.Werequireit,whichinvokesitandreturnsbackwhateverthatfilereturns.Inthiscaseisatable.Noticethatwhenwepassinthefilenamesasargumentswejustgivethefirstpartofthefilenamewithouttheextension".lua"attheend.ThisfunctionexpectsthatanyfileitisrequiringisaLuafile.
Afterwerequiredthefiles,weprintedthevaluesofthosevariables,soyoushouldseetheresultsoftheprintstatementsappearintheconsolelikebefore:
function_moduleisfunction:0x40479548
table_moduleistable:0x40479bc8
Wepulledinthereturnvaluesfromtheothertwofilesintoourmain.luafileandprintedthevalues,butsincewedidn'tinvokethefunctionsfromthosetwofilesthenwegotablankgamewindowwhenrunningtheprogram.Let'sdefinealove.drawinmain.lualikebeforeandinvokethefunctionswegotbackfrombothmodules:
localfunction_module=require('function-module')
localtable_module=require('table-module')
print('function_moduleis',function_module)
print('table_moduleis',table_module)
love.draw=function()
function_module()
table_module.print_stuff()
end
Wewereabletoinvokethefunctionsandusethereturneddataasifitwereallinthesamefile.
OrganizingmodulesItmayhelptoseearealexampleofusingmodulesinagame,solet'stakeourpreviousgamecodefrom2.8-Readingdocumentationandseehowwecanseparateoutfunctionality.Thefirstthingwedidinourgamewasdefineaworld,solet'sstartbyputtingourworld-relatedcodeinadedicatedfilenamedworld.lua:
--world.lua
localworld=love.physics.newWorld(0,9.81*128)
returnworld
Rememberthatyouneedthereturnstatementattheendofyourfilesorelsethecodewillreturn nilwhenyougotorequireitandthiscouldcauseallkindsofunexepctederrorswhenyourunit.Nextlet'screateafoldernamedentitiesthatwecankeepallourgameentitiesin.Weplanoncreatingmoreentitiessoitwillhelptokeepthemalltogether.Intheentitiesfolder,createafileandnameittriangle.lua.We'llcutallthecodefromtheoriginalmain.luathatrelatedtoourtriangleentityandputithere:
--entities/triangle.lua
2.9-Modulesandorganization
75
localworld=require('world')
localtriangle={}
triangle.body=love.physics.newBody(world,200,200,'dynamic')
triangle.body.setMass(triangle.body,32)
triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)
triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)
triangle.fixture:setRestitution(0.75)
returntriangle
Noticehowwearerequiringthe worldtablefromworld.lua,becauseweneedtoaccessthattableinthisentity'sfilesowecanaddtheentitytotheworld.Wealsoneedtodothesamethingasabovewiththebarentity:
--entities/bar.lua
localworld=require('world')
localbar={}
bar.body=love.physics.newBody(world,200,450,'static')
bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)
bar.fixture=love.physics.newFixture(bar.body,bar.shape)
returnbar
Nowourmain.luashouldonlycontainourkeymapand lovefunctions:
--main.lua
localbar=require('entities/bar')
localtriangle=require('entities/triangle')
localworld=require('world')
--Booleantokeeptrackofwhetherourgameispausedornot
localpaused=false
localkey_map={
escape=function()
love.event.quit()
end,
space=function()
paused=notpaused
end
}
love.draw=function()
love.graphics.polygon('line',triangle.body:getWorldPoints(triangle.shape:getPoints()))
love.graphics.polygon('line',bar.body:getWorldPoints(bar.shape:getPoints()))
end
love.focus=function(focused)
ifnotfocusedthen
paused=true
end
end
love.keypressed=function(pressed_key)
--Checkinthekeymapifthereisafunction
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
2.9-Modulesandorganization
76
love.update=function(dt)
world:update(dt)
end
Ourtwoentitiesandworldgetpulledintomain.luaandeverythingshouldrunexactlyasbefore.Onethingtonoteisthateventhoughwerequireworld.lua3timesinourcode,itisthesameworldandnot3copies.ThisisbecauseLuaknowstoonlyrunamodulethefirsttimeyourequireitandnotinvokeitagain.Onceitrunsthefirsttime,thereturnedresultsarestoredinmemoryforthenexttimeyoutrytorequireit.Wecanprovethisbyaddingaprintstatementtoworld.lua:
--world.lua
print("Thisistheworld")
localworld=love.physics.newWorld(0,9.81*128)
returnworld
Howmanytimesdoes "Thisistheworld"getprintedtotheconsole?
ExercisesTrycreatingtwonewmodules;Onethatreturnsastringandonethatreturnsanumber.
2.9-Modulesandorganization
77
CollisioncallbacksWhenwritingagamesuchasaplatformeryoumaywantsomethingspecialtohappenwhentwoobjectscollide.Ifit'sapowerup,forinstance,youmaywantthepoweruptodespawn(beremovedfromtheworld)ifaplayertouchesitandthengivetheplayeraspecialability(thinkMarioandmushrooms).Ifaplayerandanenemybumpintoeachother,youmaywanttheplayer'shealthtodecrement.Theworldtablehasamethodthatallowsyoutoprograminfunctionalitylikethisforwhentwoentitiescollide.Itdoesthisbyallowingyoutocreatecallbacksaswelearnedbefore,butthesecallbacksaretriggeredbefore,during,oraftercollision.TakealookatWorld:setCallbacks.
Ifyoulookattheparametersfor World:setCallbacks,youseeitcantakefourfunctions.Thedescriptionoftheseparametershelpsexplainwhenthefunctionswillbecalled. beginContactand endContactshouldbeselfexplanatory;Theyhappenatthepointwherecontactbeginsandendsinacollision,but preSolveand postSolvemaynotbeasobvious.Nonetheless,let'seditthepreviously-createdworld.luafileandwritesomecollisioncallbackstotestthisfunctionality.
--world.lua
localbegin_contact_counter=0
localend_contact_counter=0
localpre_solve_counter=0
localpost_solve_counter=0
localbegin_contact_callback=function()
begin_contact_counter=begin_contact_counter+1
print('beginContactcalled'..begin_contact_counter..'times')
end
localend_contact_callback=function()
end_contact_counter=end_contact_counter+1
print('endContactcalled'..end_contact_counter..'times')
end
localpre_solve_callback=function()
pre_solve_counter=pre_solve_counter+1
print('preSolvecalled'..pre_solve_counter..'times')
end
localpost_solve_callback=function()
post_solve_counter=post_solve_counter+1
print('postSolvecalled'..post_solve_counter..'times')
end
localworld=love.physics.newWorld(0,9.81*128)
world:setCallbacks(
begin_contact_callback,
end_contact_callback,
pre_solve_callback,
post_solve_callback
)
returnworld
Tryitout.Everytimeoneofthecallbacksisinvoked,itwillincrementitsownnumberby1thenprintamessagetotheconsoletellingyouhowmanytimesithasbeeninvoked.It'sclearrightawaythat pre_solve_callbackandpost_solve_callbackgetinvokedmanymoretimesthan begin_contact_callbackand end_contact_callbackinthissituation.
2.10-Collisioncallbacks
78
Unlessyou'veeditedthebehaviorofthetriangleentity,itwillbounceabit(becauseofthetriangle'srestitution).Onceitbouncesandneithercornerorsideistouchingthefloorunderneath,thecontactends.Thisprocessisrepeatedeverytimeitbounces.Oncethetrianglesettlesdownitwillslideabit,maybeevenalot...likeanairhockeypuck.Thisisbecauseourtriangleandbarhavenofrictionbetweenthemtopreventthat.Anyways,thisisgoodbecauseitallowsustoseethatwhilethetriangleisslidingitisstillmakingcontact.Whilethetriangleisslidingandstillmakingcontact,the pre_solve_callbackand post_solve_callbackwillcontinuetogetcalledwitheveryframeofmovement.
Pretendourtrianglewasafuturisticracecarmovingacrossaneonstripofroadthatrechargedthevehicle.Youcouldstartincreasingtheracecar'spowermeterinside begin_contact_callbackasthecarmakescontactwiththatsectionofroadandthenstopincreasingpowerwhen end_contact_callbackisinvoked.Thiscouldworkprettywell,butthentheplayermaytryparkingforamomentonthepowerstripandcontinuetogainhealthaslongastheywant.Soanotherapproachcouldbetoonlyincreasethepowermeterastheplayercontinuestomoveandmakecontactwiththeroad,increasinghealthby1pointeverytimethe post_solve_callbackfunctionisinvoked.
Youdon'tnecessarilyneedtouseallofthesecallbacks,soyoucouldjustpassinanemptyfunctionor niltoWorld:setCallbacksfortheargumentsyoudon'tneed.
Withoutknowingwhatentitiesarecolliding,thecollisioncallbacksaren'tveryuseful.Luckily,ourcallbackshaveparametersoftheirownthatwecanaccess.Let'smodifythecodeagainandcheckoutthoseparameters:
--world.lua
--Calledatthebeginningofanycontactintheworld.Parameters:
--{fixture}fixture_a-firstfixtureobjectinthecollision.
--{fixture}fixture_b-secondfixtureobjectinthecollision.
--{contact}contact-worldobjectcreatedonandatthepointof
--contact.Whenslidingalonganobject,theremaybeseveral.
--Seefurther:https://love2d.org/wiki/Contact
localbegin_contact_callback=function(fixture_a,fixture_b,contact)
print(fixture_a,fixture_b,contact,'beginningcontact')
end
localend_contact_callback=function(fixture_a,fixture_b,contact)
print(fixture_a,fixture_b,contact,'endingcontact')
end
localpre_solve_callback=function(fixture_a,fixture_b,contact)
print(fixture_a,fixture_b,contact,'abouttoresolveacontact')
end
localpost_solve_callback=function(fixture_a,fixture_b,contact)
print(fixture_a,fixture_b,contact,'justresolvedacontact')
end
localworld=love.physics.newWorld(0,9.81*128)
world:setCallbacks(
begin_contact_callback,
end_contact_callback,
pre_solve_callback,
post_solve_callback
)
returnworld
Thisshouldprintoutsomeinformationintheconsolesimilarto:
Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480beginningcollision
Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480abouttoresolveacontact
Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480justresolvedacontact
Fixture:0x561020bf8570Fixture:0x561020bf7350Contact:0x561020bf7480endingcollision
2.10-Collisioncallbacks
79
Fixture:0x561020bf8570isatextrepresentationofourfirstentity'sfixture.The 0x56...isthememoryaddressofthefixturetohelpidentifyit,althoughthisinformationstilldoesn'ttelluswhichentitythisfixturebelongsto.Wealsoprintedoutacontacttable,whichcontainsasetoffunctionsjustliketheentities.Thisinstanceofacontactprovidesinformationsuchaswherethecontacthappenedandhowmuchvelocitywasinvolved.
Let'sworkonmodifyingtheprintstatementssowecancollectmoreusefulinformationonthesecollisions.Thereisapairoffunctionsoneveryfixturethatlet'syousetanyarbitrarydatayouwantonthatfixtureandanotherfunctiontogetthatdatabackoutthefixture.Thesefunctionsarecalled Fixture:setUserDataand Fixture:getUserData.ThesefunctionscanbeusedtosetanameorIDonthefixturetohelpusidentifywhatentityitbelongsto.Wecanaccomplishthisbyfirstmodifyingourentityfilesandpassingsomestringsto Fixture:setUserData:
--entities/bar.lua
localworld=require('world')
localbar={}
bar.body=love.physics.newBody(world,200,450,'static')
bar.shape=love.physics.newPolygonShape(0,0,0,20,400,20,400,0)
bar.fixture=love.physics.newFixture(bar.body,bar.shape)
bar.fixture:setUserData('bar')
returnbar
--entities/triangle.lua
localworld=require('world')
localtriangle={}
triangle.body=love.physics.newBody(world,200,200,'dynamic')
triangle.body.setMass(triangle.body,32)
triangle.shape=love.physics.newPolygonShape(100,100,200,100,200,200)
triangle.fixture=love.physics.newFixture(triangle.body,triangle.shape)
triangle.fixture:setRestitution(0.75)
triangle.fixture:setUserData('triangle')
returntriangle
Nowgobacktotheworld'scollisioncallbacksandyoucaneasilyextractthisinformationbackoutofthefixtures:
--world.lua
--Calledatthebeginningofanycontactintheworld.Parameters:
--{fixture}fixture_a-firstfixtureobjectinthecollision.
--{fixture}fixture_b-secondfixtureobjectinthecollision.
--{contact}contact-worldobjectcreatedonandatthepointof
--contact.Whenslidingalonganobject,theremaybeseveral.
--Seefurther:https://love2d.org/wiki/Contact
localbegin_contact_callback=function(fixture_a,fixture_b,contact)
localname_a=fixture_a:getUserData()
localname_b=fixture_b:getUserData()
print(name_a,name_b,contact,'beginningcontact')
end
localend_contact_callback=function(fixture_a,fixture_b,contact)
localname_a=fixture_a:getUserData()
localname_b=fixture_b:getUserData()
print(name_a,name_b,contact,'endingcontact')
end
localpre_solve_callback=function(fixture_a,fixture_b,contact)
localname_a=fixture_a:getUserData()
2.10-Collisioncallbacks
80
localname_b=fixture_b:getUserData()
print(name_a,name_b,contact,'abouttoresolveacontact')
end
localpost_solve_callback=function(fixture_a,fixture_b,contact)
localname_a=fixture_a:getUserData()
localname_b=fixture_b:getUserData()
print(name_a,name_b,contact,'justresolvedacontact')
end
localworld=love.physics.newWorld(0,9.81*128)
world:setCallbacks(
begin_contact_callback,
end_contact_callback,
pre_solve_callback,
post_solve_callback
)
returnworld
Ah,nowwecanseewhichfixtureiscollidingwhich!
bartriangleContact:0x55bf29c07590beginningcontact
bartriangleContact:0x55bf29c07590abouttoresolveacontact
bartriangleContact:0x55bf29c07590justresolvedacontact
bartriangleContact:0x55bf29c07590endingcontact
ExercisesModifytheprintstatementsineachcollisioncallbacktoprintoutthecoordinateswheretheentities'fixturesaremakingcontact.Youcanfindtheinformationyouneedtodothisinthedocumentationforthecontacttablementionedabove.
2.10-Collisioncallbacks
81
Breakout(part1):moreentitypracticeLet'sbringalltheseconceptstogetherbymakinganothergame,abreakoutclone.Therequirementsareprettysimple:
TheobjectiveofthegameistodestroyallthebricksonthescreenTheplayercontrolsa"paddle"entitythathitsaballTheballdestroysthebricksTheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen(belowthepaddle),thegameends
Ifyoustillhavethecodefromtheprevioussections,feelfreetocopythefoldernamingthenewone"breakout"orwhateveryouwantyourbreakoutclonetobecalled.Attheendofthissectiontherewillbealinktoallthesourcecodetouseasareferenceincaseyougetstuck.Thismaybetimeconsuming,butIencourageyoutotypeouteachsectionandstoptounderstandwhatitisyouaretyping.Ifyoucopy,paste,anddon'treadthenitwillbeeasytogetlostinthischunkofthechapterasthingswillmovefast.
Thefirstmodificationwe'llmakeistosetaspecificwindowsizesonomatterwhichversionofLÖVEyou'reonwe'reworkingwiththesamewindowproportionsandentitydimensions.Todothis,openofconf.luaorcreateitifyoudon'thaveitandputinthefollowingcode:
--conf.lua
--LÖVEconfigurationfile
love.conf=function(t)
t.console=true--EnablethedebugconsoleforWindows.
t.window.width=800--Game'sscreenwidth(numberofpixels)
t.window.height=600--Game'sscreenheight(numberofpixels)
end
Theconf,orconfigurationfileletsyoudefineacallbackinthe lovetablethatmodifiesthegameengine'sconfigurationonload.Youcanreadmoreaboutalltheinterestingthingsyoucandowithitherebutmostofitsfeatureswon'tbenecessaryforoursimplegame.
Thenextmodificationwe'llmakeisdeletingtheentitiesfromthelastsection.Let'screatenewentitiestorepresenttheballandpaddle:
--entities/ball.lua
localworld=require('world')
localentity={}
entity.body=love.physics.newBody(world,200,200,'dynamic')
entity.body:setMass(32)
entity.body:setLinearVelocity(300,300)
entity.shape=love.physics.newCircleShape(0,0,10)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setRestitution(1)
entity.fixture:setUserData(entity)
returnentity
--entities/paddle.lua
localworld=require('world')
2.11-Breakout(part1)
82
localentity={}
entity.body=love.physics.newBody(world,200,560,'static')
entity.shape=love.physics.newRectangleShape(180,20)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
returnentity
Beforewetryandrunanything,takealookatafewthingswe'vedonedifferentlyindefiningtheseentitiesthanwe'vepreviouslydone.
Inball.luawearedefiningacircleshapeinsteadofapolygon.Thismeanswehavenosidesorcornerpointswecanreferencewhenspawningortrackingthepositionofthisobject.Circleshavetobetrackedfromtheircenterpointandtheirboundariesbytheirradius.Inthisfilewe'reusing Body:setLinearVelocitytoapplymovementontheballinaspecificdirectionwhentheentityspawns.Inpaddle.luawearedefiningapolygonshape,butinsteadofspecifyingeachpointweareusingthelove.physics.newRectangleShapefunctiontodefinetheshape.Thiswillstillgenerateapolygonasbefore,butinsteadofspecifyingeachpointintheshapewearegivingaheightandwidthandallowingittofigureouttheshapewewantbasedonthosetwoparameters.Thinkofitasashortcutversionofthelove.physics.newPolygonShapefunction.Thepaddlehasastaticbodywhiletheballisdynamic.Whatthisentailsistheballwillbeaffectedbythepaddlebutthepaddlewon'tbeaffectedbytheball.Eventhoughthepaddleisstatic,itcanbemanuallyrepositionedaswe'lldolaterwithbuttons.Inbothentityfiles,wearepassingthefullentitytableasthefixtureuserdatainsteadofjustastringnamelikebefore.Thiswillallowustoeasilyaccesstheentireentityinsidethecollisioncallbackaswe'llseelater.You'llwanttogobackandcomparethatcodefromtheCollisionCallbackssectiontotheseentities,butdon'tworryifitdoesn'tmakecompletesenseyet.
Nowweneedtomodifymain.luatoloadupournewentities:
--main.lua
localpaddle=require('entities/paddle')
localball=require('entities/ball')
localworld=require('world')
--Booleantokeeptrackofwhetherourgameispausedornot
localpaused=false
localkey_map={
escape=function()
love.event.quit()
end,
space=function()
paused=notpaused
end
}
love.draw=function()
localball_x,ball_y=ball.body:getWorldCenter()
love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())
love.graphics.polygon(
'line',
paddle.body:getWorldPoints(paddle.shape:getPoints())
)
end
love.focus=function(focused)
ifnotfocusedthen
paused=true
2.11-Breakout(part1)
83
end
end
love.keypressed=function(pressed_key)
--Checkinthekeymapifthereisafunction
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
love.update=function(dt)
ifnotpausedthen
world:update(dt)
end
end
Takenoteofafewthingswe'redoinghere:
Fordrawingthecircle,weneedtoinvoke love.graphics.circle.Fordrawingthepaddle,westillinvoke love.graphics.polygonastherectangleisstillapolygonshape.
Nowlet'sremoveanyprintstatementsinworld.luajusttocleanthingsup.We'llleavethecallbackstheresincewemayusethemlaterbutwe'llleavethememptyfornow.We'llalsosetthegravityto 0becausewewanttheballtobouncefreelylikeintherealBreakoutgameandnotloseanymomentum.
--world.lua
--Calledatthebeginningofanycontactintheworld.Parameters:
--{fixture}fixture_a-firstfixtureobjectinthecollision.
--{fixture}fixture_b-secondfixtureobjectinthecollision.
--{contact}contact-worldobjectcreatedonandatthepointof
--contact.Whenslidingalonganobject,theremaybeseveral.
--Seefurther:https://love2d.org/wiki/Contact
localbegin_contact_callback=function(fixture_a,fixture_b,contact)
end
localend_contact_callback=function(fixture_a,fixture_b,contact)
end
localpre_solve_callback=function(fixture_a,fixture_b,contact)
end
localpost_solve_callback=function(fixture_a,fixture_b,contact)
end
localworld=love.physics.newWorld(0,0)
world:setCallbacks(
begin_contact_callback,
end_contact_callback,
pre_solve_callback,
post_solve_callback
)
returnworld
Whathappensifyourunthegamenow?Theballfliesrightoffthescreenwithoutconsequence.Thereareacoupledifferentwaysofpreventingtheballfrommovingoffscreen.Possiblythemostsimpleapproachistoputupsomewalls.Canyouguesswhatthecodetothosewallsmaylooklike?Yup,theywillbeentitiessimilartothepaddleexceptthattheyjustsitattheedgesofthescreen.Let'screatesomeentitiesforthatpurpose:
--entities/boundary-top.lua
2.11-Breakout(part1)
84
localworld=require('world')
localentity={}
entity.body=love.physics.newBody(world,400,5,'static')
entity.shape=love.physics.newRectangleShape(800,10)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
returnentity
Takealookatthesenumbersforaminute.Forthelocationofthebodywespecified400pixels.Sostartingfromthetopleftcornerandmovingrightalongthex-axiswe'vespecifiedtheverycenterofan800-pixel-widewindow.Thereasonwe'vedonethisisbecausewewantthetopandbottomwallboundariestostretch800pixelswide,theentirelengthofthewindow,andwhencalling newBodyandspawninganentity'sbodyitwillspawnthecenterpointoftheentityshapeatthatlocation.Notallentityshapesaresquare,orevenpolygonal,soitissimplestforthegameenginetocentertheshapeonthebody'sspawnpointratherthanusinganotherpointofreferenceontheshape,likethetopleftcorneroftheshape(notallshapeshavecorners).Infact,theballandpaddlespawnedcenteredonthelocationwegavefortheirbodies.
Sowemadethewalls800pixelswideandjusttogiveitalittlevisibilitywemadethem10pixelstall.Youwouldthinkwe'dspawnthewallattheverytopofthescreen(0pixelsonthey-axis,)butsinceourwallswillbecenteredtothespawnpointsweshouldmovedownhalftheheightofthewallifwewantitalltoappearonscreen.
Nowtheboundaryonthebottomwillhavethesamedimensions,butitwillbespawnedatthebottomofthescreen(600pixels)minushalftheheightofthewall(5pixels):
--entities/boundary-bottom.lua
localworld=require('world')
localentity={}
entity.body=love.physics.newBody(world,400,595,'static')
entity.shape=love.physics.newRectangleShape(800,10)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
returnentity
Theleftandrightboundarieswillfollowthesamepatternexcepttheywillbetheheightofthescreeninsteadofthewidthofthescreen:
--entities/boundary-left.lua
localworld=require('world')
localentity={}
entity.body=love.physics.newBody(world,5,300,'static')
entity.shape=love.physics.newRectangleShape(10,600)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
returnentity
--entities/boundary-right.lua
localworld=require('world')
localentity={}
entity.body=love.physics.newBody(world,795,300,'static')
entity.shape=love.physics.newRectangleShape(10,600)
2.11-Breakout(part1)
85
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
returnentity
Wewon'tseetheseentitiesuntilwerequirethemanddrawthemonthescreen.Somodifymain.luatorequireanddrawthemthesamewaywedotheballandpaddle:
--main.lua
localboundary_bottom=require('entities/boundary-bottom')
localboundary_left=require('entities/boundary-left')
localboundary_right=require('entities/boundary-right')
localboundary_top=require('entities/boundary-top')
localpaddle=require('entities/paddle')
localball=require('entities/ball')
localworld=require('world')
--Booleantokeeptrackofwhetherourgameispausedornot
localpaused=false
localkey_map={
escape=function()
love.event.quit()
end,
space=function()
paused=notpaused
end
}
love.draw=function()
love.graphics.polygon('line',boundary_bottom.body:getWorldPoints(boundary_bottom.shape:getPoints()))
love.graphics.polygon('line',boundary_left.body:getWorldPoints(boundary_left.shape:getPoints()))
love.graphics.polygon('line',boundary_right.body:getWorldPoints(boundary_right.shape:getPoints()))
love.graphics.polygon('line',boundary_top.body:getWorldPoints(boundary_top.shape:getPoints()))
localball_x,ball_y=ball.body:getWorldCenter()
love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())
love.graphics.polygon('line',paddle.body:getWorldPoints(paddle.shape:getPoints()))
end
love.focus=function(focused)
ifnotfocusedthen
paused=true
end
end
love.keypressed=function(pressed_key)
--Checkinthekeymapifthereisafunction
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
love.update=function(dt)
ifnotpausedthen
world:update(dt)
end
end
2.11-Breakout(part1)
86
Whenyourunthegame,youshouldseeprettymuchthesamethingasthis:
Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-1
Lookingbackatourlistofminimalrequirementswe'vealreadycompletedonethingonourlist:
Theballneedstostaywithintheboundariesofthescreen
There'sstillquiteabitmoreworktocompletethislistsolet'scontinueinthenextsection.
ExercisesMaybeitwouldbebetteriftheboundarylineswereevenwiththescreensowecouldn'tseethem.Modifytheboundarypositionssoitlooksliketheballisbouncingofftheedgeofthescreen.Whathappensifwe requiretheboundariesbutdon'tdrawthemin love.draw?Doesthegamestillwork?
2.11-Breakout(part1)
87
Breakout(part2):entitymanagement
ReviewIntheprevioussectionwemadeachecklistofrequirementsandaccomplishedoneofthem:
TheobjectiveofthegameistodestroyallthebricksonthescreenTheplayercontrolsa"paddle"entitythathitsaballTheballdestroysthebricks✔TheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen,thegameends
Inthepreviousexercise,thegoalwastomovetheboundariessotheywerejustoffscreen.Thisgivestheeffectthattheballisbouncingofftheedgesofthegamewindow.
--entities/boundary-bottom.lua
entity.body=love.physics.newBody(world,400,606,'static')
--entities/boundary-left.lua
entity.body=love.physics.newBody(world,-6,300,'static')
--entities/boundary-right.lua
entity.body=love.physics.newBody(world,806,300,'static')
--entities/boundary-top.lua
entity.body=love.physics.newBody(world,400,-6,'static')
Heretheyhavebeenmoved6pixelsoffscreenjusttouseevennumbersandmakecalculationeasier.Previouslywealsoraisedthequestionofwhetherornottheboundarieswouldworkifwestill require'dtheminmain.luabutdidn'tdrawthemin love.draw.Theansweristheystillworkbutwedon'tseethem.Sincetheyareoffscreen,thatdoesn'tmatteranywayandwecansaveourprogramfromdoingextrawork:
--main.lua
love.draw=function()
localball_x,ball_y=ball.body:getWorldCenter()
love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())
love.graphics.polygon('line',paddle.body:getWorldPoints(paddle.shape:getPoints()))
end
EntitylistLet'sthinkabouttheproblemofbrickentitiesforaminute.Wecouldcreateanentityfileforeachbrick,buttheyaremoreorlessthesameexceptthattheyspawnindifferentspots.Imaginemaking50differententityfilesandtheninside love.drawmaking50linestodraweachbrickandsoon.Whatwecaninsteaddoismakeanentityfilefor1brickthenmakealistwith50copiesofit(orhowevermanybrickcopiesweendupfittingonthescreen).Wecanthenloopoverthislisttodrawthebricks.
Let'sfirstcreatethebrickentityfile:
2.12-Breakout(part2)
88
--entities/brick.lua
localworld=require('world')
returnfunction(x_pos,y_pos)
localentity={}
entity.body=love.physics.newBody(world,x_pos,y_pos,'static')
entity.shape=love.physics.newRectangleShape(50,20)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
returnentity
end
Insteadofreturninganentityinthisfile,wereturnedafunctionthattakesanx-positionandy-positionasparameters.Whenthefunctiongetsinvokedwhereveritisrequired,itwillgenerateanewentitywiththosecoordinatesforitsspawnpoint.Here'showwecanuseit:
--main.lua
localboundary_bottom=require('entities/boundary-bottom')
localboundary_left=require('entities/boundary-left')
localboundary_right=require('entities/boundary-right')
localboundary_top=require('entities/boundary-top')
localpaddle=require('entities/paddle')
localball=require('entities/ball')
localbrick=require('entities/brick')
localentities={
brick(100,100),
brick(200,100),
brick(300,100)
}
localworld=require('world')
--Booleantokeeptrackofwhetherourgameispausedornot
localpaused=false
localkey_map={
escape=function()
love.event.quit()
end,
space=function()
paused=notpaused
end
}
love.draw=function()
localball_x,ball_y=ball.body:getWorldCenter()
love.graphics.circle('fill',ball_x,ball_y,ball.shape:getRadius())
love.graphics.polygon('line',paddle.body:getWorldPoints(paddle.shape:getPoints()))
for_,entityinipairs(entities)do
love.graphics.polygon('fill',entity.body:getWorldPoints(entity.shape:getPoints()))
end
end
love.focus=function(focused)
ifnotfocusedthen
paused=true
end
end
love.keypressed=function(pressed_key)
2.12-Breakout(part2)
89
--Checkinthekeymapifthereisafunction
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
love.update=function(dt)
ifnotpausedthen
world:update(dt)
end
end
Wemadeanentitytablewithalistofbrickentitiesinit,thenin love.drawwemadeaforlooptodraweachentityinthelist.Beforewechangeanythingelsetryrunningthegameandtakingalookthatthebricksappearandthateverythingworks.
RuleofsingleresponsibilityOurgoalfortherestofthissectionwillbetosimplifyentitymanagement.Onestrategywe'llhavefordoingthisistothinkofeachfileinourgameashavingasingleresponsibility.Agoodsignthatwe'redoingthisismain.luaisverysmallandeasytoscanoverwiththeeyesanddigest.
Sowhatistheresponsibilityofmain.lua?
Createthecallbackfunctionsnecessarytorunthegame.
Here'ssomethingsitisdoingthatdon'tfitthatresponsibility:
LoadandstorealltheentitiesFigureouthowtodraweachtypeofentityin love.drawStoreamapofkeypresses
Imagineourgameisanorganizationandeachfileisaroleinthecompany.Ourmainfileislikethesecretarythatknowshowtohandlerequestsfromoutsiders.Ifsomebodycalledaskingthesecretaryaboutbuilding-maintenanceissues,thesecretarywouldn'tgrabplumbingtoolsandtakecareoftheproblembutratherdispatchthepersonwhoseresponsibilityisthatexactkindofproblem.Astheownerofthisorganizationweshouldknoweveryone'srolessoit'seasytoknowwhereeachresponsibilitylies.Itwillmakeiteasierforustogrowthecompanytothesizewedesire.
Oneeasyimprovementistonotwriteoutalltheinstructionsfordrawingeachentitywithinthemainfile,butratherleteachentityfileberesponsibleforeveryfeatureofthatentity,includinghowtodrawthatentity.Wemaywanttogetfancylateranddrawbricksindifferentcolors,forinstance.Thatcouldgetcomplicatedandwedon'twantthemainfiletoretainabunchofcodeaboutbrickcolorsandsuch.
Modifyingtheentitiesisaseasyascreating drawfunctionsintheentitytables:
--entities/brick.lua
localworld=require('world')
returnfunction(x_pos,y_pos)
localentity={}
entity.body=love.physics.newBody(world,x_pos,y_pos,'static')
entity.shape=love.physics.newRectangleShape(50,20)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
entity.draw=function(self)
love.graphics.polygon('fill',self.body:getWorldPoints(self.shape:getPoints()))
end
2.12-Breakout(part2)
90
returnentity
end
--entities/paddle.lua
localworld=require('world')
returnfunction(pos_x,pos_y)
localentity={}
entity.body=love.physics.newBody(world,pos_x,pos_y,'static')
entity.shape=love.physics.newRectangleShape(180,20)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
entity.draw=function(self)
love.graphics.polygon('line',self.body:getWorldPoints(self.shape:getPoints()))
end
returnentity
end
--entities/ball.lua
localworld=require('world')
returnfunction(x_pos,y_pos)
localentity={}
entity.body=love.physics.newBody(world,x_pos,y_pos,'dynamic')
entity.body:setMass(32)
entity.body:setLinearVelocity(300,300)
entity.shape=love.physics.newCircleShape(0,0,10)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setRestitution(1)
entity.fixture:setUserData(entity)
entity.draw=function(self)
localself_x,self_y=self.body:getWorldCenter()
love.graphics.circle('fill',self_x,self_y,self.shape:getRadius())
end
returnentity
end
Goaheadandmakealltheentitiesreturnafunctionwith x_posand y_posparametersandwe'lljustaddeverythingtotheentitylistlikethebricks.Don'tforgettochangeoutthenumbersinthe love.physics.newBody(world,200,200,'dynamic')withtheargumentsbeingpassedinbythefunction: love.physics.newBody(world,x_pos,y_pos,'dynamic').Fortheboundariesentityfilesthereisnoneedfor entity.drawfunctions,butstillmakethemreturnfunctionswiththetwoparameters.Nowupdatethe entitieslistinmain.luatoincludealltheentities:
--main.lua
localboundary_bottom=require('entities/boundary-bottom')
localboundary_left=require('entities/boundary-left')
localboundary_right=require('entities/boundary-right')
localboundary_top=require('entities/boundary-top')
localpaddle=require('entities/paddle')
localball=require('entities/ball')
localbrick=require('entities/brick')
localentities={
boundary_bottom(400,606),
boundary_left(-6,300),
2.12-Breakout(part2)
91
boundary_right(806,300),
boundary_top(400,-6),
paddle(300,500),
ball(200,200),
brick(100,100),
brick(200,100),
brick(300,100)
}
localworld=require('world')
--Booleantokeeptrackofwhetherourgameispausedornot
localpaused=false
localkey_map={
escape=function()
love.event.quit()
end,
space=function()
paused=notpaused
end
}
love.draw=function()
for_,entityinipairs(entities)do
ifentity.drawthenentity:draw()end
end
end
love.focus=function(focused)
ifnotfocusedthen
paused=true
end
end
love.keypressed=function(pressed_key)
--Checkinthekeymapifthereisafunction
--thatmatchesthispressedkey'sname
ifkey_map[pressed_key]then
key_map[pressed_key]()
end
end
love.update=function(dt)
ifnotpausedthen
world:update(dt)
end
end
Takealookatour love.drawfunction.Itismuchsimplernowthatitnolongerneedstoknowhowtodraweachentity.Itjustaskstheentityifitknowshowtodrawitselfandifitdoesittellsittodoso.Rememberthatinvokingentity:draw()isjustshorthandforwriting entity.draw(entity)becauseofthe :.
Ok,butputtingtheentitiesinalistdidn'tcleanupthisfile.Nowthisfileisresponsibleforknowingwheretospawntheentitiesandhavingtheminalistjustmakesthisfilebigger.Wellyouseethereasonweputtheminalistisbecausewewanttomakeanewgamefilecalledentities.luathatwillberesponsibleforloading,spawning,andstoringalltheentitieswhenthegamestartsup.Createanewfilethencutalltheentity requirestatementsandtheentitylistandpasteitinthenewfile:
--entities.lua
localboundary_bottom=require('entities/boundary-bottom')
localboundary_left=require('entities/boundary-left')
localboundary_right=require('entities/boundary-right')
2.12-Breakout(part2)
92
localboundary_top=require('entities/boundary-top')
localpaddle=require('entities/paddle')
localball=require('entities/ball')
localbrick=require('entities/brick')
return{
boundary_bottom(400,606),
boundary_left(-6,300),
boundary_right(806,300),
boundary_top(400,-6),
paddle(300,500),
ball(200,200),
brick(100,100),
brick(200,100),
brick(300,100)
}
Andnowthetopofourmainfileonlyneedstoloadtheentitiesfileanditwillhavethelisttousein love.drawandelsewhereasneeded:
--main.lua
localentities=require('entities')
localworld=require('world')
Whenyourunthegame,youshouldbeseeingsomethingsimilartothis:
Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-2
2.12-Breakout(part2)
93
Andthat'saboutitforentitymanagement.We'llfigureouthowtohandlekeypressesforthepaddleandeverythingelseinthenextsection.We'llfinishthecleanupinourmainfilewhilewe'reatit.
ExercisesNowthatourentitieshavepassedoffknowledgeonwheretheyspawnovertoentities.lua,ourleftandrightboundariesareidenticalfiles.Replaceboundary-left.luaandboundary-right.luawithasingleboundary-vertical.luafileandspawntwocopiesofthatinentities.lua.Ifyougetstuck,checkouttheentities.luafileinthenextsectionforhowthisisdone.
2.12-Breakout(part2)
94
Breakout(part3):inputs
ReviewIntheprevioussectionwereconstructedourentitiestomakeroomforbricksandadditionalfunctionality.Wehaven'tcompletedanynewitemsonourchecklist:
TheobjectiveofthegameistodestroyallthebricksonthescreenTheplayercontrolsa"paddle"entitythathitsaballTheballdestroysthebricks✔TheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen,thegameends
Solet'scomeupwithasystemtohandleuserinputandgetthepaddlemoving.
InputserviceInsidemain.luathereissomefunctionalityforthisthatwearegoingtoremoveandrewritestartingwithanewfilethatspecificallyhandlesalltheuserinput.Thiskindoffileistypicallycalledaservicebecauseitabstractsawaytediousfunctionalityintoaneasy-to-useservice.Iencourageyoutowriteouttheserviceinsteadofcopyingandpasting.Readthrougheachfunctionandtrytounderstandwhateachonedoes.
--input.lua
--Thistableistheserviceandwillcontainsomefunctions
--thatcanbeaccessedfromentitiesorthemain.lua.
localinput={}
--Mapspecificuserinputstogameactions
localpress_functions={}
localrelease_functions={}
--Formovingpaddleleft
input.left=false
--Formovingpaddleright
input.right=false
--Keeptrackofwhethergameispause
input.paused=false
--Lookupinthemapforactionsthatcorrespondtospecifickeypresses
input.press=function(pressed_key)
ifpress_functions[pressed_key]then
press_functions[pressed_key]()
end
end
--Lookupinthemapforactionsthatcorrespondtospecifickeyreleases
input.release=function(released_key)
ifrelease_functions[released_key]then
release_functions[released_key]()
end
end
--Handlewindowfocusing/unfocusing
input.toggle_focus=function(focused)
ifnotfocusedthen
input.paused=true
end
end
2.13-Breakout(part3)
95
press_functions.left=function()
input.left=true
end
press_functions.right=function()
input.right=true
end
press_functions.escape=function()
love.event.quit()
end
press_functions.space=function()
input.paused=notinput.paused
end
release_functions.left=function()
input.left=false
end
release_functions.right=function()
input.right=false
end
returninput
Theinputtableiswhatgetsreturned,meaningwhenwe require('input')inanotherfile,wegetbackthattableanditscontents.Insidetheinputtherearethreebooleanpropertiesthatgettoggledbyuserinput: input.left,input.right,and input.paused.Alongwiththesethreeproperties,therearethreefunctionsexposedtoustomakeuseof: input.press, input.release,and input.toggle_focus,allofwhichwewillinvokefromourcallbacksinmain.lua:
--main.lua
localentities=require('entities')
localinput=require('input')
localworld=require('world')
love.draw=function()
for_,entityinipairs(entities)do
ifentity.drawthenentity:draw()end
end
end
love.focus=function(focused)
input.toggle_focus(focused)
end
love.keypressed=function(pressed_key)
input.press(pressed_key)
end
love.keyreleased=function(released_key)
input.release(released_key)
end
love.update=function(dt)
ifnotinput.pausedthen
for_,entityinipairs(entities)do
ifentity.updatethenentity:update(dt)end
end
world:update(dt)
end
end
2.13-Breakout(part3)
96
In love.updateweskipupdatesif input.pausedis true.Howeverifthegameisnotpausedthenitwillloopthroughtheentitylist,calling entity.updateiftheentityhasanupdatefunction.Withthisaddedfunctionality,wecanappendan entity.updatefunctionintoourexistingpaddlecode:
--entities/paddle.lua
entity.update=function(self)
--Don'tmoveifbothkeysarepressed.Justreturn
--insteadofgoingthroughtherestofthefunction.
ifinput.leftandinput.rightthen
return
end
localself_x,self_y=self.body:getPosition()
ifinput.leftthen
self.body:setPosition(self_x-10,self_y)
elseifinput.rightthen
self.body:setPosition(self_x+10,self_y)
end
end
Theleftandrightarrowswillnowmovethepaddle!Thereisn'tmuchelsetosayhereinthewayofinput.Abitunrelatedtotheactualinput,butmoresothepaddlefunctionalityisitmovesoffscreenanddoesn'tadheretotheboundaries?Whyisthat?
Ifyourememberwhenwecreatedthepaddle,itisastaticentity.Itdoesn'thavetheabilitytomoveonitsownorbytheeffectofotherentities.Thiswillcauseussomeproblemslater(andwe'relearningthehardway)!Ratherthanforcingthepaddlewithaninvisiblepush,weforceanewpositionforthepaddlewhenwecall body:setPositioninsidethepaddle's entity.updatefunction.It'slikewe'reteleportingitontopofwhateverspacewewantwithakeystroke,ignoringallphysicsandcollision.Thisissimplertocodeandgetsaroundthefactthepaddle'sstaticbodywon'trespondtoforce.Tofixthis,wecanartificiallysettheboundaryonthepaddlebycheckingifitisoutofboundsbeforemovingit.
--entities/paddle.lua
entity.update=function(self)
--Don'tmoveifbothkeysarepressed.Justreturn
--insteadofgoingthroughtherestofthefunction.
ifinput.leftandinput.rightthen
return
end
localself_x,self_y=self.body:getPosition()
ifinput.leftthen
localnew_x=math.max(self_x-10,108)
self.body:setPosition(new_x,self_y)
elseifinput.rightthen
localnew_x=math.min(self_x+10,700)
self.body:setPosition(new_x,self_y)
end
end
Calling math.maxmeanswewillsetthenewx-positiontoeither self_x-10or 100,whichevernumberisbigger.Thispreventsusfromgettinganumbersosmallitrunsofftoofartotheleft. math.mindoestheoppositeandtakescareoftherightsideofthescreen.
Oneissueyoumayormaynotnoticeismovementisn'talwaysauniformspeed,anddependingonthespeedofyourcomputerthepaddlemayappeartogofasterorslower.Rememberthearticleondeltatime?Weneedtoscalethedistancetravelledtomatchtheamountoftimethathaspassed.Conveniently,wearegettingthedeltatimefromlove.updatealready.Takeacloserlookatit:
--main.lua
2.13-Breakout(part3)
97
love.update=function(dt)
ifnotinput.pausedthen
for_,entityinipairs(entities)do
--Deltatimeisbeingpassed
--totheentity.updatefunctionhere
--|
--|
--V
ifentity.updatethenentity:update(dt)end
end
world:update(dt)
end
end
Whichmeanswecandothis:
--entities/paddle.lua
entity.update=function(self,dt)
--Don'tmoveifbothkeysarepressed.Justreturn
--insteadofgoingthroughtherestofthefunction.
ifinput.leftandinput.rightthen
return
end
localself_x,self_y=self.body:getPosition()
ifinput.leftthen
localnew_x=math.max(self_x-(400*dt),100)
self.body:setPosition(new_x,self_y)
elseifinput.rightthen
localnew_x=math.min(self_x+(400*dt),700)
self.body:setPosition(new_x,self_y)
end
end
Thenumber 400isarbitraryandcanbewhateverspeedyouwantthepaddletomoveat. dtisasmallnumbersoitneedstobemultipliedbyalargenumberlike400tomatchaspeedsimilartowhatwewereseeingbeforewhenwesimplywereaddingandsubtracting 10.
Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-3
Inthenextsectionwewillworkonthephysicsmoretogivetheballmovementamorerealisticfeel.Wewillalsoimplementtheabilitytodestroybricksusingtheworldcollisioncallbacks.
ExercisesDespitehavingarestitutionof1,theballislosingmomentumasitcollideswithotherobjects.Thisisduetofriction.Howcanthatbefixed?Whenthegameispaused,makeitdisplaytextonthescreensotheplayerknowsthegameisn'tjustfrozen.Hint:you'llneedoneofthedrawfunctionsfromlove.graphicstoprintthetext.
Theanswerstotheseexerciseswillbeinthenextsection'ssourcecode.
2.13-Breakout(part3)
98
Breakout(part4):physics
ReviewInthepreviousexerciseswediscussedissueswiththeballslowingdownduetofriction.Withabitofbrowsingthroughthe love.physicsdocumentationyoumighthaveseenthatfrictionisapropertyofthefixtureandcanbesetto0in fixture:setFriction.
Howaboutcreatingthepausescreentext?Wereyouabletodoitwithouttouchingmain.lua?Takealookatthisentitythatwascreatedjustforthesingleresponsibilityofdisplayingpausetext:
--entities/pause-text.lua
localinput=require('input')
returnfunction()
localwindow_width,window_height=love.window.getMode()
localentity={}
entity.draw=function(self)
ifinput.pausedthen
love.graphics.print(
{{0.2,1,0.2,1},'PAUSED'},
math.floor(window_width/2)-54,
math.floor(window_height/2),
0,
2,
2
)
end
end
returnentity
end
That'sright.Eventhepausescreenisanentity.Thefirstnaturalplacetothinktoputitwouldbethemainfilebutentityfilesareperfectbecausewecancreateasmanyasweneedforeachtaskandaddittoentities.luawhereitwillbehandledbythegameloop.Forcenteringthetextthe love.window.getModefunctionisusedtogetthefullwindowdimensionsthenthosenumbersaredividedinhalf.Thissavesusfrommanuallycodinginnumbersthatwouldneedtobereadjustedifthewindowsizechanged.Additionally, math.floorwasusedforgoodmeasuretomakesurewearereturningawholenumber.Itisrecommendedtorounddecimalsofffromnumberswhenpassingcoordinatestothedrawingfunctions.Otherwiseitmayattempttodrawthatobjectbetweenpixelsonthescreenandcausesomeblurriness.
PhysicsupdatesAnissuewehadwiththegamephysicssincewegotthepaddlemovingisthattheballdoesn'talwaysricochetoffthepaddleasyouwouldexpect.Thisisbecausewemadethepaddlestaticsotheballdoesn'tpushitaround,butthishastheeffectofthepaddlenotinteractingwiththeballcorrectly.Thisiswhere kinematicbodiescomein.Kinematicbodies,likestaticbodiesaren'taffectedbydynamicbodies.Kinematicbodies,unlikestaticbodies,canaffectdynamicbodies.
We'regoingtomake3changestopaddle.lua:
2.14-Breakout(part4)
99
Movetheboundarydimensions,paddledimensions,andpaddlespeedtoeasily-referencedvariablesatthetopofthefile.ChangethebodytypetokinematicOverhaultheupdatecodetomovethebodywithlinearvelocityratherthanmanuallysettinganewlocationonthescreenwitheveryupdate
--entities/paddle.lua
localinput=require('input')
localworld=require('world')
returnfunction(pos_x,pos_y)
localwindow_width=love.window.getMode()
--Variablestomaketheseeasiertoadjust
localentity_width=120
localentity_height=20
localentity_speed=600
--Thelimitofhowfarleft/righttheentitycanmovetowards
--theedges(withalittlebitofpaddingthrownon).
localleft_boundary=(entity_width/2)+2
localright_boundary=window_width-(entity_width/2)-2
localentity={}
entity.body=love.physics.newBody(world,pos_x,pos_y,'kinematic')
entity.shape=love.physics.newRectangleShape(entity_width,entity_height)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
entity.draw=function(self)
love.graphics.polygon('line',self.body:getWorldPoints(self.shape:getPoints()))
end
entity.update=function(self)
--Don'tmoveifbothkeysarepressed.Justreturn
--insteadofgoingthroughtherestofthefunction.
ifinput.leftandinput.rightthen
return
end
localself_x=self.body:getX()
ifinput.leftandself_x>left_boundarythen
self.body:setLinearVelocity(-entity_speed,0)
elseifinput.rightandself_x<right_boundarythen
self.body:setLinearVelocity(entity_speed,0)
else
self.body:setLinearVelocity(0,0)
end
end
returnentity
end
Itookthelibertyofadjustingthepaddlesize,butwithourniceboundary-sizecalculationsinplacethepaddledimensionscaneasilybeadjustedandtheboundarysizewilltakethosechangesintoaccount.Let'sdrillintotheentity.updatefunction.
Oncetheinputsarecheckedtobetrueorfalsethecurrentx-positionofthepaddleischeckedtoseeifitgoesoutoftheboundaries(calculatednearthetop).Noticethatthecalculationsfortheboundarylocationsaredoneatthetopinsteadofin entity.update.Thismeansthosecalculationsaren'tdoneoneveryupdatesincetheydon'tneedtobe.
Abitmorecomplexthanthepaddlearethecalculationsfortheball:
--entities/ball.lua
localworld=require('world')
2.14-Breakout(part4)
100
returnfunction(x_pos,y_pos)
localentity_max_speed=880
localentity={}
entity.body=love.physics.newBody(world,x_pos,y_pos,'dynamic')
entity.body:setLinearVelocity(300,300)
entity.shape=love.physics.newCircleShape(0,0,10)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setFriction(0)
entity.fixture:setRestitution(1)
entity.fixture:setUserData(entity)
entity.draw=function(self)
localself_x,self_y=self.body:getWorldCenter()
love.graphics.circle('fill',self_x,self_y,self.shape:getRadius())
end
entity.update=function(self)
localvel_x,vel_y=self.body:getLinearVelocity()
localspeed=math.abs(vel_x)+math.abs(vel_y)
localvel_x_is_critical=math.abs(vel_x)>entity_max_speed*2
localvel_y_is_critical=math.abs(vel_y)>entity_max_speed*2
--Ballisbouncingtoofasttoreasonablyhit.
--Cutdownitsspeedby75%ifso.
ifvel_x_is_criticalorvel_y_is_criticalthen
self.body:setLinearVelocity(vel_x*.75,vel_y*.75)
end
ifspeed>entity_max_speedthen
self.body:setLinearDamping(0.1)
else
self.body:setLinearDamping(0)
end
end
returnentity
end
Inthefirstchunkwegetthecurrentxandyvelocity,whichtellsusthexandydirectionoftheball:
localvel_x,vel_y=self.body:getLinearVelocity()
localspeed=math.abs(vel_x)+math.abs(vel_y)
Anexample vel_x/ vel_ymaybe 212/ -300,whichmeanstheballismovingupandtowardstheright.Thespeediscalculatedbyturningboththesenumbersintoabsolutenumbersandaddingthemtogether(so 512intheexample).
Inthenextchunkthereisasafetychecktomakesuretheballdidn'tricochetwithsomuchforcethatit'sgoingtoofasttopossiblyhit.Ifeitherbooleanvariableistruethenthelinearvelocityismultipliedbyafractionofitselftoquicklyslowitdown:
localvel_x_is_critical=math.abs(vel_x)>entity_max_speed*2
localvel_y_is_critical=math.abs(vel_y)>entity_max_speed*2
--Ballisbouncingtoofasttoreasonablyhit.
--Cutdownitsspeedby75%ifso.
ifvel_x_is_criticalorvel_y_is_criticalthen
self.body:setLinearVelocity(vel_x*.75,vel_y*.75)
end
Nowthereisjustachecktoeasetheballbackdowntoacomfortablemaximumspeed.Iftheball'sspeedisgreaterthan entity_max_speedadampingisappliedwhichwillreducetheballsspeedbelow880.Oncethetargetspeedisreachedthenthedampingswitchesbackto0:
2.14-Breakout(part4)
101
ifspeed>entity_max_speedthen
self.body:setLinearDamping(0.1)
else
self.body:setLinearDamping(0)
end
Tryoutthechangestofeelitinactioncomparedtothepreviousphysicsandhopefullyyouwillfindthatit'sanimprovement.It'snotaperfectreplicaofthearcadegame,butplayingaroundwiththesetricksandfeaturesyoucangetitprettydarnclosetosomethingsatisfactory.Anotherthingtotryoutifwithintheball's entity.update,addalineunderthespeedvariablethatreads print(speed)andwatchthenumberincreaseanddecreaseagainasthedampingkicksin.Prettyneatthatmostoftheheavycalculationsarehandledbythephysicsengineforus.
CollisionThereare4changesinvolvedtomakethebricksdestructible:
Updateworld.luatocheckforcollisionfunctionalityfortheentitieswhentheycollideUpdatebrick.luatoincludeacollisioncallbackAddanewattributeonthebrickentitytoletusknowitscurrentconditionandifitneedstobedestroyed.We'lljustcallit entity.health.Updatemain.luatoremove/destroyanyentitiesthathavenomorehealth
Firsttheworld:
--world.lua
--Calledattheendofanycontactintheworld.Parameters:
--{fixture}fixture_a-firstfixtureobjectinthecollision.
--{fixture}fixture_b-secondfixtureobjectinthecollision.
--{contact}contact-worldobjectcreatedonandatthepointofcontact
--Seefurther:https://love2d.org/wiki/Contact
localend_contact_callback=function(fixture_a,fixture_b,contact)
localentity_a=fixture_a:getUserData()
localentity_b=fixture_b:getUserData()
ifentity_a.end_contactthenentity_a:end_contact()end
ifentity_b.end_contactthenentity_b:end_contact()end
end
localworld=love.physics.newWorld(0,0)
world:setCallbacks(nil,end_contact_callback,nil,nil)
returnworld
Theonlycallbackwe'llbeusingforthistutorialistheend-contactcallback,sofor world:setCallbackswearegoingtoreturning nilfortheresttokeepourcodefastandclean.Takealookatwhatishappeninginsideend_contact_callback.Rememberinsideeachentitywhenweinvoked entity.fixture:setUserData(entity)?Withtheentityattachedtoeachfixture,wecangetaccesstothoseentitiesbyinvoking fixture:getUserDatainthecallbackabove.Oncewehaveaccesstoeachentity,wechecktoseeiftheentityhasany end_contactfunctions,codespecifictothatentitythatneedstorunwhenendingthecollision.
Nowwecangotobrick.luaanddefinethatfunctionality:
--entities/brick.lua
localworld=require('world')
returnfunction(x_pos,y_pos)
2.14-Breakout(part4)
102
localentity={}
entity.body=love.physics.newBody(world,x_pos,y_pos,'static')
entity.shape=love.physics.newRectangleShape(50,20)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
--Howmanytimesthebrickcanbehitbeforeitisdestroyed
entity.health=2
entity.draw=function(self)
love.graphics.polygon('fill',self.body:getWorldPoints(self.shape:getPoints()))
end
entity.end_contact=function(self)
self.health=self.health-1
end
returnentity
end
Noticethetwonewvaluesinthetable, entity.healthand entity.end_contact.Inside end_contactwearesubtracting1healthwhenthecollisionends.Healthcouldstartatanynumberandthatmeanstheballwillneedtocollidewiththebrickthatmanytimesbeforethehealthreaches0.Lastly,weneedtogointomain.luaandadjustlove.updatesoitdoessomethingwhenitseesanentitywith0health:
--main.lua
love.update=function(dt)
ifnotinput.pausedthen
localindex=1
whileindex<=#entitiesdo
localentity=entities[index]
ifentity.updatethenentity:update(dt)end
--Whenanentityhasnohealth(brickhasbeenhitenoughtimes
--thenweremoveitfromthelistofentities.Don'tincrement
--theindexnumberifdoingthatthoughbecausewehaveshrunk
--thetableandmadealltheitemsshiftdownby1intheindex.
ifentity.health==0then
table.remove(entities,index)
entity.fixture:destroy()
else
index=index+1
end
end
world:update(dt)
end
end
Theentityisremovedfrom entitiesaswellashavingitsfixturedestroyedfromtheworld.Thiswillonlyhappentobrickswith0health.Itwon'thappentoentitieswherewedidn'tdefinehealthbecause nilisnotthesamethingas0.Noticethata whileloopwasusedhere.Thisisbecausewemayremoveentitiesfromthelistweareloopingoverandthiswouldthrowofftheindexcountforaregular forloop.
Ifyoumissedanythingorarehavingissues,here'sacopyofthecompletedsourcecodeforthissection:https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-4
Inthenextsectionwe'llreviewthechecklistandseewhatislefttocover.
ExercisesItwouldbegreatifthecolorsofthebrickschangeddependinghowmuchhealththebrickhas.Updatethebrick'sentity.drawfunctionwithsomecolors.Hint:wecoveredcolorsin2.4-Gameloop.
2.14-Breakout(part4)
103
Addmorebrickstothescreen.What'stheeasiestwaytodothat?
2.14-Breakout(part4)
104
Breakout(part5):gamestate
ReviewWe'vegottenabitdonesolet'slookatthebasicrequirementsagain:
Theobjectiveofthegameistodestroyallthebricksonthescreen✔Theplayercontrolsa"paddle"entitythathitsaball✔Theballdestroysthebricks✔TheballneedstostaywithintheboundariesofthescreenIftheballtouchesthebottomofthescreen,thegameends
Inthepreviousexercisethequestionwasbroughtupwhatwouldbetheeasiestwaytodrawabunchofbricksacrossthescreen.Asimple,butverytediousanswertothatwouldbetopositionthebricksoneatatimeinentities.lualikeso:
brick(40,80),
brick(100,140)
--andsoon...
Ifyouwanttomakeyourbricksintoashapeorsculpturethenthatmightbethebestapproach.Ifyoujustwanttoarrangeyourbricksintoagrid,thentheeasiestwaywouldbetowriteanumericfor-loop.
--entities.lua
localboundary_bottom=require('entities/boundary-bottom')
localboundary_vertical=require('entities/boundary-vertical')
localboundary_top=require('entities/boundary-top')
localpaddle=require('entities/paddle')
localpause_text=require('entities/pause-text')
localball=require('entities/ball')
localbrick=require('entities/brick')
localentities={
boundary_bottom(400,606),
boundary_vertical(-6,300),
boundary_vertical(806,300),
boundary_top(400,-6),
paddle(300,500),
pause_text(),
ball(200,200)
}
localrow_width=love.window.getMode()-20
fornumber=0,38do
localbrick_x=((number*60)%row_width)+40
localbrick_y=(math.floor((number*60)/row_width)*40)+80
entities[#entities+1]=brick(brick_x,brick_y)
end
returnentities
Okthisadmittedlylooksmorecomplicatedatfirst,butifyourememberthearithmeticandordersofoperationcoveredin1.1-Interactivecodingstatementsareprocessedfromtheinnerparenthesisandworkedoutwards.Sowhythelongcalculation?Let'sstartoffwithasimplercalculation:
localbrick_x=number*60
2.15-Breakout(part5)
105
Startingwiththe number0upto38,therewillbe39loopsandtherefore39bricksdrawn.Onthefirstloop, numberis0.Sincethebricksare50pixelswidethiswoulddrawthebrickswitha10pixelspacebetweeneach.Firstbrickat60,then120,then180...Ok,butthenafteronlyadozenbrickswewouldstartrunningoffthescreen.Thisiswherethemoduluscomesinhandy:
localbrick_x=(number*60)%row_width
row_widthishowwidewewantarowofbrickstobebe.Inthiscase row_widthisthescreenwidth,800pixels,subtract20pixelsforpadding.Sodrawthebricksevery60pixels,butthenwhenyougetto780pixels,startbackat0pixelsandbegindrawinganewrow.Thanksmodulus!Nowjusttogivethebrickssomespacingontheleftsideawayfromthewall,wecangoaheadandadd40pixelstothefinalresultforthex-position:
localbrick_x=((number*60)%row_width)+40
Thebrick'sy-positioniscalculatedalittlebitdifferently.Whatweneedtofindoutiswhichrowwe'reonsoweknowwhereonthey-axistodraw.Ifwetakethe numberandmultiplyitby60thendoamodulusweknowthatgivesusthex-position.Solet'stakethatchunkofcodefromaboveandmakethatthebasisofoury-positioncalculation:
localbrick_y=(number*60)%row_width
Ratherthanusingmodulus,ifweuseregulardivisionwegetasmallremaindereverytime (number*60)exceedstherowwidth:
localbrick_y=(number*60)/row_width
Thiswillgiveusanumberwithdecimalssotokeepthingsroundedwecanuse math.floortosnapthey-positiondowntothenearestwholenumber:
localbrick_y=math.floor((number*60)/row_width)
Great!Noweverytimethex-positionexceedstherowwidth,wegetbackthenumberoftherowwe'reon...0forthefirst,1forthesecond,2andsoon.Withthisnumberwecannowspaceouteachrowby40pixels:
localbrick_y=math.floor((number*60)/row_width)*40
Thenfinallyjusttoshiftthebricksalittlefurtherdownthescreenwegiveitapaddingthatlooksright,say80:
localbrick_y=(math.floor((number*60)/row_width)*40)+80
Andthereyougo.Theentitycanjustbeaddedtotheendoftheentitieslistsoitdoesn'tgetlost:
entities[#entities+1]=brick(brick_x,brick_y)
Inthepreviousexerciseswealsotalkedaboutdrawingthebricksdifferentcolorstoindicatetheirintegrity/healthleftbeforetheywillbedestroyed.Ratherthanreviewthatnow,let'sdiveintostatemanagementandwe'llwrapcoloringupalongtheway.
Statemanagement
2.15-Breakout(part5)
106
Youraverage,every-dayprogramhasalotofinformationitneedstostoryinmemory.Forourgametofunctionwithjustthebasicfeatures,weneedtostoreinformationabouteachentity,whetherornotthegameiscurrentlypaused,orifthegameiswonorlost.Thisinformationiscalledthestate.Thestateisdatathatmaychangeduringthelifetimeoftheapplication.Thinkofthestateofyourlightsinyourroom.Aretheycurrentlyinan"on"or"off"state?Thestatecancausedifferenteffectsontheapplication,likeifthe"pause"stateofthegameis"true"thentheworldwillnolongerreceiveupdates.
Onethingwemustthinkofishowtoorganizethestateofourapplication.Thisissomethingwetakeforgrantedoftenintherealworld;Wedon'thavetofigureoutwheretostorethestateofourlights.It'sapieceofinformationintrinsictothelamp'sdesign.
Sowhydowehavetocaresomuchaboutourgame'sstate?Tobefair,ourgameissmallsoweprobablydon'tneedto.However,itiscrucialtoreconcilesuchthingswhileapplicationsaresmallbecauseitwillbeverydifficulttogobackandfixabunchofcodeoncetheapplicationisbig.Thewayyoushouldorganizethestateofyourapplicationshouldaccomplishafewthings:
Itshouldbeeasytofindandaccessthenecessarydatathatmakesupthestate.Forinstance,howeasyisitforourmainfiletoaccesstheentitiesandloopovertheminthe love.updatefunction?Thereshouldonlybeonecopyofthestate.Ifwewanttoaccessthe"paused"stateofourgameinmultipleplacesthatisfine,butweshouldn'thavemultiple"paused"variablesfloatingaroundourgame.Ifwehada"paused"variableinsideanentityfileandanotherinsidetheinputserviceupdatingindependentlythentheycouldgetoutofsyncandourgamewouldgetconfusedonwhenitshouldbepaused.Thestateshouldonlybeaccessedwhereitisneeded.Ifyouwereaccessingorstoringthe"paused"stateinsidetheballentity,thenifthatballwasdestroyedthensomethingbadwillhappenthenexttimethegamecheckstoseeifitispaused.
Whatfilescontainthestateofourgame?
entities.lua-Eachentitytableisresponsibleforitsownstate.Forinstance,eachbrickstoresthestateofitsownhealth.Alltheentitiestablesaregeneratedandstoredhere.Theentitiesarenotstoredintheentitiesfolder.Thosearejustfunctionsusedtogeneratetheentities.Theblueprints.input.lua-Thisfileisresponsibleforcapturinguserinput,butalsostoringthestateofwhatkeysarecurrentlybeingpressed.world.lua-Thisfileisnotonlytheblueprintsforthegameworld,butitalsostorestheworldinstancethatisgeneratedwhenthegamestarts.Wemadetheworldinstanceeasilyaccessibletotherestoftheapplicationbywriting returnworldattheend.Therewouldbenogameifthiswasn'teasilyaccessible.
Afewpiecesofgamestateweneedtoaddareabooleanofwhetherthegameisover,anotherforifthestageiscleared,andalsoalistofcolorstouseinourgamewhichwe'llrefertoasourpalette.Thisinformationwouldn'treallyfitinanyoftheplaceswelistedabove,andwedon'twanttoaddittomain.luabecauseofourfirstrulethatthegamestateshouldbeeasytoaccesswhereitisneeded.Besides,that'snotthemainfile'sresponsibility.We'llgoaheadandjustmakeanewfilecalledstate.luaandstoretheoverallgamestateinthisfile.Thisisalsoalittlematterofopinionbutthe"paused"andbuttonstateswe'llalsomoveinheresincetheyaffecttheoverallgame'sstate.Thiswillalsomakeitsothatinput.lua'sonlyresponsibilityistocaptureandtranslatetheuserinput,nottohandleanystatewhatsoever.
--state.lua
--Thestateofthegame.Thiswayourdataisseparatefromourfunctionality.
return{
button_left=false,
button_right=false,
game_over=false,
palette={
{1.0,0.0,0.0,1.0},--red
2.15-Breakout(part5)
107
{0.0,1.0,0.0,1.0},--green
{0.4,0.4,1.0,1.0},--blue
{0.9,1.0,0.2,1.0},--yellow
{1.0,1.0,1.0,1.0}--white
},
paused=false,
stage_cleared=false
}
It'skindofanicefeelingtokeepallthestatetogether.Wecouldevenmovetheentitieslistintostate.luaandgetridofentities.lua,butthisdoesn'tseemnecessary.Nowwiththisshiftindataweneedtoupdateinput.luaandmain.luatoreferencethenewfile:
--input.lua
localstate=require('state')
--Mapspecificuserinputstogamestates
localpress_functions={
left=function()
state.button_left=true
end,
right=function()
state.button_right=true
end,
escape=function()
love.event.quit()
end,
space=function()
ifstate.game_overorstate.stage_clearedthen
return
end
state.paused=notstate.paused
end
}
localrelease_functions={
left=function()
state.button_left=false
end,
right=function()
state.button_right=false
end
}
--Thistableistheserviceandwillcontainsomefunctions
--thatcanbeaccessedfromentitiesorthemain.lua.
return{
--Lookupinthemapforactionsthatcorrespondtospecifickeypresses
press=function(pressed_key)
ifpress_functions[pressed_key]then
press_functions[pressed_key]()
end
end,
--Lookupinthemapforactionsthatcorrespondtospecifickeyreleases
release=function(released_key)
ifrelease_functions[released_key]then
release_functions[released_key]()
end
end,
--Handlewindowfocusing/unfocusing
toggle_focus=function(focused)
ifnotfocusedthen
state.paused=true
end
end
2.15-Breakout(part5)
108
}
--main.lua
localentities=require('entities')
localinput=require('input')
localstate=require('state')
localworld=require('world')
love.draw=function()
for_,entityinipairs(entities)do
ifentity.drawthenentity:draw()end
end
end
love.focus=function(focused)
input.toggle_focus(focused)
end
love.keypressed=function(pressed_key)
input.press(pressed_key)
end
love.keyreleased=function(released_key)
input.release(released_key)
end
love.update=function(dt)
ifstate.game_overorstate.pausedorstate.stage_clearedthen
return
end
localindex=1
whileindex<=#entitiesdo
localentity=entities[index]
ifentity.updatethenentity:update(dt)end
--Whenanentityhasnohealth(brickhasbeenhitenoughtimes
--thenweremoveitfromthelistofentities.Don'tincrement
--theindexnumberifdoingthatthoughbecausewehaveshrunk
--thetableandmadealltheitemsshiftdownby1intheindex.
ifentity.healthandentity.health<1then
table.remove(entities,index)
entity.fixture:destroy()
else
index=index+1
end
end
world:update(dt)
end
Noticethechangeto love.update.Wecheckif state.game_over, state.pausedor state.stage_clearedistrueandifso,wereturnfrom love.updatewithoutdoinganyoftheupdatesasthesekindofgamestatesmeritfreezingthescreen.
Nextup,updatepaddle.luatorequire stateinsteadof input.The entity.updatefunctionnowneedstoreferencestate.button_leftand state.button_righttotelliftheplayerhaspressedanybuttons.Tryupdatingitonyourown.Ifyoudogetstuck,thesourcecodewillbeinthelinkatthebottomwaitingforyou.
Ok,nowthatwehaveastatewherewestoredthecolorsitisprobablyagoodtimetotryandupdatebrick.lua.Firstlet'slookatthosecolorsstoredinstate.lua:
palette={
{1.0,0.0,0.0,1.0},--red
2.15-Breakout(part5)
109
{0.0,1.0,0.0,1.0},--green
{0.4,0.4,1.0,1.0},--blue
{0.9,1.0,0.2,1.0},--yellow
{1.0,1.0,1.0,1.0}--white
},
The palettetableisalistofmoretables.Eachtableinthelistrepresentscolorswherethefirstnumberistheamountofred,2ndtheamountofgreen,3rdtheamountofblue,and4thnumbertheamountofopacity.Settingthelastnumberto 0meansthecoloris100%transparentand 1meansitiscompletelyopaque.Allofthesevaluesmixtogethertoformasinglecolor.Inthecaseofthefirstcolor,wehavetheredvaluesettomaximumopaqueredwithnoothercolorsmixedin.Iwouldencourageyoutogobackandeditthecolorsinthispaletteaftereverythingisworking.Now,insidebrick.lualet'supdate entity.draw:
--entities/brick.lua
localstate=require('state')
localworld=require('world')
returnfunction(x_pos,y_pos)
localentity={}
entity.body=love.physics.newBody(world,x_pos,y_pos,'static')
entity.shape=love.physics.newRectangleShape(50,20)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
--Howmanytimesthebrickcanbehitbeforeitisdestroyed
entity.health=2
--Usedtocheckduringupdateifthisentityisabrick
--Ifnobricksarefoundthenthelevelwascleared
entity.type='brick'
entity.draw=function(self)
--Drawthebrickinadifferentcolordependingonhealth
love.graphics.setColor(state.palette[self.health]orstate.palette[5])
love.graphics.polygon('fill',self.body:getWorldPoints(self.shape:getPoints()))
--Resetgraphicsdrawerbacktothedefaultcolor(white)
love.graphics.setColor(state.palette[5])
end
entity.end_contact=function(self)
self.health=self.health-1
end
returnentity
end
Beforedrawingthebrick'spolygon,wesetthegraphicsrenderertouseoneofthecolorsfrom state.palette.Thecolortousedependsonwhatthebrick'shealthis.Soifthebrickhas2healththen state.palette[self.health]willbecome state.palette[2]whichwillgrabthe2ndcolorinthelist...green.Ifthebrick'shealthwas1,thenthefirstcolorfromthepalettewouldbeselected...red.Afterthecoloredpolygonisdrawn, entity.drawfinishesupbysettingtherenderercolorbacktowhite.Ifwedidn'tdothisstep,theballandpaddlewouldgetdrawnthesamecolorasthebricks.
Onelastthingweneedtodotogetthegameworkingisupdatepause-text.luaasitisincorrectlylookingforthe"pause"stateininput.luainsteadofthenewstate.lualocation:
--entities/pause-text.lua
localstate=require('state')
returnfunction()
localwindow_width,window_height=love.window.getMode()
2.15-Breakout(part5)
110
localentity={}
entity.draw=function(self)
ifstate.pausedthen
love.graphics.print(
{state.palette[3],'PAUSED'},
math.floor(window_width/2)-54,
math.floor(window_height/2),
0,
2,
2
)
end
end
returnentity
end
FinaltouchesWeneedthegametoendwhentheplayerdestroysallthebricksorlosestheball.Justlikethepause-textentity,displaysomemessagesbasedonthegamestate.
--entities/game-over-text.lua
localstate=require('state')
returnfunction()
localwindow_width,window_height=love.window.getMode()
localentity={}
entity.draw=function(self)
ifstate.game_overthen
love.graphics.print(
{state.palette[5],'GAMEOVER'},
math.floor(window_width/2)-100,
math.floor(window_height/2),
0,
2,
2
)
end
end
returnentity
end
--entities/stage-clear-text.lua
localstate=require('state')
returnfunction()
localwindow_width,window_height=love.window.getMode()
localentity={}
entity.draw=function(self)
ifstate.stage_clearedthen
love.graphics.print(
{state.palette[4],'STAGECLEARED'},
math.floor(window_width/2)-110,
math.floor(window_height/2),
2.15-Breakout(part5)
111
0,
2,
2
)
end
end
returnentity
end
Totriggerthe"GAMEOVER"textiseasyenough.Weneedtoaddacollisioncallbacktoboundary-bottom.luatosetthegame's state.game_overtotrueonanycollision:
--entities/boundary-bottom.lua
localstate=require('state')
localworld=require('world')
returnfunction(x_pos,y_pos)
localentity={}
entity.body=love.physics.newBody(world,x_pos,y_pos,'static')
entity.shape=love.physics.newRectangleShape(800,10)
entity.fixture=love.physics.newFixture(entity.body,entity.shape)
entity.fixture:setUserData(entity)
entity.end_contact=function(self)
state.game_over=true
end
returnentity
end
Don'tforgetweneedtoupdateentities.luatoaddourtwonewentities:
--entities.lua
localboundary_bottom=require('entities/boundary-bottom')
localboundary_vertical=require('entities/boundary-vertical')
localboundary_top=require('entities/boundary-top')
localpaddle=require('entities/paddle')
localgame_over_text=require('entities/game-over-text')
localpause_text=require('entities/pause-text')
localstage_clear_text=require('entities/stage-clear-text')
localball=require('entities/ball')
localbrick=require('entities/brick')
localentities={
boundary_bottom(400,606),
boundary_vertical(-6,300),
boundary_vertical(806,300),
boundary_top(400,-6),
paddle(300,500),
game_over_text(),
pause_text(),
stage_clear_text(),
ball(200,200)
}
localrow_width=love.window.getMode()-20
fornumber=0,38do
localbrick_x=((number*60)%row_width)+40
localbrick_y=(math.floor((number*60)/row_width)*40)+80
entities[#entities+1]=brick(brick_x,brick_y)
end
2.15-Breakout(part5)
112
returnentities
Ok,testthatoutandcheckthatthe"GAMEOVER"textworks.Ifitdoes,thenlet'scontinueonandaddtheconditionsforhowtowinthegame.Thisinvolvescheckingthroughalltheentitiesin love.updatetomakesurewestillhavebricks.Ifwedon'thaveanybricksleft,thentheplayerdestroyedthemallandthestageiscleared.
--main.lua
love.update=function(dt)
ifstate.game_overorstate.pausedorstate.stage_clearedthen
return
end
--Switchtotrueifwehavebricksleft
localhave_bricks=false
localindex=1
whileindex<=#entitiesdo
localentity=entities[index]
ifentity.type=='brick'thenhave_bricks=trueend
ifentity.updatethenentity:update(dt)end
--Whenanentityhasnohealth(brickhasbeenhitenoughtimes
--thenweremoveitfromthelistofentities.Don'tincrement
--theindexnumberifdoingthatthoughbecausewehaveshrunk
--thetableandmadealltheitemsshiftdownby1intheindex.
ifentity.healthandentity.health<1then
table.remove(entities,index)
entity.fixture:destroy()
else
index=index+1
end
end
--Flagthestageclearediftherearenomorebricks
state.stage_cleared=nothave_bricks
world:update(dt)
end
Everytime love.updateisran,wesetavariable have_brickstofalse.Ifthisbooleanstays falseallthewaytothebottomofthefunctionthen state.stage_clearedgetsswitchedtotrueandthegameiswon.Insidethe whileloop,however,wecheckeveryentitytoseeifwefindan entity.typeof 'bricks'andifso, have_bricksgetsflippedtotruetostopthegamefrombeingwonyet.
Sothataboutdoesitforcompletingourchecklist.Thegamemaynotbeasfeature-completeasatruebreakoutgame,butthatroomforimprovementleavesopportunityforyoutomodifythegametoworkhowyouwantitto.It'sreallyuptoyourimagination.Tryoutafewexercisesifyoucan'tthinkupanynewfeatures.Ifyouarehavingtroublerunningthegame,besuretocheckoutthesourcecode:
https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-5
ExercisesInsteadofgettingagameoverassoonastheballtouchesthegroundonce,addanewpropertyinstate.luanamed livesandsetittoasmanylivesasyouwanttheplayertohave.Makeissothe state.livesdecreaseswhentheballhitsthegroundandmakethe game_overnottriggerunless state.lives<1.TrysettingthepaddletodifferentshapetomakethegameplaydifferentlyComeupwithnewfeaturestomakethegameplaybetterandfeelmorepolished
ChangetheballandpaddlecolorsAddabackgroundcolor
2.15-Breakout(part5)
113
FigureouthowtoplayasoundeffectwhentheballcollideswiththingsCreatesomekindofpower-upentity
2.15-Breakout(part5)
114
BinaryandbitmasksIn2.10-Collisioncallbackswesawhowtoreacttoentitiescolliding.Inthissectionwe'regoingtodiscusshowwecanbettercontrolwhencollisionshappen.Asthetitlesuggests,thiswillinvolveunderstandingsomebinary.
Let'ssaywehaveabeat-em-upgamewheretwoplayersarefightingbadguysandwedon'twantplayerstocollidewitheachotherandinsteadonlycollidewithenemies.Thecollisioncallbackcouldlooksomethinglikethis:
localbegin_contact_callback=function(fixture_a,fixture_b)
localentity_a_type=fixture_a:getUserData()
localentity_b_type=fixture_b:getUserData()
--Checkthesearen'tthesametypeofentity
ifentity_a_type~=entity_b_typethen
--Somecodetohandlethecollisiongoeshere...
end
end
Butwhatifyouhadpower-upsandyouwantplayerstocollidewiththepower-upsbutyoudon'twantenemiestouchingthepower-ups?Thingscangetcomplicatedprettyquickly:
localbegin_contact_callback=function(fixture_a,fixture_b)
locala=fixture_a:getUserData()
localb=fixture_b:getUserData()
if(a=='powerup'andb=='player')or(a=='player'andb=='powerup')then
--Somepower-upcode...
--Don'tletpower-upscollidewithotherentitytypeslikebadguys
elseifa~=banda~='powerup'andb~='powerup'then
--Codetohandletherestofthecollisions...
end
end
Let'sfindabetterway!
BinaryoperationsBackin1.0-Programmingbasicswediscussedoperations–howtooperateonstringswithequality( ==)checks,howtooperateonnumberswitharithmeticoperations,andevenhowtoperformbooleanoperationslike andandor.Binarynumbershavetheirownoperations,oftencalledbitwiseoperations.Toperformbinaryoperations,let'sfirstlookathowtorepresentbinarynumbers.Typinganumberlike 101,Luawillinterpretitasadecimalnumber(literallyone-hundredone)soweneedtorepresentitasastringandconvertittoanumber.Toconvertastringtoanumber,youpassinthenumberandthebase(base-2inthiscase)likeso:
print(tonumber('101',2))
Whichconvertsthebinarynumber 101todecimalwhenitprintsout:
5
Forcountinginbinaryandlearninghowtoreadandconvertbetweenbinaryanddecimal,therearemanyresourcesthatalreadyexplainitinmuchbetter.Learninghowtodotheconversionsisn'tnecessarytolearningthesebasicbinaryoperations,butisanessentialskilltohaveinthefieldofcomputerscience.
2.16-Binaryandbitmasks
115
Forcountinginbinaryandlearninghowtoreadandconvertbetweenbinaryanddecimal,therearemanyresourcesthatalreadyexplainitinmuchbetter.Learninghowtodotheconversionsisn'tnecessarytolearningthesebasicbinaryoperations,butisanessentialskilltohaveinthefieldofcomputerscience.
Movingon,let'stakealookatsomeofthebasicoperations.
AND
Binarynumbersaresimilartobooleansinthatbinaryonlyhas1'sand0's.TheANDoperatoralsoworkssimilarlytotheboolean and.Yougiveittwodigitsandbothmustbe 1( true)fortheoutputtobe 1.
UnfortunatelyatthetimeofwritingthistheonlineREPLhasanoutdatedversionofLuathatdoesn'tsupportbinaryoperations.Noworries,let'screateamain.luafileandtryitoutusingLÖVE.Toperformbinaryoperations,theincluded 'bit'librarymustbeloaded.Whenrequired,itwillreturnatablewithmanyfunctionsinitrelatedtobinaryoperations.Thefirstfunctionwe'lltry, bit.band()performsabinaryANDoperation.
localbit=require('bit')
print(bit.band(0,0))
print(bit.band(0,1))
print(bit.band(1,0))
print(bit.band(1,1))
Thiswillprinttothedebugconsole:
0
0
0
1
Youcanpassitthedecimals 1and 0asthosenumbersarethesameinbinaryanddecimal.
Theoperationisnotlimitedtotwoinputs:
print(bit.band(1,0,1))
Youcanalsopassitmulti-digitnumbers:
print(bit.band(
tonumber('111',2),
tonumber('101',2)
))
Notethatyouneedtoalwaysuse tonumber()toconvertyourbinarystringtoanumberasthefunctionalwaysexpectsadecimalnumber.Likewisetheoutputwillalwaysbeadecimalnumber:
5
Layitoutlikeanarithmetictableandyoucansolveitjustaseasily:
111
101
---
101-->5
11011010
2.16-Binaryandbitmasks
116
10111100
--------
10011000-->152
ORLikewiththeboolean or,thebinaryORoutputwillbe 1ifeitherthefirstorthesecondnumberis1.Youcouldsayitistheleastpickyoperatorinthatitdoesn'tcareaslongasitgetsa1somewhereatleastonce.
--main.lua
localbit=require('bit')
print(bit.bor(1,1))
print(bit.bor(1,0))
print(bit.bor(0,0))
print(bit.bor(0,0,0,1,0))
1
1
0
1
XOR
Xor(exclusiveor),returns1onlywhenitgetsone1.Let'scompareXORinatabletotheothers:
AND
inputA inputB output
0 0 0
0 1 0
1 0 0
1 1 1
OR
inputA inputB output
0 0 0
0 1 1
1 0 1
1 1 1
XOR
inputA inputB output
0 0 0
0 1 1
1 0 1
1 1 0
2.16-Binaryandbitmasks
117
Binaryoperationsaresomeofthemostfundamentalcomputeroperationsandcanbephysicallybuiltwithafewtransistors.Giventhesimplicityoftheseoperations,italsomakesforafastmethodofcalculatingcollisions.
Bitmasks
Let'stakealookatthissceneforamomentandidentifyfromthecrudelydrawnshapessomepotentialentities:
2.16-Binaryandbitmasks
118
Alltheseentitiesfallintouniquecategoriesinthatwewanteachofthemtocollidewithcertainotherentities.Ifthiswereagame,we'ddefineeachcategorywithauniquebinarydigit,orbit,solet'sfirstdothat:
entity category
sun 0000
player 0001
powerup 0010
enemy 0100
ground 1000
Let'ssetsomerulesforeachoftheseentities.Forinstance,wewanttheplayertocollidewiththepowerup(0010),enemy(0100),andofcoursetheground(1000).Totellthegameenginethis,wecreateabitmaskforthefixture.Thisisabinarynumberwithallthebitsswitchedonthatwewanttheentitytocollidewith.Inotherwords,theplayer'sbitmaskwouldbe(1110).Weleftthefirstbitblanksothattheplayercan'tcollidewithotherpotentialplayers(player2).Let'supdatethetablewiththebitmaskwewanteachentitytohave:
entity category bitmask
sun 0000 0000
player 0001 1110
powerup 0010 1001
enemy 0100 1001
ground 1000 1111
2.16-Binaryandbitmasks
119
Sohowdoesitallcometogetherandwork?Whentwoentity'sfixturescontact,abinaryANDoperationisperformedagaintheentity'sbitmaskandtheotherentity'scategory.Iftheresultingnumberisn't0000thenwehaveacollision.Taketheplayerandenemyforinstance:
0001player'scategory
1001enemy'sbitmask
----
0001wehaveacollision
Andhowabouttheenemyandthepowerup:
0100enemy'scategory
1001powerup'sbitmask
----
0000wehaveNOcollision
Armedwiththisknowledge,wecanassertthefollowinginformationfromthetableabove:
Thesuncollideswithnothing(anddoesn'tevengetacategory).It'sjustinthebackgroundandnon-interactive.Theplayercollideswitheverythingexceptotherplayers(andofcoursethesun).Thepowerupcollidesonlywiththegroundandplayers.Theenemycollidesonlywiththegroundandplayers.Thegroundcollideswitheverything.
Copyordownloadthe"collision"gamefromtheexamplecodeandrunit:
https://github.com/RVAGameJams/learn2love/tree/master/code/collision
Dotheentitiesinteractasexpected?Takealookinsidetheentitiesfoldertoseetheparticularfunctionbeingcalledtoaccomplishapplythecategoriesandbitmaskstoeachentity–Fixture:setFilterData
--square.lua
...
square.category=tonumber('0001',2)
square.mask=tonumber('1110',2)
square.group=0
...
square.fixture:setFilterData(square.category,square.mask,square.group)
Theexamplesaboveonlyuse4bitsforthecategoryandmaskbceausethat'sallweneeded,howeverLÖVEsupportsupto16bitsforthecategoryandbitmask( 0000000000000000).Thegrouppropertyisn'tusedandshouldbesetto0whenitisn't.Wehaven'tmentionedgroupsbeforebecauseifyouknowhowtousecategoriesandbitmasksthenyoudon'tneedtousegroupsascategoriesandbitmasksofferamorepowerfulwayofdoingthesamething.Thatbeingsaid,collisiongroupsshouldberelativelystraight-forwardtolearnaboutsoitwillbeleftupasanexercisetoreadandstudy.
ExercisesPlaywiththebitmasks.Canyoumaketheenemycollidewiththepowerupinsteadoftheplayer?TakealookathowgroupsworkasdescribedinFixture:setGroupIndex.Thisisasimpler,butmorelimitedmethodofdetectingcollision.Canitbeusedtoimitatethecollisionrulesabove?
2.16-Binaryandbitmasks
120
Networking(part1)Whencreatingaprogramsuchasagame,oneofthefirstthingstoconsidershouldbewhetheritisanetworkedapplication.Suchachoicewillradicallychangethestructureandcomplexityoftheapplication.Tobuildanetworked("online")multiplayergame,wemustunderstandsomenetworkingbasics.Someofthisinformationisoversimplified,butlet'sestablishabaselineofknowledge.
Internetprotocol(IP)Networksarepossiblebecausecomputersagreeonawaytocommunicatewitheachother.Likeogres,messagessentacrosstheinternethavemanylayers.Eachlayerrepresentsadifferentprotocolthatinterpretshowthemessageshouldbehandled.Theinternetprotocol(IP)tellscomputershowtorelaymessagestotheirintendeddestination.Therearetwothingsweneedtoknowaboutthisprotocol:IPaddressesandports.
EverydeviceconnectedtotheinternethasanIPaddressassignedtoitwhenitconnects.MessagessentoutfromyourdevicearesentwithadestinationIPaddressattachedsoitknowswheretogo.Messagesarerelayedfromonemachinetoanotheruntilitreachesthedestinationmachine'saddress.
Ifyouopenyourterminalorcommandpromptandtype pinggoogle.comyouwillgetaresponsebackthattellsyouthedestinationIPaddress;TheIPaddressoftheserverrunningthegoogle.comhomepageyousee.YoumayevenbeabletotypethatIPaddressintoyourwebbrowseranditwilldirectyoutothewebsiteinthesamefashiontypinggoogle.comintheaddressbarwould(althoughthiswon'tworkforallwebsitesbecauseofunrelated,complicatedreasons).Let'ssayyouconnectedtogoogle.comthroughtheIPaddress172.217.7.14.You'reactuallyconnectingtothatIPthroughaspecificport.Portsarerepresentedasnumbers,sothatIPlikemosteveryotherwebsiteontheinternetisaccessedthroughport443.
IPportsarelikethemaritimeportsthatharborships.Asingledestinationcanhavemultipleportsfordifferentpurposes.IfIwerebringinginamilitaryvesselImaygotoadifferentportthanacommercialvessel.
DependingonyourintentionsforanetworkconnectionyouwillusedifferentIPports.Forinstance,ifyouaretryingtoviewawebsitelocatedat172.217.7.14youwilluseport443foranHTTPSconnection,port80foranHTTPconnection(ifallowed),andifIamanadministratorofthemachinerunningon172.217.7.14Iwilluseacompletelydifferentporttoestablishabackdoorconnectionsuchasport22.
ForourexampleprogramwewilltryconnectingtoaspecialreservedIPaddress,127.0.0.1.ThisIPaddressisyourmachine'sownIPaddressituseswhenitwantstoconnecttoitself.Sincewe'llbetestingourprogrambyrunningbothcopiesonthesamecomputerwewon'tneedtoworryaboutmultipleIPaddressesfornow.Fortheportyouhavearangefrom0to65535anditdoesn'treallymatterwhichoneyouusesolongasit'snotalreadyinuseorbeingreservedforotherpurposes.We'llpickarandomonethatisunlikelytobeinusebyotherprograms...6789.
TransportlayerThetransportlayerdecideshowyourdatawillbepackagedandstreamed.Youhaveachoiceonafewdifferentprotocolsforthetransportlayer.Understandingthedetailsofeachprotocolinthetransportlayerisn'ttooimportantforthissectionofthebookbutlet'sdiscusswhywemaywanttouseoneortheother.
TCP-Thisprotocolprovidesdifferentfeaturestomakesuredatadoesn'tgetcorrupt.Mostnotably,itwaitsforaconfirmationresponsefromtheotherendtomakesurethemessagewasreceived.Ifaresponseisn'treceivedbyacertaintimeoutthentheconnectionisconsideredafailure.WebsitesuseTCP99%ofthetimebecauseofitsreliabilityandensuringyou'vereceivedthesite'sfullcontent.
2.17-Networking(part1)
121
UDP-Thisprotocolsendsdatatoaserverandexpectsnoresponseback.Sendingdatawithoutconfirmingitreachesthedestinationcouldleadtolessreliabledatatransportation.However,lessbackandforthcommunicationcouldmeanafasterconnection.Thisprotocolisusedbyapplicationsneedingtosendlotsofdataquickly,likeanaudiostreamoravideogame.Thisistheprotocolwe'lluse.
ImagineyouhavetwoplayersneedingtocommunicatetheirpositionwitheachothersowedecidetouseUDP.Youmaysendmessagesbackandforthseveraltimesasecondtocommunicateyourpositions.Sinceyouaresendingdatasorapidly,ifoneofthosemessagesislostthentheplayerpositioncanbere-synchronizednextmessage.Thisisfastandunlessoneoftheplayershasafaultyinternetconnectionyoutypicallywon'tnoticeasmalljitterorhiccupeverynowandthen.
Nowimagineanotherscenariowherewewanttosendamessagethataplayergainedanextralife.IfwewereusingUDPandthatmessagegotlost,wecouldhavetwoonlineplayerswithout-of-syncinformationthatwouldultimatelyjeopardizegameplay.OnesolutionaroundthiswouldbetouseTCPformission-criticalmessagesandUDPforeverythingelse.AnothersolutionistokeepallmessagesinUDP,buttowriteacallbackinLuaaroundourmission-criticalmessagestocheckthatwegetareply.Yup,youcanhaveyourapplicationsendUDPmessagesandexpectaresponsebuteventhoughUDPdoesn'thavethisfeatureaspartofitsprotocolyoucanstillprograminyourapplicationatimeoutthatexpectsaresponse.Thissoundslikealotofwork,butLuaandmanyotherlanguageshavelibrariesavailableyoucanrequireinyourprojectthatdothisforyou.We'llseehoweasythisislateron.
ApplicationlayerFinallywehavetheprotocolwecreateforeachrunningcopyofagametoknowhowtocommunicateonceaconnectionisestablished.Forinstanceifamessagewiththestring "ping"isbeingreceived,wemaywanttorespond "pong".Themorecomplicatedthegameis,themorecomplicatedtheprotocolwillbe.Let'scheckoutoneofthelibrariesLuaoffersfornetworkingandbuildatestprogramwithabasicapplicationprotocolwheretheserverrespondsto "meow"with "bark"andtheclientrespondsto "bark"with "meow".Asyoucanguessthiswillleadtoaninfiniteback-and-forthconversationbetweenthetwohostsifwearesuccessful.
ENetThereareseveralthird-partylibrariesforLuafornetworking.LÖVEincludestwoofthemostpopular,LuaSocketandlua-enet.LuaSocketisveryflexibleandallowsyoutocreateTCPandUDPconnections.Lua-enetisbuiltontopoftheENetlibrary,asimpleyethighperformancenetworkinglibrary.ItusesUDP,buthandleseverythingaroundthetransportlayerforussowecanfocusonourapplicationlayer.ItevendoesmessageconfirmationoverUDPforuswhenweneedittosowegetthebestofbothworlds.Let'screateaserverandclientprograminLÖVEandwe'llrunthemseparately,connectingthemtoeachother.
OurserverapplicationCreateafoldercalled serverandinitcreateafilenamed server.lua.We'llstartbyrequiring enet:
--server/server.lua
localenet=require('enet')
Thisfilewillreturnatableoffunctionsforstartingandstoppingtheserver.Tostarttheserver,weneedtocreateahostandtellitwhichIPaddressandportitisrunningon.Let'screatea server.startfunctionthatdoesjustthat:
--server/server.lua
localenet=require('enet')
2.17-Networking(part1)
122
localhost
localserver={}
server.start=function()
host=enet.host_create('127.0.0.1:6789')
end
returnserver
TheIPaddressis 127.0.0.1aswesaidwewoulduse.ThatistellingENetwewanttostarttheserveronourmachine'slocaladdress.TheIPaddressisfollowedbyacolon( :)thentheportnumber( 6789)whichisanarbitraryportthatshouldbefreetouse.Ifwecreateamain.luafilewecanrequireserver.luaandcreateaserverwhenLÖVEstarts.
--server/main.lua
--Ourserverapplication
localserver=require('server')
love.load=function()
server.start()
end
Ifwetryandrunthis,nothingwillhappen.Let'sdefine love.drawandprintsometexttotelluswhensomeoneconnectstoourserver:
--server/main.lua
--Ourserverapplication
localserver=require('server')
love.load=function()
--Keeptextpixelssharpandintactinsteadofblurring
--https://love2d.org/wiki/FilterMode
love.graphics.setDefaultFilter('nearest','nearest')
server.start()
end
love.draw=function()
--Scaleupthesizeofthetextbeingprinted
localtransform=love.math.newTransform(0,0,0,3)
ifserver.is_connected()then
love.graphics.print('clientconnectedtous(seeconsole)',transform)
else
love.graphics.print('serverstarted...awaitingclients',transform)
end
end
--It'sconvenienttobeabletopressescapetoclosetheprogram
love.keypressed=function(pressed_key)
ifpressed_key=='escape'then
love.event.quit()
end
end
Withthisdone,weneedtofigureouthowtheserverknowssomeoneisconnected.Wecall server.is_connected()inlove.draw,solet'sstartbydefiningthat:
--server/server.lua
localenet=require('enet')
localhost
2.17-Networking(part1)
123
localreceived_data=false
localserver={}
server.start=function()
host=enet.host_create('127.0.0.1:6789')
end
server.is_connected=function()
returnreceived_data
end
returnserver
Ok,so server.is_connected()willreturnthevalueof received_datawhichdefaultsto false.Nowthepartthatdoesalltheaction:
--server/server.lua
localenet=require('enet')
localhost
localpeer
localreceived_data=false
localserver={}
server.start=function()
host=enet.host_create('127.0.0.1:6789')
end
server.is_connected=function()
returnreceived_data
end
server.update=function()
ifnothostthenreturnend
localevent=host:service()
ifeventthen
received_data=true
peer=event.peer
print('----')
fork,vinpairs(event)do
print(k,v)
end
event.peer:send('bark')
end
end
returnserver
Let'stakeacloselookat server.updatepiecebypiece.Firstthingisan ifstatementtocheckthat hostisdefined.If server.updateiscalledbefore server.startthenitwon'tbesothereisnoserverupdatetobemade.Ifourserverhostwascreatedandwegetpasttheif-statementcheck,wecall host:service().Ifwereadthedocumentationfor host:servicewecanseethepurposeofcallingthisistocheckforanyincomingpackets(messages)andsendoutanywehavequeuedup.Ifwereceiveany,wewillgetbackan eventtable.Ifwedogetaneventtable,we'llchange received_datato true(whichinturnmeans server.is_connected()nowreturns true).Nextwewillcapturethepeer(theclient)thatsentusthisdatawhichwecanusetosendmessagestolater:
peer=event.peer
Whilewehavetheeventtable,let'sjustiterateoveritandprintitscontentstotheconsole:
fork,vinpairs(event)do
print(k,v)
2.17-Networking(part1)
124
end
Thenfinallywe'llsendtheclientamessagethatsimplyreads"bark".
event.peer:send('bark')
Wecannowcall server.updateinsideourgameloop's love.updatefunction:
love.update=function()
server.update()
end
Weneedtotestourserver,buttotestourserver,weneedaclient.
OurclientapplicationCreatea"client"folderlikethe"server"foldercreatedabove.Mostofthecodewillbeidenticaltoourserver.Themaindifferenceisthatwhenwecreateahostwewon'tpassitanIPaddressandporttoserveon,butinsteadwilltellittoconnecttotheaddressandporttheserverisrunningon.
--client/main.lua
--Ourclientapplication
localclient=require('client')
love.load=function()
--Keeptextpixelssharpandintactinsteadofblurring
--https://love2d.org/wiki/FilterMode
love.graphics.setDefaultFilter('nearest','nearest')
client.start()
end
love.draw=function()
--Scaleupthesizeofthetextbeingprinted
localtransform=love.math.newTransform(0,0,0,3)
ifclient.is_connected()then
love.graphics.print('connectedtoserver(seeconsole)',transform)
else
love.graphics.print('establishingaconnection...',transform)
end
end
love.keypressed=function(pressed_key)
ifpressed_key=='escape'then
love.event.quit()
end
end
love.update=function()
client.update()
end
--client/client.lua
localenet=require('enet')
localclient={}
localhost
localpeer
localreceived_data=false
client.start=function()
2.17-Networking(part1)
125
host=enet.host_create()
peer=host:connect('127.0.0.1:6789')
end
client.is_connected=function()
returnreceived_data
end
client.update=function()
ifhostthen
localevent=host:service()
ifeventthen
received_data=true
print('----')
fork,vinpairs(event)do
print(k,v)
end
event.peer:send('meow')
end
end
end
returnclient
Ifwereceiveamessagefromtheserverwe'll"meow"backatit.
TestingthingsoutIfyouruntheserveryouwillseeamessagesaying"serverstarted...awaitingclients".Sinceweareprintingtotheconsole,ifyouarerunningthiscodeonWindowsrememberthatyouwillneedtoenabletheconsole.Thiscanbedonebycreatingaconf.luafileinboththeclientandserverfolders.
--LÖVEconfigurationfile
love.conf=function(t)
t.console=true--EnablethedebugconsoleforWindows.
t.window.width=800--Game'sscreenwidth(numberofpixels)
t.window.height=600--Game'sscreenheight(numberofpixels)
end
Iftheserverisupandrunningwiththeconsoleenabled,goaheadandstarttheclientwithitsconsoleenabledtoo.Youshouldimmediatelyseeafloodofeventsprintingoutintheserverandclientconsoles.
Serverconsole:
----
peer127.0.0.1:58384
channel0
datameow
typereceive
Clientconsole:
----
peer127.0.0.1:6789
channel0
databark
typereceive
2.17-Networking(part1)
126
Thiswillgobackandforthuntilyoucloseeitheroneofthem.Ifyoucloseonethough,themessageswillstopanditwilljustsitthere.Ifyouclosetheserverfirst,forinstance,theclientwillsittherethenafterseveralsecondsamessagewillappear:
----
peer127.0.0.1:6789
data0
typedisconnect
Normallyadisconnectlikethiswouldn'tbedetectedwithUDP,buttheENetlibrarysends"heartbeat"messagesbackandforthtomakesurebothpeersarestillconnectedtoeachother.Thetimeoutisdefinedtobesomewherebetween5and30secondsbeforethepeerrealizesithasbeendisconnnectedfromtheotherone.Justtopolishthingsoffhere,let'smakeENetsendadisconnecteventtotheotherpeerimmediatelywhenweareclosingourapplication.Thelua-enetdocumentationlistsafunctionwecaninvoketodothat, peer:disconnect_now.LÖVEhasa love.quitcallbackthatiscalledwhenourapplicationisclosing.Wecanwritea server.disconnectfunctionandcallitfromlove.quit.
Server:
--server/main.lua
...
love.quit=function()
server.disconnect()
end
--server/server.lua
...
server.disconnect=function()
ifpeerthen
peer:disconnect_now()
peer=nil
end
host=nil
received_data=false
end
The client.disconnectcodewouldbeidentical.
ToseethisfullexampleorifyouhaveanyproblemsgettingyourcodetoruncheckoutthecodeonGitHub:https://github.com/RVAGameJams/learn2love/tree/master/code/networking-1
Inthenextpartwewilllookatnetworkarchitectureandaddentitiestothescreentoworkwith.
ExercisesWhathappensifyoutrytoconnectmultipleclientstotheserver?WhataboutrunningmultipleserversonthesameIPaddressandport?Whydoesitbehavelikeitdoes?
2.17-Networking(part1)
127
Networking(part2)Intheprevioussectionwemadetwoapplicationsthatcouldtalktoeachother.Oneapplicationwastheserverandthesecondoneconnectingtoitwastheclient.Ingamedesignthisstyleofnetworkingcanbedescribedasadirectconnection.
Directconnection
Inadirectconnectiononeoftheplayerstakesontheroleofserver,meaningtheirgameworldistheultimateauthorityifthereareanydiscrepanciesorout-of-synccommunicationbetweenthetwo.Thisalsomeanstheserverplayercanfindwaystocheatandexploitthegame.
Oneoftheadvantagestothissetupissinceyouaredirectlyconnectedtoeachother,yougetasminimallagaspossible.Thisadvantagedoesn'tholdtrueiftherearemorethan2players.Ifplayer1istheserverandplayer2and3areconnectedtoplayer1,thenplayer2and3havetorelayupdatestoeachotherthroughplayer1insteadofdirectlytoeachother.Outsideof2-playergames,thissetupisn'taspopularashavingadedicatedserver.
Dedicatedserver
2.18-Networking(part2)
128
Dedicatedserversareexactlywhattheysoundlike.Theyarehostsdedicatedtoservingplayers.Thedifferencehereisallplayersareclientsandtheserverisaneutralgroundwhereplayerscanconnectandcommunicateindirectlywitheachotherthroughit.Serverstypicallyrunamodifiedversionofthegamecodethathasnouserinterfaceandthereforecanrunonalessexpensivecomputer.Ifoneoftheplayersisdetectedcheatingtheservercandetectthatsomethingiswrongandkickthemfromthegame.Theserveristheultimateauthorityoverthestateofthegameworld.
Ournetworksetupwillsortofbeamixbetweenthetwostylesofnetworking.We'llhaveadedicatedserverthatdoesn'tparticipateinthegameplay,buttheserverwillhaveagraphicalinterfacesowecanviewwhatisgoingonduringourtesting.
ConsolidatingourcodeRatherthanmanagingtwofoldersofcodelikeintheprevioussection,we'llcombinethecodeanduseamenusystemtoselectbetweenbeingaserverandbeingaclient.Themenucodeisn'timportanttothistutorialsotrytofocusontheclientandservercodeasbefore.Therefactoredcodecanbefoundinthecoderepositoryhere.
Giventheamountoffilesitiseasiesttodownloadthezipofthewholeprojectwhereyouwillfindtherelevantfilesinside code/networking-2:https://github.com/RVAGameJams/learn2love/archive/master.zip
Oncedownloaded,whenyouruntheprogramyoushouldbegreetedwithamenuscreenlikeso:
2.18-Networking(part2)
129
Testitoutandconfirmyoucanconnectaserverandclientinstancewiththenewcode.
Ok.Themodificationsto main.luashouldbeeasyenoughtounderstand.Let'stakealookatthatandthenew"net"servicefirstbeforewebeginmakinganymodifications.Atthetopofthefilewe'reloadingthenetandmenuservicesthentellingthemenuservicewhichmenutoloadonstartup:
--main.lua
localmenu_service=require('services/menu')
localnet_service=require('services/net')
love.load=function()
--Keeptextpixelssharpandintactinsteadofblurring
--https://love2d.org/wiki/FilterMode
love.graphics.setDefaultFilter('nearest','nearest')
menu_service.load('main-menu')
end
Next,ifakeyispresseditwillpassthatpressed-keyeventtothemenuservice.Ifweareinthegameandnomenuisloaded,themenuservicewilldonothingwiththeevent.
love.keypressed=function(pressed_key)
menu_service.handle_keypress(pressed_key)
end
Inside love.drawwehaveasimilarstory.Ifwehaveanactivemenuthen menu_service.draw()willdrawitOtherwiseitwon'tdoanything.(Ifyouopen services/menu.luayouwillseethedrawfunctionwherethisallhappens.)
2.18-Networking(part2)
130
love.draw=function()
menu_service.draw()
--Scaleupthesizeofthetextbeingprinted
localtransform=love.math.newTransform(0,0,0,3)
ifnet_service.is_connected()then
love.graphics.print('peerconnected(seeconsole)',transform)
end
end
Anotherthinginside love.drawisachecktoseeifwe'vemadeaconnectioneitherasserverorclient(usingnet_service.is_connected())thendrawthe"peerconnected"textonthescreenasbefore.Weusetheword"peer"asagenerictermtorefertoeithertheclientwe'reconnectedto(ifwe'retheserver)ortheserver(ifwe'retheclient).
Inside love.updateand love.quitwehavecombinedthecodewehadbeforeandaddeda menu.update()call.Ifthereisamenu,updateit.Ifeitheraserverhostorclienthostisrunning, net.update()willupdateit.
love.update=function()
menu_service.update()
net_service.update()
end
love.quit=function()
net_service.disconnect()
end
Soeverywherewewerecalling"server"or"client"wejustcall net_serviceanditwilldoit'sthingnomatterthetypeofconnection.Let'sopenup net.luaandwe'llseesomethingveryclosetotheoriginalcode:
--net.lua
localenet=require('enet')
--Populateoneortheotherdependingifwestartaserverorclienthost
localclient_host
localserver_host
--Asaserver,wewanttokeeptrackofalltheconnectedclients
localpeers={}
localreceived_data=false
--Theservicewewillbereturning
localnet={}
Atthetopofthefilewecreatesomeemptylocalvariables.The nettableisfulloffunctionsthatarebeingusedinmain.luaandelsewhere.
net.start_server=function()
server_host=enet.host_create('localhost:6789')
end
net.start_client=function()
client_host=enet.host_create()
server_host=client_host:connect('localhost:6789')
end
Onthemenuwhenyouselect"Host"or"Join",the net.start_serverand net.start_clientfunctionsarebeingcalledrespectively.
Belowthataresomefunctionstocheckwhatkindofconnectionwehave:
2.18-Networking(part2)
131
net.is_connected=function()
returnreceived_data
end
net.is_client=function()
returnclient_hostandtrueorfalse
end
net.is_server=function()
returnserver_hostandnotclient_host
end
Thenfinallywehavethe updateand disconnectcodelikeweoriginallyhadintheserverandclientservices,butcombined.Oneadditiontothedisconnectfunctionisweareloopingoverthe peerslist.The peerslistexistsbecauseweareexpectingtohavemultipleplayersconnecttotheserverandiftheserverisrunning,itwillwanttodisconnectfromthemallwhenthegamequits.
CommunicationlayerBeforeweupdatethecode,let'sdiscussthefunctionalityanddrawoutthenetworkcommunicationforthatfunctionality.
WhenconnectingtotheserveryoushouldhaveacontrollableplayerspawnonscreenWhenyoumoveyourplayer,yourpeersshouldbeabletoseeyourplayermoveWhenotherpeersmovetheirplayers,youshouldbeabletoseethemmoveEachplayershouldlookdifferentsoyouknowwhichoneisyou
Client Server Description
(Createahost) (Createahost)Bothclientandserver'snetservicesarebooted.Nocommunicationhasbeenmadebetweenthetwoatthispoint.
Send"connect"messagetoserver
Clientsendsaconnectmessageautomatically.Thiswillbeinterpretedasarequesttojointhegame.
(Spawnentity)Servergeneratesandstoresalltheentitiesinthegame.Whenspawninganentity,associatetheclient'sconnectionIDwithit.
Respond"your-id|4177457821|100|500"
Serverrespondsbypassingtheclienta "your-id"message.Allmessagessentmustbestringssointhiscasewheremultiplevaluesneedtobeembeddedinthemessage,eachvalueisseparatedbyapipe("|")charactertohelpusre-separatethevalueswhenreceivingthemessageclientside.ThefirstvalueistheuniqueplayerIDthattheserverhasassignedtheclient,followedbytheXpositionthenYposition.
Send"peer-id|233142890|326|177"
Serversendsthenewclientotherpeersthatneedtobespawnedonscreen.IncludedaretheID,XandYpositionofthatplayer.
Send"move|233142890|327|177"
ServerislettingtheclientknowtheplayerwiththeID"233142890"haschangedposition.NoticetheupdatedXposition.
Send"move|233142890|328|177" Anothermoveupdate.
Send"move|100|502" Clientislettingtheserverknowitismovingitsplayertoo.
2.18-Networking(part2)
132
Send"peer-id|81850530|500|500" Anotherplayerhasjoinedtheserver.
AddingentitiesOnconnectionfromaclient,theserverneedstospawnentitiessolet'screateanentityserviceforhandlingallourentity-relatedneeds:
--entity.lua
localentity_service={}
--Allplayerentities
entity_service.entities={}
entity_service.spawn=function(player_id,x_pos,y_pos)
return{
--TODO:We'lladdacolorrandomizerlater
color={1,1,1,1},
id=player_id,
--TODO:We'lladdashaperandomizerlatertoo
shape=love.physics.newPolygonShape(0,0,50,0,50,50,0,50),
x_pos=x_pos,
y_pos=y_pos
}
end
entity_service.draw=function(entity)
love.graphics.setColor(entity.color)
localpoints={entity.shape:getPoints()}
foridx,pointinipairs(points)do
ifidx%2==1then
points[idx]=point+entity.x_pos
else
points[idx]=point+entity.y_pos
end
end
love.graphics.polygon('line',points)
end
returnentity_service
Theseentitieswilljustbebasicshapes.Noworld,body,orfixturestoworryaboutasdealingwithphysicsisabitoutofthescopeofthissection.
Nowwe'lldosomeheavyupgradestothenetservice.Somewherenearthetopofthefilewe'lldefinetwotableswithcallbacksthatwillgetinvokedwhenaneventcomesin.They'llbeemptyfunctionsfornowandwe'llfillthemoutaswego.
--Callbackstoinvokewhencertaineventsarereceivedfromapeer
--Defineacallbacktohandleeverytypeofmessageinourapplicationprotocol
localmessage_handlers={
['your-id']=function(message,event,is_server)
end,
['peer-id']=function(message,event,is_server)
end,
['move']=function(message,event,is_server)
end
}
--TheseeventtypesaredefinedbyLua-enet.A"receive"typeofevent
--isagenericeventthatcarriesanyofthemessagesabove.
2.18-Networking(part2)
133
localevent_handlers={
connect=function(event,is_server)
end,
disconnect=function(event,is_server)
end,
receive=function(event,is_server)
end
}
Thenwe'llmodifythe net.updatefunctionsoitcancalloneofthethreecallbacksinsidethe event_handlerstable:
net.update=function()
localhost=client_hostorserver_host
ifnothostthenreturnend
localevent=host:service()
ifeventthen
received_data=true
--event.typewillbeeither"connect","disconnect",or"receive"
event_handlers[event.type](event,net.is_server())
--Printouttheeventtablefordebugpurposes
print('----')
fork,vinpairs(event)do
print(k,v)
end
end
end
Soyousee, event_handlers.connectwillbecalledwhena"connect"typeeventcomesinandwe'llpassittheeventand net.is_server()booleanasitstwoparameters.Nowwecangobackandfilloutthe"connect"eventhandler.Readeachcodecommentasthereisalotgoingonhereinjustafewlinesofcode.
localevent_handlers={
connect=function(event,is_server)
--Onlytheserverneedstodostuffhereonconnect
ifis_serverthen
--event.peer:connect_id()providesuswithauniquenumber.
--We'llconvertthatnumbertoastringanduseitastheplayerID.
localplayer=entity_service.spawn(tostring(event.peer:connect_id()),100,100)
--StorethisplayerintheplayertablewiththeplayerIDasthekey.
entity_service.entities[player.id]=player
--Sendtheinitial"your-id"messagebacktotheconnectingclientsotheycanspawnthisentitytoo.
event.peer:send('your-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)
end
end,
disconnect=function(event,is_server)
--TODO:Addcodetoremoveentitieswhenaclientdisconnects
end,
receive=function(event,is_server)
--TODO:Addcodetoparse"receive"eventsandcalltheappropriatemessagehandlercallback
end
}
Sincewe'reusingtheentityserviceinnet.luawe'llneedtorequireitattheverytop:
localentity_service=require('services/entity')
Nowthattheplayerreceivesthemessagetospawnanentity,wecanfilloutthe"receive"eventhandler.
localevent_handlers={
connect=function(event,is_server)
--Onlytheserverneedstodostuffhereonconnect
ifis_serverthen
2.18-Networking(part2)
134
--We'llconvertthatnumbertoastringanduseitastheplayerID.
localplayer=entity_service.spawn(tostring(event.peer:connect_id()),100,100)
--StorethisplayerintheplayertablewiththeplayerIDasthekey.
entity_service.entities[player.id]=player
--Sendtheinitial"your-id"messagebacktotheconnectingclientsotheycanspawnthisentitytoo.
event.peer:send('your-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)
end
end,
disconnect=function(event,is_server)
--TODO:Addcodetoremoveentitieswhenaclientdisconnects
end,
receive=function(event,is_server)
--Extractthemessageoutfromtheeventandcalltheappropriatecallbackabove
localmessage={}
formatchin(event.data..'|'):gmatch('(.-)|')do
table.insert(message,match)
end
message_handlers[message[1]](message,event,is_server)
end
}
Don'tletthiscodefeelintimidatingasit'sonlytakingthestringmessagefromtheevent( "your-id:87335:500:500")andsplittingitintoalisttable( {"your-id","87335","500","500"})sowecanworkwiththedata.Oncewehavethemessagefragments,wecall message_handlers[message[1]](),where message[1]willbeoneofthekeysinthemessage_handlerstable: "your-id", "peer-id",or "move".Let'sfilloutthe"your-id"handlerfirst:
--Callbackstoinvokewhencertaineventsarereceivedfromapeer
--Defineacallbacktohandleeverytypeofmessageinourapplicationprotocol
localmessage_handlers={
['your-id']=function(message,event,is_server)
localplayer_id=message[2]
localx_pos=message[3]
localy_pos=message[4]
entity_service.player_id=player_id
entity_service.entities[player_id]=entity_service.spawn(player_id,x_pos,y_pos)
end,
['peer-id']=function(message,event_is_server)
end,
['move']=function(message,event,is_server)
end
}
Nowwhentheclientgetsthe"your-id"message,itcanspawnanentitytoo.Let'saddtheappropriatecodetomain.luafordrawingentitiessowecanseeifourprotocolisworkingsofar:
--main.lua
localentity_service=require('services/entity')
...
love.draw=function()
menu_service.draw()
--Scaleupthesizeofthetextbeingprinted
localtransform=love.math.newTransform(0,0,0,3)
ifnet_service.is_connected()andnet_service.is_server()then
love.graphics.setColor({1,1,1,1})
love.graphics.print('Serverpreview.Seeconsolefordetails.',transform)
end
for_,entityinpairs(entity_service.entities)do
entity_service.draw(entity)
end
end
2.18-Networking(part2)
135
Ifyou'veupdatedeverythingcorrectly,you'llseethishappenwhenaserverandclientrunside-by-side:
Ifyougetanerror,remembertocheckthelinenumberandfilenamewheretheerroroccurredandmakesurethecodelookssimilartohowitisabove.Ifyougetstuck,therewillbeafullexampleavailableatthebottomofthissection.
Ifeverythingisworkingforyouthenfantastic.Thismeanstheserverandclientaresuccessfullysyncinganentitystatewitheachother.
Nextlet'saddmovementsowecanseeliveupdateshappeningbetweenthetwogamewindows.Insidetheservicesfolder,we'llcreateaninput.luafilethatreturnsanemptytable:
--input.lua
return{}
Whyarewereturninganemptytable?Well,it'sonlyemptyrightnow,butaskeysarepressedandreleasedourtablewillgetupdated.Let'supdate love.keypressedandadda love.keyreleasedfunctiontomain.luatoseehowthatworks:
--main.lua
localinput_service=require('services/input')
...
love.keypressed=function(pressed_key)
menu_service.handle_keypress(pressed_key)
input_service[pressed_key]=true
end
love.keyreleased=function(released_key)
input_service[released_key]=false
end
Nowwhenwepresssomearrowkeysforinstance,ourtableininput.luawilllookmorelikethisinmemory:
{
left=true,
right=false,
up=true,
down=false
2.18-Networking(part2)
136
}
Nowinsideentity.luawe'lladdan entity_service.movefunctionthatchecksforinputchangesandmovestheentityifanyofthearrowkeysarepressed:
--entity.lua
localinput_service=require('services/input')
...
entity_service.move=function()
localplayer=entity_service.entities[entity_service.player_id]
--Don'tlettheplayerpressupanddownatthesametime
ifinput_service.upandnotinput_service.downthen
player.y_pos=player.y_pos-2
elseifinput_service.downandnotinput_service.upthen
player.y_pos=player.y_pos+2
end
--Don'tlettheplayerpressleftandrightatthesametime
ifinput_service.leftandnotinput_service.rightthen
player.x_pos=player.x_pos-2
elseifinput_service.rightandnotinput_service.leftthen
player.x_pos=player.x_pos+2
end
end
Thiswillcauseourentitytomoveacrosstheclient'sscreen,buttheserverwon'tgettheseupdatesunlesstheclientsendsthemover.Weneedtogobacktomain.luaandsenda"move"message.Inside love.updatechecktoseeiftheplayerpositionhaschangedandsendamovemessagetotheserverifso:
love.update=function()
menu_service.update()
--Checktoseeifaplayerhasspawnedandupdateitsmovementifanydirectionkeysarebeingpressed
ifentity_service.player_idthen
localplayer=entity_service.entities[entity_service.player_id]
localold_x=player.x_pos
localold_y=player.y_pos
entity_service.move()
ifplayer.x_pos~=old_xorplayer.y_pos~=old_ythen
net_service.send('move|'..player.id..'|'..player.x_pos..'|'..player.y_pos)
end
end
net_service.update()
end
The net_service.sendfunctionwehaven'tdefinedthatyet.Jumpbackovertonet.luaandwe'lldefinethatforsendingeitherclientorservermessagesifneeded:
net.send=function(message)
ifnet.is_client()then
server_host:send(message)
else
server_host:broadcast(message)
end
end
2.18-Networking(part2)
137
Nowwearesendingamessagetotheserverwhenwemove,butweneedtohavetheserverreadthemessageandupdatetheentitypositiononitsendtoo.Let'sfilloutthe"move"messagehandlerinnet.luatoaccomplishthis:
localmessage_handlers={
['your-id']=function(message,event,is_server)
localplayer_id=message[2]
localx_pos=message[3]
localy_pos=message[4]
entity_service.player_id=player_id
entity_service.entities[player_id]=entity_service.spawn(player_id,x_pos,y_pos)
end,
['peer-id']=function(message,event,is_server)
--TODO:handlepeer-idmessagesforwhenmoreplayersjointheserver
end,
['move']=function(message,event,is_server)
localplayer_id=message[2]
localx_pos=message[3]
localy_pos=message[4]
entity_service.entities[player_id].x_pos=x_pos
entity_service.entities[player_id].y_pos=y_pos
ifis_serverthen
--Relaythismessagetotheotherplayers
forid,peerinpairs(peers)do
ifid~=player_idthen
peer:send(event.data)
end
end
end
end
}
Noticetheextraservercheck.Iftheserverreceivedthe"move"commanditshouldrelayitovertoanyotherconnectedplayersotheycanseeyoumovingtoo.The peerstableneedstobedefinedinsidenet.luaabovethemessagehandlersoryoumaygetanerrorwhenittriestoloopoverthetableandseesanilvaluethathasn'tbeendefinedyet.
Tryitoutagainandweshouldseethesquaremovingonbothscreensnow.
Allthathardworkisstartingtopayoff!Wehaveafewmorechangestomakeforthistobefullyfunctionalthough.Ifyoutryandrunthegamenowwithmultipleclients,theplayerswon'tseeeachother.Inourapplicationprotocolwedefineda peer-idmessagesothatwhennewplayersconnectwereceiveinformationtospawnthem.
Insidenet.lua,goaheadandfilloutthe"peer-id"messagehandlersothatitspawnsanentitywheninvoked:
['peer-id']=function(message,event,is_server)
localplayer_id=message[2]
localx_pos=message[3]
localy_pos=message[4]
entity_service.entities[player_id]=entity_service.spawn(player_id,x_pos,y_pos)
end,
Nowtheserverneedstosendallthepeerstoanewplayerconnecting,butitalsoneedstosendnewplayerstopre-existingpeersduringaconnectevent.We'llupdatethe"connect"eventhandlertodothat:
connect=function(event,is_server)
--Onlytheserverneedstodostuffhereonconnect
ifis_serverthen
--event.peer:connect_id()providesuswithauniquenumber.
--We'llconvertthatnumbertoastringanduseitastheplayerID.
localplayer=entity_service.spawn(tostring(event.peer:connect_id()),100,100)
--StorethisplayerintheplayertablewiththeplayerIDasthekey.
entity_service.entities[player.id]=player
2.18-Networking(part2)
138
--Sendtheinitial"your-id"messagebacktotheconnectingclientsotheycanspawnthisentitytoo.
event.peer:send('your-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)
--Letalltheotherpeersknowaboutthisplayer
for_,peerinpairs(peers)do
localpeer_player=entity_service.entities[tostring(peer:connect_id())]
peer:send('peer-id|'..player.id..'|'..player.x_pos..'|'..player.y_pos)
event.peer:send('peer-id|'..peer_player.id..'|'..peer_player.x_pos..'|'..peer_player.y_pos)
end
--Addthispeertothepeerlist
peers[tostring(event.peer:connect_id())]=event.peer
end
end,
Aftersendingthenewclienttheiridas"your-id",wesendthatidtoeveryotherclientas"peer-id"intheforloop.Noticeweaddthenewconnectingclienttothepeertableattheveryend.Wedothisafterloopingoverthepeerlistsowedon'tsendthatclienta"peer-id"messageofthemselves.Again,whenregisteringpeersandentitieswecalltostring()onthe connect_id()toensurewearestoringIDsasstringsratherthannumbers.Storingdataintablesitmatterswhetheryoustorethemaskeysornumbers.See1.14-Tables(part2)toseewhatImean.
Anyways,tryrunningthegamenowwithmultipleclientsconnectingtotheserverandyouwillseeeachentitycanmoveseparatelyandthechangeswillbesynchronizedacrossallclients.Onechangetomaketheentitieseasiertodistinguishwouldbetorandomizetheircolorandshape.Let'smodify entity_service.spawninsideentity.lua:
entity_service.spawn=function(player_id,x_pos,y_pos)
localcolors={
{1,0,0,1},
{0,1,0,1},
{0,0,1,1},
{0,1,1,1},
{1,0,1,1},
{1,1,0,1},
{1,1,1,1}
}
localshapes={
love.physics.newPolygonShape(25,0,50,50,0,50),
love.physics.newPolygonShape(0,0,50,0,50,50,0,50),
love.physics.newPolygonShape(12,0,36,0,49,15,49,33,36,49,12,49,0,33,0,15)
}
return{
--Cyclethroughthelistofcolorsbasedonwhatevertheplayeridis
--Callingtonumber()tomakeplayer_idanumberinsteadofastringsowecandomathonit
color=colors[(tonumber(player_id)%#colors)+1],
id=player_id,
shape=shapes[(tonumber(player_id)%#shapes)+1],
x_pos=x_pos,
y_pos=y_pos
}
end
Hereweuseamodulustocyclethroughthelistofcolorsandshapesandassignonebasedonthepseudo-randomplayerIDwereceived.Thisalsomeansaplayerwilllookthesameoneveryotherplayers'client.Tryrunningitagainandyoushouldseesomethingsimilar:
2.18-Networking(part2)
139
Thefinalnetworkingcodecanbefoundhere.Again,giventheamountoffilesifyouneedthewholefolderthenitiseasiesttodownloadthezipofthewholeproject.Youwillfindtherelevantfilesinside code/networking-3:https://github.com/RVAGameJams/learn2love/archive/master.zip
ConclusionThisisaboutasbasicinfunctionalityasamultiplayergamecanget.Therearemanyfeaturesmissingfromourcode.Justtonameafewmajorones:
Sanitychecksonmessages.Youcancrashtheserverbysendingitaninvalidmessage.The"move"messageexpectsaplayeridwhentheservershouldbeabletofigurethisoutonitsown.Theservercaneasilybefooledintoaccepting"move"messagesfromaclienttomoveanotherclient.Tomakethingssimplerthereisnoworldorphysicsnoranynetcodetohandlecollisionsoranythingofthelike.Theentitiesjuststickaroundwhenyouclose/disconnectaclient.Ifyouloseconnection,thereisnoattempttorestoretheconnection.
ThereisagreatsetofarticlesbyGabrielGambettaontheproblemsfacedbymakingaaction-basedmultiplayergamesandit'sworthareadtogetahigh-leveloverviewofthechallengesandhowtoreasonaboutthem.Linktohisarticles.
ExercisesHavingalltheplayersspawnattheexactsamepointisn'tideal.Insidenet.luainthe"connect"eventhandler,makealistofspawnpointsandmaketheplayersspawnatoneofthepointsbasedontheir peer:connect_id().Hint:thecodeshouldlooksimilartothecolorcyclecodeinside entity.spawninentity.lua.Insidenet.lua,completethe"disconnect"eventhandlerandmakeissowhenaclientquitstheirentityisremovedfromthegameforallpeers.
2.18-Networking(part2)
140
Chapter3:ProgrammingindepthThegoalofthischapteristotouchonavarietyoftopicsandproblemsfacedwhileprogrammingandtounderstandandsolvethemusingLua.Awidervarietyoftopicswillbecoveredhere.Onetopicwillleadintoanotherwhichwillthenbuildontopofconceptsintroducedintheprevioustopic.
3.0-Programmingin-depth
141
PrimitivesandreferencesTakealookatthiscode:
localstring1="hello"
localstring2='hello'
print(string1==string2)
localnumber1=14
localnumber2=14
print(number1==number2)
localtable1={}
localtable2={}
print(table1==table2)
localfunction1=function()end
localfunction2=function()end
print(function1==function2)
Whatwouldhappenifyouweretorunthis?
Inchapter1welearnedaboutcomparingstringswiththe ==operatorwhenwetalkedaboutbooleans.RunthecodeaboveintheREPLandseewhatitreturns:
true
true
false
false
Thestringsequalandthenumbersequal,butwhyaren'tthetablesandfunctionsequalsincetheyarebothempty?Tryprintingthetablesandfunctionsandlookwhathappens:
localtable1={}
localtable2={}
print(table1)
print(table2)
localfunction1=function()end
localfunction2=function()end
print(function1)
print(function2)
table:0x16af270
table:0x16af220
function:0x16ae840
function:0x16aeff0
Attemptingtoprinteachvalueyouaregivenbackahexadecimalnumber,theplaceinmemorywherethosevaluesarelocated.Eachtableandfunctionresidesinadifferentplaceinmemory.Sohowisthisrelevant?
3.01-Primitivesandreferences
142
Whencheckingdatalikestringsandnumbers,the ==operatordoesindeedcheckthatthedatamatches.Thesedatatypesaresimpleandtakeverylittleeffortforacomputertocheckthattheyareequal.Booleans,strings,numbers,and nilareallprimitivetypesofdataandbehavethisway.
Whencheckingdatalikefunctionsandtables,however,the ==operatorchecksthememorylocationofthedataonbothsidesoftheoperatorandifthevariablesreferencethesamelocationthentheyareequal.Inotherwords,the==operatorchecksthesedatatypestoseeiftheyhavethesameidentity.Nomatterhowmanyemptytablesorfunctionsyouhave,eachoneiscreatedwithauniqueidentity.
localstring1='hello'
localstring2="hello"
--Anothercopyof"hello"iscreatedinmemory:
localstring3=string2
--Butthesetwocopiesareequal
print(string2==string3)
localtable1={}
localtable2=table1
print(table1==table2)
Whatistheresultof print(table1==table2)?Aha!Boththesevariablesreferencethesamedata.Quick–amagicianwavestwowandsinfrontofyourfaceandasksyoutocounthowmanywandsthereare.Howdoyouknowiftherearereallytwowandsorifthisisjustatrickwithmirrors?Whatdoyoudo?Youtakeoneofthewandsandbreakitofcourse.Iftheotherwandbreaksthentheywerethesamewandtheentiretime.Let'strythatwiththetwoobjects:
localtable1={}
localtable2=table1
table1.rabbit='white'
print(table2.rabbit)--Equals'white'too
Aslongasyourvariablesreferencethesametable,updatingthetablefromonevariableyouwillseetheresultwhencheckingtheothervariable.Thisdoesn'tworkwithprimitivedatabecauseyou'realwaysmakingacopywhenassigningittoanewvariablename:
localstring1='hello'
localstring2=string1
string1='world'
returnstring2
=>hello
Primitiveversusnon-primitivedatatypesWheneverweassignnon-primitivedatatoanewvariable,we'realwaysreferencingtheoriginaldata:
localgrocery_list={
'carrots',
'celery',
'pecans'
}
localsame_list=grocery_list
3.01-Primitivesandreferences
143
grocery_list[1]='grapes'
returnsame_list[1]
Butassigningprimitivedatatoavariable,evenprimitivedatainsidetables,we'realwaysmakingauniquecopy:
localgrocery_list={
'carrots',
'celery',
'pecans'
}
localitem_copy=grocery_list[1]
print('item_copyis'..item_copy)
grocery_list[1]='grapes'
print('item_copystillis'..item_copy)
Ifyouneedtomakeeachiteminyourtablereference-able,youneedtomakeeachitemanon-primitivedatatype:
localgrocery_list={
{name='carrots',location='produce'},
{name='celery',location='produce'},
{name='pecans',location='baking'}
}
localitem_reference=grocery_list[1]
print('item_referenceis'..item_reference.name)
grocery_list[1].name='grapes'
print('item_referenceisnow'..item_reference.name)
Soratherthanreplacingthefirstiteminthelist,thefirstitemwasretainedandonlymodified:
item_referenceiscarrots
item_referenceisnowgrapes
Cloningnon-primitivedatatypesAswearefamiliarwithatthispoint,tablesareaspecialdatatypethatcancontainotherdatatypes.Youcanbuildstructurescontainingstrings,variables,andevenothertables.Thatmakesthetableacompositedatatype,inotherwords,adatatypewithdistinguishableparts.Notalllanguageshavecompositedatatypes,butforLuathetableisoneofitsprimaryfeatures.
Onethingaprogrammermaywanttodowithatableisonceconstructed,createacopyofit.Iftherewasatableforamonsterinavideogame,youmaywanttohavemorethanonetable.Ifyoudidthis:
localenemy1={health=10,strength=12,type='orc'}
localenemy2=enemy1
Youwouldstillonlyhaveonetable.Youcouldusealooptocopyallthevaluesoutofatableandintoabrandnewtable.Afunctiontodothatmaylooklikethis,moreorless:
3.01-Primitivesandreferences
144
localcopy=function(orig_table)
localnew_table={}
forkey,valueinpairs(orig_table)do
new_table[key]=value
end
returnnew_table
end
localenemy1={health=10,strength=12,type='orc'}
localenemy2=copy(enemy1)
Thereisnothingterriblywrongwiththismethod,butamoreefficientwaytodosuchathingwouldbetoconstructeachmonstertableinsideafunctioninsteadofcopyingonefromanother.Thismethodwillbefamiliaralreadyifyoureadandfollowedthroughthebreakoutgame.
localcreate_orc=function(strength)
return{
health=10,
strength=strength,
type='orc'
}
end
localenemy1=create_orc(12)
localenemy2=create_orc(12)
Everytimethefunction create_orcisran,itconstructsanewtablefromscratch.Youdefineanorc-styletableonlyonceanddon'tneedtoreadvaluesinfromonetabletoanother.Afunctionthatconstructstablesforyouisacommonparadigminprogrammingknownasafactoryfunction.Youmadeafactorythatbuildsorcs!Ofcoursethisfactoryfunctionparadigmworkswithothernon-primitivetypesofdataaswell:
localcreate_function=function()
returnfunction()return1+1end
end
localfn1=create_function()
localfn2=create_function()
print(fn1)
print(fn2)
Afunctionthatgeneratesotherfunctions?Thismayseemlikeanoddthingtowanttodo,butthismethodofprogrammingcanbequiteusefulaswe'llseein3.02-Higher-orderfunctionsandlaterfollow-upsections.Onethingthatshouldbementionedthoughisthatfunctionscanalsobeconsideredacompositedatatypeasitcanreturnotherdatatypes,andevenotherfunctions.Compositeinthatyoucancomposehigher-orderfunctionalityinthewaytablescanbeusedtocomposehigher-orderstructures.
ConclusionWhencomparingorreferencingdata,alwayskeepinmindwhetheryouhandlingprimitiveornon-primitivedata.Ifyouaremodifyingdatainoneplace,thinkifthismightbeaffectingyousomewhereelseinyourprogram.Evenwhenwritingouta localsome_module=require('some-module')inyourcode some_moduleisjustatableandlikeeveryothertable,everyreferencetoitcanaffecteachother.Somodifying some_moduleintwodifferentfilescanhaveeitherbeneficialordisastrousconsequencesdependinghowmuchcareandregardyougiveyourcode.
3.01-Primitivesandreferences
145
Higher-orderfunctionsIn1.07Makingfunctionswelearnedabout,well,makingfunctions.Sowhatabouthigher-orderfunctions?Whataretheyandhowdowemakethem?Simplyput,higher-orderfunctionsarefunctionsbuiltontopofotherfunctions.Here'sabasicexample:
localrun_twice=function(some_function,some_data)
some_function(some_data)
some_function(some_data)
end
run_twice(print,'HelloWorld!')
Itcantakeanyfunctionandrunittwiceforyou,inthiscasethe printfunction,butitcouldbeanyfunctionyoupassit.Typicallyhigher-orderfunctionsreturndata.Here'satrickierexamplethatdoesjustthat:
localtwice=function(fn,val)
returnfn(fn(val))
end
localadd_four=function(num)
returnnum+4
end
returntwice(add_four,12)
Takealookatthebottomlineforasecond.Wearecallingthefunction twicewithtwoarguments,the add_fourfunctionandthenumber 12.Thepurposeofthe twicefunctionistotakeavalue, 12inthiscase,andrunitthroughthegivenfunction( add_four)twice.Nowtakealookinsidethe twicefunction.Insideitreturnsfn(fn(val)).Givenwhatweknowisbeingpassedtothisfunction,thiscanbereadassayingadd_four(add_four(12)).Theorderofoperationsaystostartfromtheinner-mostparenthesisandworkyourwayout:
add_four(add_four(12))
becomes
add_four(16)
whichbecomes
20
andthatiswhatisreturnedwhenyourunthecode.Thepowerofthesehigher-orderfunctionsisthattheyarere-usable.Youcangivethe twicefunctionanythingthattakesandreturnsavalue:
localtwice=function(fn,val)
returnfn(fn(val))
end
localdouble=function(number)
returnnumber*2
end
returntwice(double,3)
3.02-Higher-orderfunctions
146
...orsimilartoouroriginalexample:
localtwice=function(fn,val)
returnfn(fn(val))
end
localshout=function(message)
print(message..'!!')
returnmessage
end
returntwice(shout,'hello')
Thereareallexamplesofhigher-orderfunctionsthatacceptafunctionasanargument.Anotherkindofhigher-orderfunctionisonethatreturnsanotherfunction:
localwrapper=function()
returnfunction()
return'Youfoundthetreasure!'
end
end
localkinder_surprise=wrapper()
localsecret=kinder_surprise()
returnsecret
Whenweran wrapperitreturnedusanotherfunctionthatwehadtoinvoketogettotheinnermostvalue.Toavoidallthevariablenames,youcansavesometimeandinvokesuchkindsoffunctionslikeso:
localwrapper=function()
returnfunction()
return'Youfoundthetreasure!'
end
end
returnwrapper()()
ClosuresWhichnumberwillprintoutbyrunningthefollowingcode?
localnumber=3
localclosure=function()
localnumber=5
returnfunction()
print(number)
end
end
localprint_number=closure()
print_number()
Strange?
Ok,solet'stryathissamefunction-returning-a-functionthingbutpassinginsomedata:
3.02-Higher-orderfunctions
147
localadder=function(a)
returnfunction(b)
returna+b
end
end
localadd_three=adder(3)
returnadd_three(1)
The add_threevariableisassignedauniqueandspecialfunction.Itisassignedtheinnerfunctionwithintheadderfunction,butwiththedatawepassedinnowassignedtothe avariable.Eventhoughthefunctionwasreturnedoutsideofthescopeitwasdefinedin,thescope'sdatawasenclosedinsidethereturnedfunctionuntilthefunctionwasdiscardedandtheprogramexited.Thesetypesoffunctionsarecommoninsituationswhereafunctionneedstobegeneratedmultipletimesbutwithdifferentdatasets.
Thedataintheclosurecanalsocontinuetobeupdated,givingyoutheabilitytomakestoragecontainersforyourdata.Trythisout:
localmake_counter=function()
localnumber=0
returnfunction()
number=number+1
returnnumber
end
end
localcount=make_counter()
print(count())
print(count())
print(count())
print(count())
InprogramslikeLÖVEtherearecallbacksystemswhereasimilareffecthappens:
localentity=require('entity')
love.draw=function()
entity:draw()
end
Asseeninthepreviouschapter,the love.drawcallbackisdefinedinamain.luafileandlaterinvokedsomewherewithinthegameengine.Since love.drawwasdefinedinthescopewheretheentityvariableisdefined,theentityvariablelivesonandcanbeusedinside love.drawlongafterthemain.luafileisdonebeinginvoked.
ConclusionClosurestakesomepracticetounderstandandappreciate,butonceyouseepracticalexamplesofwhereandhowtousethemtheybecomeanindispensableitemonyourprogrammingtoolbelt.Intheprevioussectionweusedthetermcompositedatatocompareprimitiveandnon-primitivedatatypes.Inthissectionwesawhowtogoaboutcomposinghigher-orderfunctions.Inthefollowingpageswewillcoversomehigher-orderfunctionsthatarethebuildingblocksforoldandmodernsoftwarealike.
Exercises
3.02-Higher-orderfunctions
148
Inthe make_counterexampleabove,trygeneratingmultiplecounters:
--Dothenumbersineachcounterstayin
--syncoraretheytrackedindependently?
localcount_a=make_counter()
localcount_b=make_counter()
Usingthesame make_counterexample,modifyittoreturnatableinsteadofafunction.Withinthistable,definean incrementand decrementfunctionsothatyoucanmakethecounternumbergoupordown.Howwouldyouusesuchafunction?
3.02-Higher-orderfunctions
149
MapandfilterIntheprevioussectionwepracticedcreatingsomehigherorderfunctions.Inthissectionswe'llcomposetwohigher-orderfunctionscommonlyusedininternetapplicationsfortransforminglists.
We'llstartbytakingalookatourgrocerylisttoseewhatitemsweneedtopickup:
localgrocery_list={
{
name='grapes',
price='7.20',
location='produce'
},
{
name='celery',
price='5.50',
location='produce'
},
{
name='walnuts',
price='6.20',
location='baking'
},
{
name='sugar',
price='8.00',
location='baking'
},
{
name='mayonnaise',
price='3.50',
location='dressings'
},
{
name='cream',
price='3.00',
location='dairy'
}
}
Thislisthasmoreinformationthanwewanttoseeataquickglance.Ifwewantedtoonlydisplayanumberedlistofitemnames,wecoulddosobywritingafor-loopthatgeneratesanewlistforus:
localnew_grocery_list={}
forkey,valueinipairs(grocery_list)do
new_grocery_list[key]=key..'.'..value.name
end
for_,valueinipairs(new_grocery_list)do
print(value)
end
Herewegeneratedalistwithaloopthenloopedoverthelistagaintoprintourresults:
1.grapes
2.celery
3.walnuts
4.sugar
5.mayonnaise
6.cream
3.03-Mapandfilter
150
Thisworksgreatforsimplecodelikethisexample,butitcangetmessyifyouareworkingwithmanylistsorifyouwanttotransformliststodifferentformats.
MapHere'sourhigherorderfunction, map.Ittakesalistandafunctionasargumentsthenreturnsanewlist.
localmap=function(list,transform_fn)
localnew_list={}
forkey,valueinipairs(list)do
new_list[key]=transform_fn(value,key)
end
returnnew_list
end
Anewlistiscreatedbyloopingovereachitemintheoriginallist,applyingyourfunctiontotheitem,thenassigningthetransformeddatatothenewlist.Ourcodecanbere-writtentousethemapfunction:
localmap=function(list,transform_fn)
localnew_list={}
forkey,valueinipairs(list)do
new_list[key]=transform_fn(value,key)
end
returnnew_list
end
localgrocery_list={
{
name='grapes',
price='7.20',
location='produce'
},
{
name='celery',
price='5.50',
location='produce'
},
{
name='walnuts',
price='6.20',
location='baking'
},
{
name='sugar',
price='8.00',
location='baking'
},
{
name='mayonnaise',
price='3.50',
location='dressings'
},
{
name='cream',
price='3.00',
location='dairy'
}
}
localnew_grocery_list=map(grocery_list,function(item,index)
returnindex..'.'..item.name
end)
3.03-Mapandfilter
151
for_,valueinipairs(new_grocery_list)do
print(value)
end
Calling map(...)wegetbackthenewlistthenweloopoveritagainjusttoprintourresultsout.Noticehowthesecondargumentwepassedintomapisjustafunctionwithnoname.Functionswithnonamesaresometimescalledanonymousfunctions.Insomelanguagesthey'recalledlambdas,especiallywhenusedinsideahigher-orderfunctioninasituationlikethis.Thetransformfunctiontakesintheitemanditsindexandmustreturnbackanewresultformaptoputinsidethenewfunction.
Maybeafewmoreexampleswillhelpout,sowhatifwewanttoreturnanotherlistwithjustthepricessowecanadduphowmuchweneedtospend?
localprice_list=map(grocery_list,function(item)
print(item.price)
returnitem.price
end)
7.20
5.50
6.20
8.00
3.50
3.00
Herethemapfunctionispassedinatransformfunctionwithaprintstatementinsideit.Thatwayitwillprinttheitempricesasitbuildsthelistsoyoucanseewhateachvaluewillbe.
Ifyouhadotherlistsforwhichyouwantedtoprintprices,itcouldbedonequiteeasilywith map:
localtransform_fn=function(item)returnitem.priceend
map(grocery_list,transform_fn)
map(car_parts,transform_fn)
map(card_transactions,transform_fn)
FilterLet'ssaywewantedtoonlyseethethingsonourgrocerylistthatareinthebakingaisle.Wecouldwritealooptodothat:
localfiltered_list={}
for_,valueinipairs(grocery_list)do
ifvalue.location=='baking'then
filtered_list[#filtered_list+1]=value
end
end
for_,valueinipairs(filtered_list)do
print(value.name)
end
Tryrunningthatandonceitmakessense,let'sthinkabouthowtoturnthisintoare-usablehigher-orderfunctionlikemap.We'llmakeafunctioncalled filterthat,like map,takesalistandafunction.Thefunctionwillreturn trueifitwantstoputaniteminthenewlistor falseifitdoesn't.We'llcallitthepredicatefunction.
3.03-Mapandfilter
152
localfilter=function(list,predicate_fn)
localnew_list={}
forkey,valueinipairs(list)do
--Thepredicate_fnthatwaspassedinshouldreturn
--avaluethatevaluatestoeithertrueorfalse.
ifpredicate_fn(value,key)then
new_list[#new_list+1]=value
end
end
returnnew_list
end
Andwecanusethisfunctiontofilterdowntojustourbakingitemslikethis:
localfilter=function(list,predicate_fn)
localnew_list={}
forkey,valueinipairs(list)do
ifpredicate_fn(value,key)then
new_list[#new_list+1]=value
end
end
returnnew_list
end
localgrocery_list={
{
name='grapes',
price='7.20',
location='produce'
},
{
name='celery',
price='5.50',
location='produce'
},
{
name='walnuts',
price='6.20',
location='baking'
},
{
name='sugar',
price='8.00',
location='baking'
},
{
name='mayonnaise',
price='3.50',
location='dressings'
},
{
name='cream',
price='3.00',
location='dairy'
}
}
localfiltered_list=filter(grocery_list,function(item)
returnitem.location=='baking'
end)
for_,valueinipairs(filtered_list)do
print(value.name)
end
3.03-Mapandfilter
153
walnuts
sugar
Noticeourpredicatefunctionwewrote:
function(item)
returnitem.location=='baking'
end
Theoperationafterthe returnalwaysreturnsaboolean trueor false,so filterknowsexactlywhattodowiththeitembasedonthoseresults.
Youcanimaginethe filterfunctioncouldbeusefulforprocessingasearchquery.Forinstance,ifwewantedtoseeonlymedium-sizedshirtsthatfitaspecificpricerange:
filter(products,function(item)
ifitem.type=='shirt'then
ifitem.size=='M'then
returnitem.price<40
end
end
returnfalse
end)
CaveatsThefilterfunctionreturnsanewlist,buttheitemsintheliststillreferencetheoldlistiftheyaren'tprimitives.Forinstanceifwemodifiedthegrocerylist,thefilteredcopywouldbeupdated.
localfiltered_list=filter(grocery_list,function(item)
returnitem.location=='baking'
end)
grocery_list[3].name='peanuts'
print(filtered_list[1].name)
peanuts
Thisbehaviorcanbeadvantageousifit'sexpected,butit'ssomethingthatshouldbeunderstoodabouthowLuaandsimilarprogramminglanguageswork.Thisisexplainedmorein3.1-Primitivesandreferences.
Anotherthingtoconsideriswhetherornottowritethefunctionsyourselfortouseapre-writtenlibraryyoucanrequireintoyourproject.Notallimplementationsarethesameandsomemayperformbetterthanothers,orbehavedifferently.Somelanguageshavebuilt-inversionsofthesefunctionstostandardizethings.UnfortunatelyLuadoesn'tprovidethesefunctionsbuiltinorasastandardlibrary.
Atleastyounowknowhowtowritethemyourselfiftheneedarises.
ExercisesTryfilteringthegrocerylisttoonly"produce"items,thenmappingthoseresultsdowntojustthenames.Using filter,nowcanyoureturnthenumberofitemsinthegrocerylistwithapriceofmorethan5?Hint:youwillneedtousetonumber()toconverttheitempricestonumbersforcomparing.
3.03-Mapandfilter
154
StackandrecursionWhenrunningaprogram,theinterpreter(Luainthiscase)keepstrackofvariablesdefinedinascopeandwhichfunctionyouarecurrentlyin.Itorganizesthisinformationintoalistinmemorycalledthestack.Thefirstiteminthestackisthestartingpoint-therootofyourapplication.Takethefollowingexample:
localtwo=function()
print('two')
end
localone=function()
print('one')
two()
end
one()
Whenstartingtheprogram,thestartofthestackisthetoplevelofthemodule.TheLuastackcallsthisthe"mainchunk".Whenafunctionisinvoked,anotherlayerisaddedtothestack.Everytimeafunctioniscalledfromanotherfunction,thestackcontinuestobuild.Sowiththeexamplecodeabove,Thestackwillfollowtheprogression:
Stackis {"mainchunk"}.Nowstartexecuting one.Stackis {"mainchunk","one"}.Nowstartexecuting twowhilestillin one.Stackis {"mainchunk","one","two"}.twoisdoneexecuting.Stackisnow {"mainchunk","one"}.oneisdoneexecuting.Stackisnow {"mainchunk"}.Programexits.
Thiscanbevisualizedbythrowinganerroratanypointtheprogram.Theinterpreterwillgiveyoubackastacktracethatdetailswhereitwaswhentheproblemoccurred.Luaprovidesahelpful errorfunctionfordebuggingthatwecanusehere:
localthree=function()
error('Thisisanerror.')
end
localtwo=function()
print('two')
three()
end
localone=function()
print('one')
two()
end
one()
UnfortunatelytheREPLdoesn'tprovideuswithstacktraces,butifyouhaveaLuainterpreteronyourcomputer( luacommand, luajit,orLÖVE)youwillseetheerrormessageandastacktracelikethis:
lua:test.lua:2:Thisisanerror.
stacktraceback:
[C]:infunction'error'
test.lua:2:inupvalue'three'
test.lua:7:inupvalue'two'
test.lua:12:inlocal'one'
3.04-Stackandrecursion
155
test.lua:15:inmainchunk
[C]:in?
Fromthe"stacktraceback"youcanseethenewestfromthetopofthestacktotheoldestonthebottom.Incomplexprogramsiscanbeverybeneficialtoseewhichfunctioninvokedanotherfunctiontohelptracedownhowanerrorcameabout.
Understandingthestackisbeneficialformorethanjustreadingerrors.Let'sswitchtheconversationovertosomethingseeminglyunrelatedforabit.
RecursionWhenthinkingofloops,manyprogrammersfirstthinkofthe forlooporthe whileloop.Anothercommonmethodistomakeafunctioncallitself.Similartothe whileloop,youcancreateinfiniteloopslikethisone
localloop
loop=function()
print('hello!')
loop()
end
Whenafunctioninvokesitself,whetherdirectlyorindirectly,thisiscalledrecursion.Thesamefunctionwillrecuragainandagainuntilaconditionchanges.Orinthecaseabove, loop()willbecalledunconditionally.Withoutacondition,anykindofloopwillruninfinitely(orcrashtrying).Here'saloopthatisalittlesafertorun:
localcount_to_5
count_to_5=function(current_number)
print(current_number)
ifcurrent_number<5then
count_to_5(current_number+1)
end
end
count_to_5(1)
Whichprints:
1
2
3
4
5
Onequicklittleaside;Noticehowthefunctionwasdefinedinboththesesituations:
localloop
loop=function()
...
Thevariablewasdefinedbeforethefunctionwascreated.Sincethefunctionneedstoaccessthevariableinsideitself,thevariableneedstoexistatthetimethefunction'sscopeiscreated.Variablescreatedafterthefunctionareunknowntothefunction.Thisisdiscussedin1.17-ScopesandisalimitationofLua'sdesign.Fortunatelythereisshorthandsyntaxforwritingrecursivefunctions:
localfunctioncount_to_5(current_number)
print(current_number)
3.04-Stackandrecursion
156
ifcurrent_number<5then
count_to_5(current_number+1)
end
end
count_to_5(1)
isthesameaswriting:
localcount_to_5
count_to_5=function(current_number)
...
Let'stryanotherrecursiveloop:
localgrocery_list={
'pumpkin',
'pecans',
'butter',
'flour',
'sugar'
}
localfunctionprint_items(list,index)
index=indexor1
ifindex<=#listthen
print(list[index])
print_items(list,index+1)
end
end
print_items(grocery_list)
Whichprintsthegrocerylist.Don'tforgetthe localatthebeginningof localfunctionprint_items,otherwiseyouwillaccidentallygenerateglobalvariablesinyourcodewhentryingtodefinefunctions.
Wecanevenre-implementour mapfunctionfromearliertouserecursioninsteadofa forloop.
localgrocery_list={
'pumpkin',
'pecans',
'butter',
'flour',
'sugar'
}
localfunctionmap(orig_list,transform_fn,new_list)
new_list=new_listor{}
if#new_list<#orig_listthen
localindex=#new_list+1
new_list[index]=transform_fn(orig_list[index],index)
returnmap(orig_list,transform_fn,new_list)
end
returnnew_list
end
localnew_list=map(grocery_list,function(value,index)
returnindex..'.'..value
end)
map(new_list,function(value)
print(value)
returnvalue
end)
3.04-Stackandrecursion
157
Whichprints:
1.pumpkin
2.pecans
3.butter
4.flour
5.sugar
StackoverflowSowhatdoesthestacklooklikeduringrecursionwhenafunctionentersitself?Here'sascripttotest:
localfunctionrecur(n)
--assertislikeerror,buttakesanexpressiontotest.Ifthe
--expressionpassedbecomesfalsethenitthrowstheerrormessage.
assert(n<5,'Thisisaconditionalerror')
print(n)
recur(n+1)
end
recur(1)
lua:test2.lua:2:Thisisaconditionalerror
stacktraceback:
[C]:infunction'assert'
test2.lua:2:inupvalue'recur'
test2.lua:4:inupvalue'recur'
test2.lua:4:inupvalue'recur'
test2.lua:4:inupvalue'recur'
test2.lua:4:inlocal'recur'
test2.lua:7:inmainchunk
[C]:in?
Everytimethefunctionrecurswegetanotheradditiontothestack.Thiscanbeaproblemifyouareloopingoveralargesetofdatabecausethestackwillconsumemoreandmorememoryasitstacksup.Thiscanbeaccomplishedbycreatingarecursiveloopthatrunsinfinitely.Ifyouhaven'ttriedsoalready,here'saneasyexample:
localfunctionrecur()
recur()
end
recur()
Whenthestackreachesacriticalsize,yougetastackoverflowerror:
lua:test3.lua:2:stackoverflow
stacktraceback:
test3.lua:2:inupvalue'recur'
test3.lua:2:inupvalue'recur'
...
test3.lua:2:inupvalue'recur'
test3.lua:2:inupvalue'recur'
test3.lua:2:inlocal'recur'
test3.lua:5:inmainchunk
[C]:in?
Withaspecific returnstatementaddedtotheloop,however,wenolongergetastackoverflow:
3.04-Stackandrecursion
158
localfunctionrecur()
returnrecur()
end
recur()
Thiswillrununtilyoumanuallykilltheapplicationprocess.Killingitreturnsasomewhatmysteriousstacktrack:
lua:test4.lua:2:interrupted!
stacktraceback:
test4.lua:2:infunction<test4.lua:1>
(...tailcalls...)
test4.lua:2:infunction<test4.lua:1>
(...tailcalls...)
test4.lua:5:inmainchunk
[C]:in?
Sohowdidourmodificationsaveusfromoverflowingourstack?
TailcalloptimizationInsideafunctionwhenyoureturnanotherfunctioncall,theinterpreterhastheabilitytore-usethesamelayerofthestackinsteadofcreatinganotherlayer.Thisworkswithdirectrecursion(functioncallingitself)andindirect(mutual)recursionsuchastwofunctionscallingeachother:
localone
localtwo
one=function()
returntwo()
end
two=function()
returnone()
end
one()
ProgramminginLuagoesintogreaterdetailonwhenarecursionwillorwon'tbeoptimized,butthesimplethingtorememberisthatthefunction(s)mustreturnthevalueofinvokingafunctionforthistowork.Thefollowingwillbetail-calloptimized:
localone
localtwo
one=function(n)
print(n)
returntwo(n+1)
end
two=function(n)
print(n)
returnone(n+1)
end
--Countuntilwerunoutofnumbers
one(1)
Butthefollowingwon't,sinceitreturnsanoperationincludingthefunctioncallinsteadofjustthefunctioncallitself:
3.04-Stackandrecursion
159
localone
localtwo
one=function(n)
print(n)
return1+two(n)
end
two=function(n)
print(n)
return1+one(n)
end
--Thiswon'twork!
one(1)
ThecaseforrecursiveloopsSowhywouldwewanttodorecursion?Itseemstrickierthana forloopandperhapsjustaseasytomessupasawhileloop.
It'snotnecessarilyareplacementforthe forloop,butallowsyoutodocertainthingsyoucan'teasilydowithoutrecursion.TakethisexamplefromRosettaCodewhichwillflattenalistoflistsintoasingle,flatlist.Itusesa forloopandarecursiveloopinconjunctionwitheachother:
localfunctionflatten(list)
iftype(list)~="table"thenreturn{list}end
localflat_list={}
for_,eleminipairs(list)do
for_,valinipairs(flatten(elem))do
flat_list[#flat_list+1]=val
end
end
returnflat_list
end
localtest_list={
{1},
2,
{{3,4},5},
{{{}}},
{{{6}}},
7,
8,
{}
}
print(table.concat(flatten(test_list),","))
Whichprints:
1,2,3,4,5,6,7,8
Thisfunctionisn'ttail-calloptimized,butitprobablywon'tbepassedanestedlistdeepenoughtocauseastackoverflow.
Here'sjustafewofthemanysituationswhererecursionisusuallythebesttoolforthejob:
SortingdataSearchingtrees(nesteddata)inadatabaseornestedfoldersinafilesystem.
3.04-Stackandrecursion
160
FindingtheshortestpathbetweentwopointsLoopsthatincrementordecrementinirregularpatternsEvaluatingafinitesetofmovesinagamelikechess
Thepointisn'ttoreplacethe forloop,althoughyoucan.Takethefollowingexample,whichreturnsthefactorialofthegivennumber(5):
localfact=function(n)
localacc=1
foriteration=n,1,-1do
acc=acc*iteration
end
returnacc
end
print(fact(5))
Thesamefunctionalitywrittenwitharecursiveloopwouldlookverydifferent:
localfunctionfact(n,acc)
acc=accor1
ifn==0then
returnacc
end
returnfact(n-1,n*acc)
end
...butonemethodwouldn'tofferanadvantageovertheotherhere.Dependingonthelanguageyouareworkingin,onemethodmaybeeasiertoreadthantheother.Maybethelanguagesupportsonetypeofloopandnottheother.Thesearethefactorsthatwilloftendothedecidingforyou.
3.04-Stackandrecursion
161
Reduce(fold)Inprevioussectionswediscussedmanymethodsforiteratingoverdataandtransformingit.Inthissectionwe'lldiscussanotherhigherorderfunctionthatisarguablyoneofthemostpowerful.Itisaconceptrecognizedacrossenoughprogramminglanguagestogetitsownwikipediaarticle.Mostpopularlanguagescallitreduce,althoughsomelanguageswillcallitfoldorinject.Here'stheparametersittakes,andalthoughtheorderoftheparametersmaybedifferentinotherlanguagesthefunctionalityandoutputwillbethesame.
reduce(list,fn,starting_value)
Likewith map()and filter(),ittakesalistyouwanttotransformandafunction( fn)todothetransformation.Thetransformationfunctionbehaveslikearecursivelooplikeseeninthelastsection.Here'safunctionthattakesalistofnumbersandgivesyouthetotalsumofthosenumbers.
locallist={23,63,12,48,3}
localsum_fn=function(accumulator,current_number)
returnaccumulator+current_number
end
localtotal_sum=reduce(list,sum_fn,0)
Wepassreduceastartingnumberof 0.Whathappensis sum_fnisinvokedwiththefirstparameter,theaccumulatorbeingthestartingnumber0and current_numberbeingthefirstnumberinthelist.Whatevervaluethefunctionreturnsbecomesthenewvaluefor accumulatornextlooparound.
Luadoesn'thaveareducefunctionbuiltinsowe'llimplementourownherewithadetaileddescriptionofalltheparameters.Trynottogettoohungupontheactualreducefunction'simplementationatthetop,butratherfocusbelowthatonhowitworks.Therewillbeseveralmoreexamples.Onceyouunderstandhowtouseit,gobacktothetopandlookattheactual reducefunction'simplementation.CopyallthiscodeintothetexteditorwindowontheREPLandrunit:
--Appliesfnontwoargumentscumulativetotheitemsofthearrayt,
--fromlefttoright,soastoreducethearraytoasinglevalue.If
--afirstvalueisspecifiedtheaccumulatorisinitializedtothis,
--otherwisethefirstvalueinthearrayisused.
--@param{table}t-atabletoreduce
--@param{function}fn-thereducerforcomparingthetwovalues
--@param{*}acc-Theaccumulatoraccumulatesthecallback'sreturn
--values;Itistheaccumulatedvaluepreviouslyreturnedinthe
--lastinvocationofthecallback,or`first_value`,ifsupplied.
--@param{*}current_value-Thecurrentelementbeingprocessedinthelist.
--@param{number}current_index-Theindexofthecurrentelement
--beingprocessedinthelist,startingat1.
--@param{*}first_value-Theinitialvalueoftheaccumulation.Ifthearrayis
--empty,thefirst_valuewillalsobethereturnedvalue.Ifthearrayisempty
--andnofirstvalueisspecifiedanerrorisraised.
--@example
----returns'zxy'
--reduce(
--{'x','y'},
--function(a,b)returna+bend,
--'z'
--)
localfunctionreduce(t,fn,first)
localacc=first
3.05-Reduce
162
localstarting_value=first~=nil
fori,vinipairs(t)do
--Nostartingvalue,starton
--thefirstelementinthelist
ifstarting_valuethen
acc=fn(acc,v,i,t)
else
acc=v
starting_value=true
end
end
assert(
starting_value,
'Attemptedtoreduceanemptytablewithnofirstvalue.'
)
returnacc
end
locallist={23,63,12,48,3}
localsum_fn=function(accumulator,current_number)
print(accumulator)
returnaccumulator+current_number
end
localtotal_sum=reduce(list,sum_fn,0)
print('Thetotalsumis:',total_sum)
Followingthe printstatementinsideof sum_fn,wecanseethatthe accumulatorstartsoutwiththe0wepassin.Weadd current_numberto accumulatoranditbeginstoaccumulateallthevaluesasitgoes.
0
23
86
98
146
Thetotalsumis:149
Ifwedon'tpassinastartingnumber,theaccumulatorwillbeginrightawaywiththefirstnumberinthelist:
localsum_fn=function(accumulator,current_number)
print(accumulator)
returnaccumulator+current_number
end
localtotal_sum=reduce(list,sum_fn)
23
86
98
146
Thetotalsumis:149
Ifyou'veusedjavascript,youmaybestartingtoseetheuncannyresemblanceitbearstojavascript'sreducefunction.Bothlanguagesareverysimilarsyntactically,andgiventheubiquityofjavascriptthisLuaimplementationfollowsmuchofthesamebehavior.
Let'slookatsomemoreexamplestobetterunderstandhowtoreduceandwhatsituationsdoingsocouldproveuseful.Thereducefunctionisomittedinthefollowingexamples,butyoucancopyandpastethefunctionintheREPLalongsidetheexamplestorunthecodeyourself.
--Concatenatealistofwords
3.05-Reduce
163
locallist={'this','is','a','sentence'}
localsentence=reduce(list,function(acc,word,index,list)
--Addaperiodifthisisthelastword
ifindex==#listthen
word=word..'.'
end
--Otherwiseaddaspacebetweenthewords
returnacc..''..word
end)
print(sentence)
thisisasentence.
--Onlykeepoddnumbers
locallist={23,63,12,48,3}
localodd_numbers=reduce(list,function(acc,current_number)
ifcurrent_number%2==0then
returnacc
end
acc[#acc+1]=current_number
returnacc
end,{})
forkey,valueinipairs(odd_numbers)do
print(value)
end
23
63
3
Thislookssimilartowhatwemightdowiththe filterfunctionpreviouslycoveredin3.3-Mapandfilter.Infact,wecancompose filterand mapfrom reduce.Takealookatthesamecoderefactoredout:
localfilter=function(list,predicate_fn)
returnreduce(list,function(acc,val,i,t)
ifpredicate_fn(val,i,t)then
acc[#acc+1]=val
returnacc
end
returnacc
end,{})
end
--Onlykeepoddnumbers
locallist={23,63,12,48,3}
localodd_numbers=filter(list,function(current_number)
returncurrent_number%2~=0
end)
forkey,valueinipairs(odd_numbers)do
print(value)
end
Anexampleofwrapping reducewithanew mapfunctionwon'tbeexplainedhere,butratherleftuptothereaderasanexerciseattheendofthissection.
3.05-Reduce
164
Here'sonemoreexamplethatisabitmorecomplex,afunctioncalled composethatcreatesapipelineforpassingdatathrough.Itaccomplishesthisbypassinganyfunctionsyougiveitthroughto reduceasalist:
--Functionthatallowsyoutocomposeotherfunctions
--togethertoformapipeline.Theresultingpipeline
--isafunctionthatyoucanpassyourintendeddatathrough.
localcompose=function(...)
--"..."and"arg"arespecialkeywordsinLua.
--See:https://www.lua.org/pil/5.2.html
localfns=arg
returnfunction(x)
returnreduce(fns,function(acc,v)
returnv(acc)
end,x)
end
end
--Someexamplecomposablefunctions
localadd=function(x)
returnfunction(y)
returny+x
end
end
localmultiply=function(x)
returnfunction(y)
returny*x
end
end
localsubtract=function(x)
returnfunction(y)
returny-x
end
end
localnumber_pipeline=compose(add(12),multiply(2),subtract(9))
print(number_pipeline(3))
print(number_pipeline(2))
Alternativereduceimplementations
IteratingtablesLet'sgobacktotheimplementationofreduceforamoment.Takealookattheimplementationofitgivenabove.Noticetheiterationinsideisusing ipairswhichexpectsanarray/list-typetable.Ifwewantedtoreduceanon-listtablewecouldmodify reducetofirstcheckifthetableisanarrayanddoappropriateiterationoverthetablewhetherornotitis.Let'stestthat:
localfunctionreduce(t,fn,first)
localget_iterator=function(t)
iftype(t)=='table'then
--Ifpropertyof1isemptythen
--iterateasaregularkeyedtable
ift[1]==nilthen
returnpairs(t)
end
returnipairs(t)
end
error('Expectedtable,got'..tostring(t))
end
localacc=first
localstarting_value=first~=nil
--Whetherwedoipairsorpairsisconditional
3.05-Reduce
165
fori,vinget_iterator(t)do
--Nostartingvalue,starton
--thefirstelementinthelist
ifstarting_valuethen
acc=fn(acc,v,i,t)
else
acc=v
starting_value=true
end
end
assert(
starting_value,
'Attemptedtoreduceanemptytablewithnofirstvalue.'
)
returnacc
end
locallist={
monday=23,
tuesday=63,
wednesday=12,
thursday=48,
friday=3
}
localtotal_sum=reduce(list,function(acc,current_number,key)
print(key..':'..current_number)
returnacc+current_number
end)
print('totalsum:'..total_sum)
Thisshouldprintsomethinglikethis:
wednesday:12
friday:3
thursday:48
monday:23
totalsum:149
Notethattheorderthekeysareiteratedinarenotguaranteed.Also"tuesday"wasn'tprintedoutbecauseitwasthestartingnumber,butitwasstillincludedinthetotal.Passinganextraargumentof 0to reducewouldhavecausedallthedaystobepassedthroughourreducerfunctionandprintedout.
Breakearly
Ok,here'sanotherexamplethatseemstrickyatfirstglance;Let'ssayyouimplementedsomesearchfunctionalityontopofreducelikethis:
locallist={23,63,12,48,3}
localfind=function(list,predicate_fn)
returnreduce(list,function(acc,v,i,t)
ifpredicate_fn(v,a,t)then
returnv
end
returnacc
end)
end
print(find(list,function(val)
returnval>50
end))
print(find(list,function(val)
3.05-Reduce
166
returnval%8==0
end))
Whichprintsouttheexpectedresults:
63
48
Butdoyouseewhat'sproblematicaboutthis?Ifwefindtheresultswewant,thereducefunctionwillkeeprunningthroughtheentirelistunnecessarily.Typicallywhendoingasearchyouonlywantthefirstitemyoufindanyway,buttheaboveimplementationwillreturnthelastitemfoundifmorethanonematchismade.Doyourememberhowthereducefunctionpassesinthetableasthelastargumenttothereducerfunction?Wecantakecontrolofiteratorviathetableandkilltheiterationprematurely.Thisinvolvedmutatingthetable:
locallist={23,63,12,48,3}
localfind=function(list,predicate_fn)
returnreduce(list,function(acc,v,i,t)
ifpredicate_fn(v,a,t)then
--Ifaresultwasfound,destroythenextiteminthelist
--topreventtheiterationfromgoinganyfurther.
t[i+1]=nil
returnv
end
returnacc
end)
end
print(find(list,function(val)
returnval>1
end))
Thisreturnsthecorrectresult:
23
Butifweloopoverthetableafterwardswecanseewe'vemessedwiththeoriginaldatawhichcanleadtounexpectedconsequencesinarealapplication.Ifyourdataiscomingfromanimmutablesource,meaningsomethingisgeneratinganewcopyeachtimeyouuseitthenthiswouldn'tbeaproblem:
localgenerate_list=function()
return{23,63,12,48,3}
end
reduce(generate_list(),function()
...
...
Howeverwecouldfixallofthisifwearewillingtoaddanotherparametertoourreduceimplementation.
localfunctionreduce(t,fn,first)
localget_iterator=function(t)
iftype(t)=='table'then
--Ifpropertyof1isemptythen
--iterateasaregularkeyedtable
ift[1]==nilthen
returnpairs(t)
end
returnipairs(t)
3.05-Reduce
167
end
error('Expectedtable,got'..tostring(t))
end
localacc=first
localstarting_value=first~=nil
fori,vinget_iterator(t)do
--Exittheloopwhentrue
localshould_break=false
--Nostartingvalue,starton
--thefirstelementinthelist
ifstarting_valuethen
acc,should_break=fn(acc,v,i,t)
ifshould_breakthen
break
end
else
acc=v
starting_value=true
end
end
assert(
starting_value,
'Attemptedtoreduceanemptytablewithnofirstvalue.'
)
returnacc
end
Nowifwepass trueasasecondreturnparameterthenwewillgetthefirstnumberwearelookingforinsteadofthelast.Loopthroughandprintoutthelistafterwardtomakesurewehaven'tmutateditunexpectedly.
locallist={23,63,12,48,3}
localfind=function(list,predicate_fn)
returnreduce(list,function(acc,v,i,t)
ifpredicate_fn(v,a,t)then
returnv,true
end
returnacc
end,false)
end
print(find(list,function(val)
returnval>1
end))
foridx,valinipairs(list)do
print(idx,val)
end
reduce_right
Anotherpossiblechangeyouwouldwanttomakeistoreplacetheiteratorwithacustom-madeonetotransformdatainaspecificorderorpattern.Takenfromlua-users.org'sIterationTutorialisthisreverse-ipairs( ripairs)implementationthatallowsyoutoiterateoveratablefromrighttoleft.Thismodifiedversionof reduceistypicallycalled reduce_right.
localfunctionreduce_right(t,fn,first)
localripairs=function(t)
localmax=1
whilet[max]~=nildo
max=max+1
end
localfunctionripairs_it(t,i)
i=i-1
3.05-Reduce
168
localv=t[i]
ifv~=nilthen
returni,v
else
returnnil
end
end
returnripairs_it,t,max
end
localacc=first
localstarting_value=first~=nil
fori,vinripairs(t)do
--Exittheloopwhentrue
localshould_break=false
--Nostartingvalue,starton
--thefirstelementinthelist
ifstarting_valuethen
acc,should_break=fn(acc,v,i,t)
ifshould_breakthen
break
end
else
acc=v
starting_value=true
end
end
assert(
starting_value,
'Attemptedtoreduceanemptytablewithnofirstvalue.'
)
returnacc
end
Thenswapout reducefor reduce_rightintheplacesyouwanttouseit:
locallist={23,63,12,48,3}
localfind=function(list,predicate_fn)
returnreduce_right(list,function(acc,v,i,t)
ifpredicate_fn(v,a,t)then
returnv,true
end
returnacc
end,false)
end
print(find(list,function(val)
returnval>1
end))
Recursive
Sincewetalkedaboutrecursioninthelastsection,let'stryarecursiveimplementationof reduce.AlthoughwithLuathere'snopracticalreasontochoosearecursiveimplementationoverafor-looporwhile-loopimplementation,doingrecursionisfun.
localfunctionreduce(t,fn,acc,key)
--Checkforstartingvalue
ifkey==nilandacc==nilthen
key=next(t,key)
acc=t[key]
end
--Beginnextiteration.NextisaLuabuilt-infunction
--thatfetchesthenextkeyinatableafterthegivenkey.
3.05-Reduce
169
--See:https://www.lua.org/pil/7.3.html
key=next(t,key)
--Returnaccifwe'veiteratedallkeys
ifkey==nilthen
returnacc
end
localbreak_early=false
--Collectnewaccumulatorfrompredicatefunction
acc,break_early=fn(acc,t[key],key,t)
--Checktoseeifthepredicatewantstoendearly
ifbreak_earlythen
returnacc
end
--Recur
returnreduce(t,fn,acc,key,acc)
end
--Testitbygettingthetotalsumfromatablelikebefore
locallist={
monday=23,
tuesday=63,
wednesday=12,
thursday=48,
friday=3
}
localtotal_sum=reduce(list,function(acc,current_number,key)
print(key..':'..current_number)
returnacc+current_number
end,0)
print('totalsum:'..total_sum)
Thissupportsbreakingearlylikethetwopreviousimplementations.
ExercisesCreatea countfunctionthatcountsupthenumberofitemsinalistthatmatchthepredicateandreturnsthetotal.Itshouldworklikethis:
localcount=function(list,predicate_fn)
????
end
locallist={23,63,12,48,3}
--Printnumberofitemsevenlydivisibleby3(shouldreturn4)
print(count(list,function(v)
returnv%3==0
end))
Gobacktothemapsectionin3.3andseeifyoucanreimplementthe mapfunctionontopof reduce.
3.05-Reduce
170