Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
PythonGameProgrammingByExample TableofContents PythonGameProgrammingByExample Credits AbouttheAuthors AbouttheReviewers www.PacktPub.com Supportfiles,eBooks,discountoffers,andmore Whysubscribe? FreeaccessforPacktaccountholders Preface Whatthisbookcovers Whatyouneedforthisbook Whothisbookisfor Conventions Readerfeedback Customersupport Downloadingtheexamplecode Downloadingthecolorimagesofthisbook Errata Piracy Questions 1.Hello,Pong! InstallingPython AnoverviewofBreakout ThebasicGUIlayout DivingintotheCanvaswidget Basicgameobjects TheBallclass ThePaddleclass TheBrickclass AddingtheBreakoutitems Movementandcollisions Startingthegame PlayingBreakout Summary 2.CocosInvaders Installingcocos2d Gettingstartedwithcocos2d Handlinguserinput Updatingthescene Processingcollisions Creatinggameassets SpaceInvadersdesign ThePlayerCannonandGameLayerclasses Invaders! Shoot’emup! AddinganHUD Extrafeature–themysteryship Summary 3.BuildingaTowerDefenseGame Thetowerdefensegameplay Cocos2dactions Intervalactions Instantactions Combiningactions Customactions Addingamainmenu Tilemaps TiledMapEditor Loadingtiles Thescenariodefinition Thescenarioclass Transitionsbetweenscenes Gameovercutscene Thetowerdefenseactors Turretsandslots Enemies Bunker Gamescene TheHUDclass Assemblingthescene Summary 4.SteeringBehaviors NumPyinstallation TheParticleSystemclass Aquickdemonstration Implementingsteeringbehaviors Seekandflee Arrival Pursuitandevade Wander Obstacleavoidance Gravitationgame Basicgameobjects Planetsandpickups Playerandenemies Explosions Thegamelayer Summary 5.Pygameand3D Installingpackages GettingstartedwithOpenGL Initializingthewindow Drawingshapes Runningthedemo RefactoringourOpenGLprogram Processingtheuserinput AddingthePygamelibrary Pygame101 Pygameintegration DrawingwithOpenGL TheCubeclass Enablingfaceculling Basiccollisiondetectiongame Summary 6.PyPlatformer Anintroductiontogamedesign Leveldesign Platformerskills Component-basedgameengines IntroducingPymunk Buildingagameframework Addingphysics Renderablecomponents TheCameracomponent TheInputManagermodule TheGameclass DevelopingPyPlatformer Creatingtheplatforms Addingpickups Shooting! ThePlayerclassanditscomponents ThePyPlatformerclass Summary 7.AugmentingaBoardGamewithComputerVision PlanningtheCheckersapplication SettingupOpenCVandotherdependencies Windows Mac Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMint Fedoraanditsderivatives,includingRHELandCentOS OpenSUSEanditsderivatives SupportingmultipleversionsofOpenCV Configuringcameras Workingwithcolors Buildingtheanalyzer Providingaccesstotheimagesandclassificationresults Providingaccesstoparametersfortheusertoconfigure Initializingtheentiremodelofthegame Updatingtheentiremodelofthegame Capturingandconvertinganimage Detectingtheboard’scornersandtrackingtheirmotion Creatingandanalyzingthebird’s-eyeviewoftheboard Analyzingthedominantcolorsinasquare Classifyingthecontentsofasquare Drawingtext ConvertingOpenCVimagesforwxPython BuildingtheGUIapplication Creatingawindowandbindingevents CreatingandlayingoutimagesintheGUI Creatingandlayingoutcontrols Nestinglayoutsandsettingtherootlayout Startingabackgroundthread Closingawindowandstoppingabackgroundthread Configuringtheanalyzerbasedonuserinput Updatingandshowingimages Runningtheapplication Troubleshootingtheprojectinreal-worldconditions FurtherreadingonOpenCV Summary Index PythonGameProgrammingByExample PythonGameProgrammingByExample Copyright©2015PacktPublishing Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem, ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthe publisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews. Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyofthe informationpresented.However,theinformationcontainedinthisbookissoldwithout warranty,eitherexpressorimplied.Neithertheauthors,norPacktPublishing,andits dealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecaused directlyorindirectlybythisbook. PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthe companiesandproductsmentionedinthisbookbytheappropriateuseofcapitals. However,PacktPublishingcannotguaranteetheaccuracyofthisinformation. Firstpublished:September2015 Productionreference:1230915 PublishedbyPacktPublishingLtd. LiveryPlace 35LiveryStreet BirminghamB32PB,UK. ISBN978-1-78528-153-2 www.packtpub.com Credits Authors AlejandroRodasdePaz JosephHowse Reviewers BenjaminJohnson DennisO’Brien AcquisitionEditors OwenRoberts SonaliVernekar ContentDevelopmentEditor DharmeshParmar TechnicalEditor RyanKochery CopyEditor VikrantPhadke ProjectCoordinator HarshalVed Proofreader SafisEditing Indexer RekhaNair Graphics JasonMonteiro ProductionCoordinator ManuJoseph CoverWork ManuJoseph AbouttheAuthors AlejandroRodasdePazisacomputerengineerandgamedeveloperfromSeville,Spain. HecameacrossPythonbackin2009,whilehewasstudyingattheUniversityofSeville. AlejandrodevelopedseveralacademicprojectswithPython,fromwebcrawlersto artificialintelligencealgorithms.Inhissparetime,hestartedbuildinghisowngamesin Python.HedidaminoringamedesignatHogeschoolvanAmsterdam,wherehecreateda small3Dgameenginebasedontheideashelearnedduringthisminor. Hehasalsodevelopedsomeopensourceprojects,suchasaPythonAPIforthePhilips Huepersonallightingsystem.YoucanfindtheseprojectsinhisGitHubaccountat https://github.com/aleroddepaz. Priortothispublication,AlejandrocollaboratedwithPacktPublishingasatechnical revieweronthebookTkinterGUIApplicationDevelopmentHotshot. Iwouldliketothankmyparents,FelicianoandMaríaTeresa,fortheirabsolutetrustand support.Theyhavebeenaninspirationtomeandanexampleofhardwork. Iwouldalsoliketothankmygirlfriend,Lucía,forherloveandforputtingupwithme whileIworkedonthisbook. JosephHowseisawriter,softwaredeveloper,andbusinessownerfromHalifax,Nova Scotia,Canada.Computergamesandcodeareimbibedinhisearliestmemories,ashe learnedtoreadandtypebyplayingtextadventureswithhisolderbrother,Sam,and watchinghimwritegraphicsdemosinBASIC. Joseph’sotherbooksincludeOpenCVforSecretAgents,OpenCVBlueprints,Android ApplicationProgrammingwithOpenCV3,andLearningOpenCV3ComputerVisionwith Python.Heworkswithhiscatstomakecomputervisionsystemsforhumans,felines,and otherusers.Visithttp://nummist.comtoreadaboutsomeofhislatestprojectsdoneat NummistMediaCorporationLimited. IdedicatemyworktoSam,Jan,Bob,Bunny,andmycats,whohavebeenmylifelong guidesandcompanions. Icongratulatemycoauthorforproducinganexcellentcompendiumofclassicexamplesof gamedevelopment.Iamgratefulfortheopportunitytoaddmychapteroncheckers (draughts)andcomputervision. Iamalsoindebtedtothemanyeditorsandtechnicalreviewerswhohavecontributedto planning,polishing,andmarketingthisbook.Ihavecometoexpectanoutstandingteam whenworkingwithPacktPublishing,andonceagain,allofthemhaveguidedmewith theirexperienceandsavedmefromsundryerrorsandomissions.Pleasemeetthe technicalreviewersbyreadingtheirbiographieshere. Finally,Iwanttothankmyreadersandeverybodyintheopensourcecommunity.Weare unitedinoureffortstobuildandshareallkindsofprojectsandknowledge,pavingthe wayforbookssuchasthistosucceed. AbouttheReviewers BenjaminJohnsonisanexperiencedPythonprogrammerwithapassionforgame programming,softwaredevelopment,andwebdesign.Heiscurrentlystudyingcomputer scienceatTheUniversityofTexasatAustinandplanstospecializeinsoftware engineering.HismostpopularPythonprojectsincludeanadventuregameengineanda particlesimulator,bothdevelopedusingPygame.YoucancheckoutBenjamin’slatest Pygameprojectsandarticlesonhiswebsiteatwww.learnpygame.com. IwouldliketothankPacktPublishingforgivingmetheopportunitytoreadandreview thisexcellentbook! DennisO’BrienisthedirectorofdatascienceatGameShowNetworkGames.Hestudied physicsattheUniversityofChicagoasanundergraduateandcompletedhisgraduate studiesincomputersciencefromtheUniversityofIllinois,Chicago.Hewastheprincipal softwareengineeratElectronicArts,aseniorsoftwareengineeratLeapfrogEnterprises, andaleadgamedeveloperatJellyvisionGames. www.PacktPub.com Supportfiles,eBooks,discountoffers,and more Forsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com. DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFand ePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandas aprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwith usat<[email protected]>formoredetails. Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signup forarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooks andeBooks. https://www2.packtpub.com/books/subscription/packtlib DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigital booklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks. Whysubscribe? FullysearchableacrosseverybookpublishedbyPackt Copyandpaste,print,andbookmarkcontent Ondemandandaccessibleviaawebbrowser FreeaccessforPacktaccountholders IfyouhaveanaccountwithPacktatwww.PacktPub.com,youcanusethistoaccess PacktLibtodayandview9entirelyfreebooks.Simplyuseyourlogincredentialsfor immediateaccess. Preface WelcometoPythonGameProgrammingByExample.Ashobbyistprogrammersor professionaldevelopers,wemaybuildawidevarietyofapplications,fromlargeenterprise systemstowebapplicationsmadewithstate-of-the-artframeworks.However,game developmenthasalwaysbeenanappealingtopic,maybesimplyforcreatingcasualgames andnotjustforhigh-budgetAAAtitles. IfyouwanttoexplorethedifferentwaysofdevelopinggamesinPython,alanguagewith clearandsimplesyntax,thenthisisthebookforyou.Ineachchapter,wewillbuildanew gamefromscratch,usingseveralpopularlibrariesandutilities.Bytheendofthisbook, youwillbeabletoquicklycreateyourown2Dand3Dgames,andhaveahandfulof Pythonlibrariesinyourtoolbelttochoosefrom. Whatthisbookcovers Chapter1,Hello,Pong!,detailstherequiredsoftware,itsinstallation,andthebasicsyntax ofPython:datastructures,controlflowstatements,objectorientation,andsoon.Italso includesthefirstgameofthebook,theclassic“Hello,world”game. Chapter2,CocosInvaders,introducesthecocos2dgameengineandexplainshowtobuild agamesimilartoSpaceInvaderstoputthisknowledgeintopractice.Here,youlearnthe basicsofcollisions,inputhandling,andscenesetup. Chapter3,BuildingaTowerDefenseGame,iswhereyoulearntodevelopafull-fledged gamewithcocos2d.Thisgameincludessomeinterestingcomponents,suchasaHUDand amainmenu. Chapter4,SteeringBehaviors,coversseeminglyintelligentmovementsforautonomous characters.Youwillbeaddingthesestrategiesgradually,indifferentlevelsofabasic gamebuiltwithparticlesystems. Chapter5,Pygameand3D,presentsthefoundationsof3Dandguidesyouthroughthe basicstructureofanOpenGLprogram. Chapter6,PyPlatformer,iswhereyoudevelopa3Dplatformergamewithallthe techniqueslearnedinthepreviouschapter. Chapter7,AugmentingaBoardGamewithComputerVision,introducesthetopicof computervision,whichallowssoftwaretolearnabouttherealworldviaacamera.Inthis chapter,youbuildasystemtoanalyzeagameofcheckers(draughts)inrealtimeas playersmovepiecesonaphysicalboard. Whatyouneedforthisbook TheprojectscoveredinthisbookassumethatyouhaveinstalledPython3.4ona computerwithWindows,MacOSX,orLinux.Wealsoassumethatyouhaveincluded pipduringtheinstallationprocess,sinceitwillbethepackagemanagerusedtoinstallthe requiredthird-partypackages. Whothisbookisfor IfyouhaveeverwantedtocreatecasualgamesinPythonandyouwishtoexplorethe variousGUItechnologiesthatthislanguageoffers,thenthisisthebookforyou.Thistitle isintendedforbeginnersinPythonwithlittleornoknowledgeofgamedevelopment,and itcoversstepbystephowtobuildsevendifferentgames,fromthewell-knownSpace Invaderstoaclassical3Dplatformer. Conventions Inthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkinds ofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheir meaning. Codewordsintext,databasetablenames,foldernames,filenames,fileextensions, pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“For instance,onUbuntu,youneedtoinstallthepython3-tkpackage.” Ablockofcodeissetasfollows: new_list=[] forelemincollection: ifelemisnotNone: new_list.append(elem) Anycommand-lineinputoroutputiswrittenasfollows: $python–-version Python3.4.3 Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen, forexample,inmenusordialogboxes,appearinthetextlikethis:“Makesurethatyou checktheTcl/Tkoptiontoincludethelibrary.” Note Warningsorimportantnotesappearinaboxlikethis. Tip Tipsandtricksappearlikethis. Readerfeedback Feedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthis book—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsus developtitlesthatyouwillreallygetthemostoutof. Tosendusgeneralfeedback,simplye-mail<[email protected]>,andmentionthe book’stitleinthesubjectofyourmessage. Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingor contributingtoabook,seeourauthorguideatwww.packtpub.com/authors. Customersupport NowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelp youtogetthemostfromyourpurchase. Downloadingtheexamplecode Youcandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.com forallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbook elsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilesemaileddirectlytoyou. Additionally,up-to-dateexamplecodeforChapter7,AugmentingaBoardGamewith ComputerVision,ispostedathttp://nummist.com/opencv. Downloadingthecolorimagesofthisbook WealsoprovideyouwithaPDFfilethathascolorimagesofthescreenshots/diagrams usedinthisbook.Thecolorimageswillhelpyoubetterunderstandthechangesinthe output.Youcandownloadthisfilefrom https://www.packtpub.com/sites/default/files/downloads/B04505_Graphics.pdf. Errata Althoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdo happen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthe code—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveother readersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufind anyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata, selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthe detailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedand theerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataunderthe Erratasectionofthattitle. Toviewthepreviouslysubmittederrata,goto https://www.packtpub.com/books/content/supportandenterthenameofthebookinthe searchfield.TherequiredinformationwillappearundertheErratasection. Additionally,anyerrataforChapter7,AugmentingaBoardGamewithComputerVision, willbepostedathttp://nummist.com/opencv. Piracy PiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.At Packt,wetaketheprotectionofourcopyrightandlicensesveryseriously.Ifyoucome acrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswith thelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy. Pleasecontactusat<[email protected]>withalinktothesuspectedpirated material. Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluable content. Questions Ifyouhaveaproblemwithanyaspectofthisbook,youcancontactusat <[email protected]>,andwewilldoourbesttoaddresstheproblem. Youcanalsocontacttheauthorsdirectly.AlejandraRodasdePaz,authorofChapters1to 6,canbereachedat<[email protected]>.JosephHowse,authorofChapter7,canbe reachedat<[email protected]>,andanswerstocommonquestionscanbefound onhiswebsite,http://nummist.com/opencv. Chapter1.Hello,Pong! Gamedevelopmentisahighlyevolvingsoftwaredevelopmentprocess,andithas improvedcontinuouslysincetheappearanceofthefirstvideogamesinthe1950s. Nowadays,thereareawidevarietyofplatformsandengines,andthisprocesshasbeen facilitatedwiththearrivalofopensourcetools. Pythonisafreehigh-levelprogramminglanguagewithadesignintendedtowritereadable andconciseprograms.Thankstoitsphilosophy,wecancreateourowngamesfrom scratchwithjustafewlinesofcode.ThereareaplentyofgameframeworksforPython, butforourfirstgame,wewillseehowwecandevelopitwithoutanythird-party dependency. Inthischapter,wewillcoverthefollowingtopics: Installationoftherequiredsoftware AnoverviewofTkinter,aGUIlibraryincludedinthePythonstandardlibrary Applyingobject-orientedprogrammingtoencapsulatethelogicofourgame Basiccollisionandinputdetection Drawinggameobjectswithoutexternalassets DevelopingasimplifiedversionofBreakout,apong-basedgame InstallingPython YouwillneedPython3.4withTcl/Tk8.6installedonyourcomputer.Thelatestbranch ofthisversionisPython3.4.3,whichcanbedownloadedfrom https://www.python.org/downloads/.Here,youcanfindtheofficialbinariesforthemost popularplatforms,suchasWindowsandMacOS.Duringtheinstallationprocess,make surethatyouchecktheTcl/Tkoptiontoincludethelibrary. ThecodeexamplesincludedinthebookhavebeentestedagainstWindows8andMac, butcanberunonLinuxwithoutanymodification.Notethatsomedistributionsmay requireyoutoinstalltheappropriatepackageforPython3.Forinstance,onUbuntu,you needtoinstallthepython3-tkpackage. OnceyouhavePythoninstalled,youcanverifytheversionbyopeningCommandPrompt oraterminalandexecutingtheselines: $python--version Python3.4.3 Afterthischeck,youshouldbeabletostartasimpleGUIprogram: $python >>>fromtkinterimportTk >>>root=Tk() >>>root.title('Hello,world!') >>>root.mainloop() Thesestatementscreateawindow,changeitstitle,andrunindefinitelyuntilthewindowis closed.Donotclosethenewwindowthatisdisplayedwhenthesecondstatementis executed.Otherwise,itwillraiseanerrorbecausetheapplicationhasbeendestroyed. Wewillusethislibraryinourfirstgame,andthecompletedocumentationofthemodule canbefoundathttps://docs.python.org/3/library/tkinter.html. Tip TkinterandPython2 TheTkintermodulewasrenamedtotkinterinPython3.IfyouhavePython2installed, simplychangetheimportstatementwithTkinterinuppercase,andtheprogramshould runasexpected. AnoverviewofBreakout TheBreakoutgamestartswithapaddleandaballatthebottomofthescreenandsome rowsofbricksatthetop.Theplayermusteliminateallthebricksbyhittingthemwiththe ball,whichreboundsagainstthebordersofthescreen,thebricks,andthebottompaddle. AsinPong,theplayercontrolsthehorizontalmovementofthepaddle. Theplayerstartsthegamewiththreelives,andiftheymisstheball’sreboundandit reachesthebottomborderofthescreen,onelifeislost.Thegameisoverwhenallthe bricksaredestroyed,orwhentheplayerlosesalltheirlives. Thisisascreenshotofthefinalversionofourgame: ThebasicGUIlayout Wewillstartoutgamebycreatingatop-levelwindowasinthesimpleprogramweran previously.However,thistime,wewillusetwonestedwidgets:acontainerframeandthe canvaswherethegameobjectswillbedrawn,asshownhere: WithTkinter,thiscaneasilybeachievedusingthefollowingcode: importtkinterastk lives=3 root=tk.Tk() frame=tk.Frame(root) canvas=tk.Canvas(frame,width=600,height=400,bg='#aaaaff') frame.pack() canvas.pack() root.title('Hello,Pong!') root.mainloop() Tip Downloadingtheexamplecode Youcandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.com forallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbook elsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilesemaileddirectlytoyou. Throughthetkalias,weaccesstheclassesdefinedinthetkintermodule,suchasTk, Frame,andCanvas. Noticethefirstargumentofeachconstructorcallwhichindicatesthewidget(thechild container),andtherequiredpack()callsfordisplayingthewidgetsontheirparent container.ThisisnotnecessaryfortheTkinstance,sinceitistherootwindow. However,thisapproachisnotexactlyobject-oriented,sinceweuseglobalvariablesand donotdefineanynewclassestorepresentournewdatastructures.Ifthecodebasegrows, thiscanleadtopoorlyorganizedprojectsandhighlycoupledcode. Wecanstartencapsulatingthepiecesofourgameinthisway: importtkinterastk classGame(tk.Frame): def__init__(self,master): super(Game,self).__init__(master) self.lives=3 self.width=610 self.height=400 self.canvas=tk.Canvas(self,bg='#aaaaff', width=self.width, height=self.height) self.canvas.pack() self.pack() if__name__=='__main__': root=tk.Tk() root.title('Hello,Pong!') game=Game(root) game.mainloop() Ournewtype,calledGame,inheritsfromtheFrameTkinterclass.Theclass Game(tk.Frame):definitionspecifiesthenameoftheclassandthesuperclassbetween parentheses. Ifyouarenewtoobject-orientedprogrammingwithPython,thissyntaxmaynotsound familiar.Inourfirstlookatclasses,themostimportantconceptsarethe__init__method andtheselfvariable: The__init__methodisaspecialmethodthatisinvokedwhenanewclassinstance iscreated.Here,wesettheobjectattributes,suchasthewidth,theheight,andthe canvaswidget.Wealsocalltheparentclassinitializationwiththesuper(Game, self).__init__(master)statement,sotheinitialstateoftheFrameisproperly initialized. Theselfvariablereferstotheobject,anditshouldbethefirstargumentofamethod ifyouwanttoaccesstheobjectinstance.Itisnotstrictlyalanguagekeyword,butthe PythonconventionistocallitselfsothatotherPythonprogrammerswon’tbe confusedaboutthemeaningofthevariable. Intheprecedingsnippet,weintroducedtheif__name__=='__main__'condition, whichispresentinmanyPythonscripts.Thissnippetchecksthenameofthecurrent modulethatisbeingexecuted,andwillpreventstartingthemainloopwherethismodule wasbeingimportedfromanotherscript.Thisblockisplacedattheendofthescript,since itrequiresthattheGameclassbedefined. Tip New-andold-styleclasses YoumayseetheMySuperClass.__init__(self,arguments)syntaxinsomePython2 examples,insteadofthesupercall.Thisistheold-stylesyntax,theonlyflavoravailable uptoPython2.1,andismaintainedinPython2forbackwardcompatibility. Thesuper(MyClass,self).__init__(arguments)isthenew-classstyleintroducedin Python2.2.Itisthepreferredapproach,andwewilluseitthroughoutthisbook. Seethechapter1_01.pyscript,whichcontainsthiscode.Sincenoexternalassetsare needed,youcanplaceitinanydirectoryandexecuteitfromthePythoncommandlineby runningchapter1_01.py.Themainloopwillrunindefinitelyuntilyouclickontheclose buttonofthewindow,oryoukilltheprocessfromthecommandline. Thisisthestartingpointofourgame,solet’sstartdivingintotheCanvaswidgetandsee howwecandrawandanimateitemsinit. DivingintotheCanvaswidget Sofar,wehavethewindowsetupandnowwecanstartdrawingitemsonthecanvas.The Canvaswidgetistwo-dimensionalandusestheCartesiancoordinatesystem.Theorigin— the(0,0)orderedpair—isplacedinthetop-leftcorner,andtheaxiscanberepresented asshowninthefollowingscreenshot: Keepingthislayoutinmind,wecanusetwomethodsoftheCanvaswidgettodrawthe paddle,thebricks,andtheball: canvas.create_rectangle(x0,y0,x1,y1,**options) canvas.create_oval(x0,y0,x1,y1,**options) Eachofthesecallsreturnsaninteger,whichidentifiestheitemhandle.Thisreferencewill beusedlatertomanipulatethepositionoftheitemanditsoptions.The**optionssyntax representsakey/valuepairofadditionalargumentsthatcanbepassedtothemethodcall. Inourcase,wewillusethefillandthetagsoption. Thex0andy0coordinatesindicatethetop-leftcornerofthepreviousscreenshot,andx1 andy1areindicatedinthebottom-rightcorner. Forinstance,wecancallcanvas.create_rectangle(250,300,330,320, fill='blue',tags='paddle')tocreateaplayer’spaddle,where: Thetop-leftcornerisatthecoordinates(250,300). Thebottom-rightcornerisatthecoordinates(300,320). Thefill='blue'meansthatthebackgroundcoloroftheitemisblue. Thetags='paddle'meansthattheitemistaggedasapaddle.Thisstringwillbe usefullatertofinditemsinthecanvaswithspecifictags. WewillinvokeotherCanvasmethodstomanipulatetheitemsandretrievewidget information.ThistablegivesthereferencestotheCanvaswidgetthatwillbeusedinthis chapter: Method Description canvas.coords(item) Returnsthecoordinatesoftheboundingboxofanitem. canvas.move(item,x,y) Movesanitembyahorizontalandaverticaloffset. canvas.delete(item) Deletesanitemfromthecanvas. canvas.winfo_width() Retrievesthecanvaswidth. canvas.itemconfig(item,**options) Changestheoptionsofanitem,suchasthefillcolororitstags. canvas.bind(event,callback) Bindsaninputeventwiththeexecutionofafunction.Thecallbackhandler receivesoneparameterofthetypeTkinterevent. canvas.unbind(event) Unbindstheinputeventsothatthereisnocallbackfunctionexecutedwhen theeventoccurs. canvas.create_text(*position, **opts) Drawstextonthecanvas.Thepositionandtheoptionsargumentsare similartotheonespassedincanvas.create_rectangleand canvas.create_oval. canvas.find_withtag(tag) Returnstheitemswithaspecifictag. canvas.find_overlapping(*position) Returnstheitemsthatoverlaporarecompletelyenclosedbyagiven rectangle. Youcancheckoutacompletereferenceoftheeventsyntaxaswellassomepractical examplesathttp://effbot.org/tkinterbook/tkinter-events-and-bindings.htm#events. Basicgameobjects Beforewestartdrawingallourgameitems,let’sdefineabaseclasswiththefunctionality thattheywillhaveincommon—storingareferencetothecanvasanditsunderlying canvasitem,gettinginformationaboutitsposition,anddeletingtheitemfromthecanvas: classGameObject(object): def__init__(self,canvas,item): self.canvas=canvas self.item=item defget_position(self): returnself.canvas.coords(self.item) defmove(self,x,y): self.canvas.move(self.item,x,y) defdelete(self): self.canvas.delete(self.item) AssumingthatwehavecreatedaCanvaswidgetasshowninourpreviouscodesamples,a basicusageofthisclassanditsattributeswouldbelikethis: item=canvas.create_rectangle(10,10,100,80,fill='green') game_object=GameObject(canvas,item)#createnewinstance print(game_object.get_position()) #[10,10,100,80] game_object.move(20,-10) print(game_object.get_position()) #[30,0,120,70] game_object.delete() Inthisexample,wecreatedagreenrectangleandaGameObjectinstancewiththeresulting item.Thenweretrievedthepositionoftheitemwithinthecanvas,movedit,and calculatedthepositionagain.Finally,wedeletedtheunderlyingitem. ThemethodsthattheGameObjectclassofferswillbereusedinthesubclassesthatwewill seelater,sothisabstractionavoidsunnecessarycodeduplication.Nowthatyouhave learnedhowtoworkwiththisbasicclass,wecandefineseparatechildclassesfortheball, thepaddle,andthebricks. TheBallclass TheBallclasswillstoreinformationaboutthespeed,direction,andradiusoftheball.We willsimplifytheball’smovement,sincethedirectionvectorwillalwaysbeoneofthe following: [1,1]iftheballismovingtowardsthebottom-rightcorner [-1,-1]iftheballismovingtowardsthetop-leftcorner [1,-1]iftheballismovingtowardsthetop-rightcorner [-1,1]iftheballismovingtowardsthebottom-leftcorner Arepresentationofthepossibledirectionvectors Therefore,bychangingthesignofoneofthevectorcomponents,wewillchangetheball’s directionby90degrees.Thiswillhappenwhentheballbouncesagainstthecanvasborder, whenithitsabrick,ortheplayer’spaddle: classBall(GameObject): def__init__(self,canvas,x,y): self.radius=10 self.direction=[1,-1] self.speed=10 item=canvas.create_oval(x-self.radius,y-self.radius, x+self.radius,y+self.radius, fill='white') super(Ball,self).__init__(canvas,item) Fornow,theobjectinitializationisenoughtounderstandtheattributesthattheclasshas. Wewillcovertheballreboundlogiclater,whentheothergameobjectshavebeendefined andplacedinthegamecanvas. ThePaddleclass ThePaddleclassrepresentstheplayer’spaddleandhastwoattributestostorethewidth andheightofthepaddle.Aset_ballmethodwillbeusedtostoreareferencetotheball, whichcanbemovedwiththeballbeforethegamestarts: classPaddle(GameObject): def__init__(self,canvas,x,y): self.width=80 self.height=10 self.ball=None item=canvas.create_rectangle(x-self.width/2, y-self.height/2, x+self.width/2, y+self.height/2, fill='blue') super(Paddle,self).__init__(canvas,item) defset_ball(self,ball): self.ball=ball defmove(self,offset): coords=self.get_position() width=self.canvas.winfo_width() ifcoords[0]+offset>=0and\ coords[2]+offset<=width: super(Paddle,self).move(offset,0) ifself.ballisnotNone: self.ball.move(offset,0) Themovemethodisresponsibleforthehorizontalmovementofthepaddle.Stepbystep, thefollowingisthelogicbehindthismethod: Theself.get_position()calculatesthecurrentcoordinatesofthepaddle Theself.canvas.winfo_width()retrievesthecanvaswidth Ifboththeminimumandmaximumx-axiscoordinates,plustheoffsetproducedby themovement,areinsidetheboundariesofthecanvas,thisiswhathappens: Thesuper(Paddle,self).move(offset,0)callsthemethodwithsamename inthePaddleclass’sparentclass,whichmovestheunderlyingcanvasitem Ifthepaddlestillhasareferencetotheball(thishappenswhenthegamehasnot beenstarted),theballismovedaswell Thismethodwillbeboundtotheinputkeyssothattheplayercanusethemtocontrolthe paddle’smovement.WewillseelaterhowwecanuseTkintertoprocesstheinputkey events.Fornow,let’smoveontotheimplementationofthelastoneofourgame’s components. TheBrickclass EachbrickinourgamewillbeaninstanceoftheBrickclass.Thisclasscontainsthelogic thatisexecutedwhenthebricksarehitanddestroyed: classBrick(GameObject): COLORS={1:'#999999',2:'#555555',3:'#222222'} def__init__(self,canvas,x,y,hits): self.width=75 self.height=20 self.hits=hits color=Brick.COLORS[hits] item=canvas.create_rectangle(x-self.width/2, y-self.height/2, x+self.width/2, y+self.height/2, fill=color,tags='brick') super(Brick,self).__init__(canvas,item) defhit(self): self.hits-=1 ifself.hits==0: self.delete() else: self.canvas.itemconfig(self.item, fill=Brick.COLORS[self.hits]) Asyoumayhavenoticed,the__init__methodisverysimilartotheoneinthePaddle class,sinceitdrawsarectangleandstoresthewidthandtheheightoftheshape.Inthis case,thevalueofthetagsoptionpassedasakeywordargumentis'brick'.Withthistag, wecancheckwhetherthegameisoverwhenthenumberofremainingitemswiththistag iszero. AnotherdifferencefromthePaddleclassisthehitmethodandtheattributesituses.The classvariablecalledCOLORSisadictionary—adatastructurethatcontainskey/valuepairs withthenumberofhitsthatthebrickhasleft,andthecorrespondingcolor.Whenabrick ishit,themethodexecutionoccursasfollows: Thenumberofhitsofthebrickinstanceisdecreasedby1 Ifthenumberofhitsremainingis0,self.delete()deletesthebrickfromthecanvas Otherwise,self.canvas.itemconfig()changesthecolorofthebrick Forinstance,ifwecallthismethodforabrickwithtwohitsleft,wewilldecreasethe counterby1andthenewcolorwillbe#999999,whichisthevalueofBrick.COLORS[1]. Ifthesamebrickishitagain,thenumberofremaininghitswillbecomezeroandtheitem willbedeleted. AddingtheBreakoutitems Nowthattheorganizationofouritemsisseparatedintothesetop-levelclasses,wecan extendthe__init__methodofourGameclass: classGame(tk.Frame): def__init__(self,master): super(Game,self).__init__(master) self.lives=3 self.width=610 self.height=400 self.canvas=tk.Canvas(self,bg='#aaaaff', width=self.width, height=self.height) self.canvas.pack() self.pack() self.items={} self.ball=None self.paddle=Paddle(self.canvas,self.width/2,326) self.items[self.paddle.item]=self.paddle forxinrange(5,self.width-5,75): self.add_brick(x+37.5,50,2) self.add_brick(x+37.5,70,1) self.add_brick(x+37.5,90,1) self.hud=None self.setup_game() self.canvas.focus_set() self.canvas.bind('<Left>', lambda_:self.paddle.move(-10)) self.canvas.bind('<Right>', lambda_:self.paddle.move(10)) defsetup_game(self): self.add_ball() self.update_lives_text() self.text=self.draw_text(300,200, 'PressSpacetostart') self.canvas.bind('<space>', lambda_:self.start_game()) Thisinitializationismorecomplexthatwhatwehadatthebeginningofthechapter.We candivideitintotwosections: Gameobjectinstantiation,andtheirinsertionintotheself.itemsdictionary.This attributecontainsallthecanvasitemsthatcancollidewiththeball,soweaddonly thebricksandtheplayer’spaddletoit.Thekeysarethereferencestothecanvas items,andthevaluesarethecorrespondinggameobjects.Wewillusethisattribute laterinthecollisioncheck,whenwewillhavethecollidingitemsandwillneedto fetchthegameobject. Keyinputbinding,viatheCanvaswidget.Thecanvas.focus_set()callsetsthe focusonthecanvas,sotheinputeventsaredirectlyboundtothiswidget.Thenwe bindtheleftandrightkeystothepaddle’smove()methodandthespacebartotrigger thegamestart.Thankstothelambdaconstruct,wecandefineanonymousfunctions aseventhandlers.Sincethecallbackargumentofthebindmethodisafunctionthat receivesaTkintereventasanargument,wedefinealambdathatignoresthefirst parameter—lambda_:<expression>. Ournewadd_ballandadd_brickmethodsareusedtocreategameobjectsandperforma basicinitialization.Whilethefirstonecreatesanewballontopoftheplayer’spaddle,the secondoneisashorthandwayofaddingaBrickinstance: defadd_ball(self): ifself.ballisnotNone: self.ball.delete() paddle_coords=self.paddle.get_position() x=(paddle_coords[0]+paddle_coords[2])*0.5 self.ball=Ball(self.canvas,x,310) self.paddle.set_ball(self.ball) defadd_brick(self,x,y,hits): brick=Brick(self.canvas,x,y,hits) self.items[brick.item]=brick Thedraw_textmethodwillbeusedtodisplaytextmessagesinthecanvas.The underlyingitemcreatedwithcanvas.create_text()isreturned,anditcanbeusedto modifytheinformation: defdraw_text(self,x,y,text,size='40'): font=('Helvetica',size) returnself.canvas.create_text(x,y,text=text, font=font) Theupdate_lives_textmethoddisplaysthenumberoflivesleftandchangesitstextif themessageisalreadydisplayed.Itiscalledwhenthegameisinitialized—thisiswhen thetextisdrawnforthefirsttime—anditisalsoinvokedwhentheplayermissesaball rebound: defupdate_lives_text(self): text='Lives:%s'%self.lives ifself.hudisNone: self.hud=self.draw_text(50,20,text,15) else: self.canvas.itemconfig(self.hud,text=text) Weleavestart_gameunimplementedfornow,sinceittriggersthegameloop,andthis logicwillbeaddedinthenextsection.SincePythonrequiresacodeblockforeach method,weusethepassstatement.Thisdoesnotexecuteanyoperation,anditcanbe usedasaplaceholderwhenastatementisrequiredsyntactically: defstart_game(self): pass Seethechapter1_02.pymodule,ascriptwiththesamplecodewehavesofar.Ifyou executethisscript,itwilldisplayaTkinterwindowliketheoneshowninthefollowing figure.Atthispoint,wecanmovethepaddlehorizontally,sowearereadytostartthe gameandhitsomebricks: Movementandcollisions Nowthatwehaveplacedallofourgameobjects,wecandefinethemethodsthatwillbe executedinthegameloop.Thislooprunsindefinitelyuntilthegameends,andeach iterationupdatesthepositionoftheballandchecksthecollisionthatoccurs. WiththeCanvaswidget,wecancalculatewhattheitemsthatoverlapwiththegiven coordinatesare,sofornow,wewillimplementthemethodsthatareresponsiblefor movingtheballandchangingitsdirection. Let’sstartwiththemovementoftheballandtheconditionsforrecreatingthebouncing effectwhenitreachesthecanvasborders: defupdate(self): coords=self.get_position() width=self.canvas.winfo_width() ifcoords[0]<=0orcoords[2]>=width: self.direction[0]*=-1 ifcoords[1]<=0: self.direction[1]*=-1 x=self.direction[0]*self.speed y=self.direction[1]*self.speed self.move(x,y) Theupdatemethoddoesthefollowing: Itgetsthecurrentpositionandthewidthofthecanvas.Itstoresthevaluesinthe coordsandwidthlocalvariables,respectively. Ifthepositioncollideswiththeleftorrightborderofthecanvas,thehorizontal componentofthedirectionvectorchangesitssign Ifthepositioncollideswiththeupperborderofthecanvas,theverticalcomponentof thedirectionvectorchangesitssign Wescalethedirectionvectorbytheball’sspeed Theself.move(x,y)movestheball Forinstance,iftheballhitstheleftborder,thecoords[0]<=0conditionevaluatesto true,sothex-axiscomponentofthedirectionchangesitssign,asshowninthisdiagram: Iftheballhitsthetop-rightcorner,bothcoords[2]>=widthandcoords[1]<=0 evaluatetotrue.Thischangesthesignofboththecomponentsofthedirectionvector,like this: Thelogicofthecollisionwithabrickisabitmorecomplex,sincethedirectionofthe rebounddependsonthesidewherethecollisionoccurs. Wewillcalculatethex-axiscomponentoftheball’scenterandcheckwhetheritis betweenthelowermostanduppermostx-axiscoordinatesofthecollidingbrick.To translatethisintoaquickimplementation,thefollowingsnippetshowsthepossible changesinthedirectionvectoraspertheballandbrickcoordinates: coords=self.get_position() x=(coords[0]+coords[2])*0.5 brick_coords=brick.get_position() ifx>brick_coords[2]: self.direction[0]=1 elifx<brick_coords[0]: self.direction[0]=-1 else: self.direction[1]*=-1 Forinstance,thiscollisioncausesahorizontalrebound,sincethebrickisbeinghitfrom above,asshownhere: Ontheotherhand,acollisionfromtheright-handsideofthebrickwouldbeasfollows: Thisisvalidwhentheballhitsthepaddleorasinglebrick.However,theballcanhittwo bricksatthesametime.Inthissituation,wecannotexecutethepreviousstatementsfor eachbrick;ifthey-axisdirectionismultipliedby-1twice,thevalueinthenextiteration ofthegameloopwillbethesame. Wecouldcheckwhetherthecollisionoccurredfromaboveorbehind,buttheproblem withmultiplebricksisthattheballmayoverlapthelateralofoneofthebricksand, therefore,changethex-axisdirectionaswell.Thishappensbecauseoftheball’sspeed andtherateatwhichitspositionisupdated. Wewillsimplifythisbyassumingthatacollisionwithmultiplebricksatthesametime occursonlyfromaboveorbelow.Thatmeansthatitchangesthey-axiscomponentofthe directionwithoutcalculatingthepositionofthecollidingbricks: iflen(game_objects)>1: self.direction[1]*=-1 Withthesetwoconditions,wecandefinethecollidemethod.Aswewillseelater, anothermethodwillberesponsiblefordeterminingthelistofcollidingbricks,sothis methodonlyhandlestheoutcomeofacollisionwithoneormorebricks: defcollide(self,game_objects): coords=self.get_position() x=(coords[0]+coords[2])*0.5 iflen(game_objects)>1: self.direction[1]*=-1 eliflen(game_objects)==1: game_object=game_objects[0] coords=game_object.get_position() ifx>coords[2]: self.direction[0]=1 elifx<coords[0]: self.direction[0]=-1 else: self.direction[1]*=-1 forgame_objectingame_objects: ifisinstance(game_object,Brick): game_object.hit() Notethatthismethodhitseverybrickinstancethatiscollidingwiththeball,sothehit countersaredecreasedandthebricksareremovediftheyreachzerohits. Startingthegame Finally,wehavebuiltthefunctionalityneededtorunthegameloop—thelogicrequiredto updatetheball’spositionaccordingtotherebounds,andrestartthegameiftheplayer losesonelife. NowwecanaddthefollowingmethodstoourGameclasstocompletethedevelopmentof ourgame: defstart_game(self): self.canvas.unbind('<space>') self.canvas.delete(self.text) self.paddle.ball=None self.game_loop() defgame_loop(self): self.check_collisions() num_bricks=len(self.canvas.find_withtag('brick')) ifnum_bricks==0: self.ball.speed=None self.draw_text(300,200,'Youwin!') elifself.ball.get_position()[3]>=self.height: self.ball.speed=None self.lives-=1 ifself.lives<0: self.draw_text(300,200,'GameOver') else: self.after(1000,self.setup_game) else: self.ball.update() self.after(50,self.game_loop) Thestart_gamemethod,whichweleftunimplementedinaprevioussection,is responsibleforunbindingtheSpacebarinputkeysothattheplayercannotstartthegame twice,detachingtheballfromthepaddle,andstartingthegameloop. Stepbystep,thegame_loopmethoddoesthefollowing: Itcallsself.check_collisions()toprocesstheball’scollisions.Wewillseeits implementationinthenextcodesnippet. Ifthenumberofbricksleftiszero,itmeansthattheplayerhaswon,anda congratulationstextisdisplayed. Supposetheballhasreachedthebottomofthecanvas: Then,theplayerlosesonelife.Ifthenumberoflivesleftiszero,itmeansthat theplayerhaslost,andtheGameOvertextisshown.Otherwise,thegameis reset Otherwise,thisiswhathappens: Thepositionoftheballisupdatedaccordingtoitsspeedanddirection,andthe gameloopiscalledagain.The.after(delay,callback)methodonaTkinter widgetsetsatimeouttoinvokeafunctionafteradelayinmilliseconds.Since thisstatementwillbeexecutedwhenthegameisnotoveryet,thiscreatesthe loopnecessarytoexecutethislogiccontinuously: defcheck_collisions(self): ball_coords=self.ball.get_position() items=self.canvas.find_overlapping(*ball_coords) objects=[self.items[x]forxinitems\ ifxinself.items] self.ball.collide(objects) Thecheck_collisionsmethodlinksthegameloopwiththeballcollisionmethod.Since Ball.collidereceivesalistofgameobjectsandcanvas.find_overlappingreturnsalist ofcollidingitemswithagivenposition,weusethedictionaryofitemstotransformeach canvasitemintoitscorrespondinggameobject. RememberthattheitemsattributeoftheGameclasscontainsonlythosecanvasitemsthat cancollidewiththeball.Therefore,weneedtopassonlytheitemscontainedinthis dictionary.Oncewehavefilteredthecanvasitemsthatcannotcollidewiththeball,such asthetextdisplayedinthetop-leftcorner,weretrieveeachgameobjectbyitskey. Withlistcomprehensions,wecancreatetherequiredlistinonesimplestatement: objects=[self.items[x]forxinitemsifxinself.items] Thebasicsyntaxoflistcomprehensionsisthefollowing: new_list=[expr(elem)forelemincollection] Thismeansthatthenew_listvariablewillbealistwhoseelementsaretheresultof applyingtheexprfunctiontoeacheleminthelistcollection. Wecanfiltertheelementstowhichtheexpressionwillbeappliedbyaddinganifclause: new_list=[expr(elem)forelemincollectionifelemisnotNone] Thissyntaxisequivalenttothefollowingloop: new_list=[] forelemincollection: ifelemisnotNone: new_list.append(elem) Inourcase,theinitiallististhelistofcollidingitems,theifclausefilterstheitemsthat arenotcontainedinthedictionary,andtheexpressionappliedtoeachelementretrieves thegameobjectassociatedwiththecanvasitem.Thecollidemethodiscalledwiththis listasaparameter,andthelogicforthegameloopiscompleted. PlayingBreakout Openthechapter1_complete.pyscripttoseethefinalversionofthegame,andrunitby executingchapter1_complete.py,asyoudidwiththepreviouscodesamples. Whenyoupressthespacebar,thegamestartsandtheplayercontrolsthepaddlewiththe rightandleftarrowkeys.Eachtimetheplayermissestheball,thelivescounterwill decrease,andthegamewillbeoveriftheballreboundismissedagainandthereareno livesleft: Inourfirstgame,alltheclasseshavebeendefinedinasinglescript.However,asthe numberoflinesofcodeincreases,itbecomesnecessarytodefineseparatescriptsforeach part.Inthenextchapters,wewillseehowitispossibletoorganizeourcodebymodules. Summary Inthischapter,webuiltoutfirstgamewithvanillaPython.Wecoveredthebasicsofthe controlflowandtheclasssyntax.WeusedTkinterwidgets,especiallytheCanvaswidget anditsmethods,toachievethefunctionalityneededtodevelopagamebasedoncollisions andsimpleinputdetection. OurBreakoutgamecanbecustomizedaswewant.Feelfreetochangethecolordefaults, thespeedoftheball,orthenumberofrowsofbricks. However,GUIlibrariesareverylimited,andmorecomplexframeworksarerequiredto achieveawiderrangeofcapabilities.Inthenextchapter,wewillintroduceCocos2d,a gameframeworkthathelpsuswiththedevelopmentofournextgame. Chapter2.CocosInvaders Inthepreviouschapter,webuiltagamewithaGraphicalUserInterface(GUI)package calledTkinter.SinceitispartofthePython’sstandardlibraries,itwaseasytosetupthe projectandcreatetherequiredwidgetswithafewlinesofcode.However,thiskindof modulefallsshortofprovidingthecorefunctionalityofgamedevelopmentbeyondthe2D graphics. Theprojectcoveredinthischapterisdevelopedwithcocos2d.Thisframeworkhasforks fordifferentprogramminglanguagesbesidesPython,suchasC++,Objective-C,and JavaScript. Thegamethatwewilldevelopisavariantofaclassicalgame,SpaceInvaders.This arcadewasasuccessofitstime,andithasbecomepartofthecultureofvideogaming. Inthistwo-dimensionalgame,theplayermustdefeatwavesofdescendingaliensby shootingthemwithalasercannon,whichcanbemovedhorizontallyandisprotectedby somedefensebunkers.Theenemyfireattemptstodestroytheplayer’scannon,andit graduallydamagesthebunkers. Inourversion,wewillleaveoutthedefensebunkers,andthegamewilllookasfollows: Withthisproject,youwilllearnthefollowing: Thefoundationsofcocos2d Howtoworkwithsprites Processinginputevents Handlingmovementsandcollisions Complementingcocos2dwiththePygletAPI Installingcocos2d Inourpreviousgame,wedidnotuseanythird-partypackagesinceTkinterwaspartofthe Pythonstandardlibrary.Thisisnotthecasewithournewgameframework,whichmust bedownloadedandinstalled. ThePythonPackageIndex(alsocalledPyPI)isanofficialsoftwarerepositoryofthirdpartypackages,andthankstopackagemanagersystems,thisprocessbecomesvery straightforward.Thepipisthepackagingtoolrecommendation,anditisalreadyincluded inthelatestPythoninstallations. Youcancheckwhetherpipisinstalledbyrunningpip--version.Theoutputindicates theversionofpipandwhereitislocated.Thisinformationmayvarydependingonyour Pythonversionanditsinstallationdirectory: $pip--version pip6.0.8fromC:\Python34\lib\site-packages(python3.4) Sincecocos2disavailableonthePyPI,youcaninstallitbyrunningthefollowing command: $pipinstallcocos2d Thiscommanddownloadsandinstallsnotonlythecocos2dpackagebutalsoits dependencies(Pygletandsix).Oncerun,theconsoleoutputshouldprinttheprogressand indicatethattheinstallationwasexecutedsuccessfully.Toverifythatcocos2discorrectly installed,youcanrunthiscommand: $python-c"importcocos;print(cocos.version)" 0.6.0 Atthetimeofwritingthisbook,thelatestversionofcocos2dis0.6.0,sotheoutputmight beagreaterversionthanthisone. Tip ThePythonpackagingecosystem Apartfrompip,thereareotherpackagingutilities,suchaseasy_installandconda.Each toolhasdifferentfeaturesandlimitations,butinthisbook,wewillstickwithpipdueto itseaseofuseandthefactthatitisincludedinthelatestPythoninstallationsbydefault. Gettingstartedwithcocos2d Aswithanyotherframework,cocos2discomposedofseveralmodulesandclassesthat implementdifferentfeatures.InordertodevelopapplicationswiththisAPI,weneedto understandthefollowingbasicconcepts: Scene:Eachofthestagesofyourapplicationisascene.Whileyourgamemayhave manyscenes,onlyoneisactiveatanygivenpointintime.Thetransitionbetween scenesdefinesyourgame’sworkflow. Layer:Everysheetcontainedinascene,whoseoverlaycreatesthefinalappearance ofthescene,iscalledalayer.Forinstance,yourgame’smainscenemayhavea backgroundlayer,anHUDlayerwithplayerinformationandscores,andan animationlayer,whereeventsandcollisionsbetweenspritesarebeingprocessed. Sprite:Thisisa2Dimagethatcanbemanipulatedthoughcocos2dactions,suchas move,scale,orrotate.Inourgames,spriteswillrepresenttheplayer’scharacter, enemies,andvisualinformation,suchasthenumberoflivesleft. Director:Thisisasharedobjectthatinitializesthemainwindowandcontrolsthe currentsceneaswellastherestofthescenesthatareonhold. AlloftheseclassesexceptDirectorinheritfromCocosNode.Thisclassrepresents elementsthatare—orcontain—nodesthatgetdrawn,anditispartofthecoreofthe library. Thefunctionalityofthisclasscoversparent-childrelations,specialplacements,rendering, andscheduledactions.CocosNodesarealsonotifiedwhentheyareaddedasachildof anothernodeandwhentheyareremovedfromtheirparentnode. Thistablehasasummaryofthemostrelevantmembersofthisclass: Member Description add(child,z=0, name=None) Thisaddsachild.Itraisesanexceptionifthenamealreadyexists. remove(child) Thisremovesachildgivenitsnameortheobject.Itraisesanexceptionifthechildisnoton thelistofchildren. kill() Removesitselffromitsparent. get_children() Returnsalistofthechildrenofthenode,orderedbytheirzindex. parent Theparentnode. position Thepositioncoordinatesrelativetotheparentnode. x Thex-axispositioncoordinate. y They-axispositioncoordinate. schedule(callback) Thisschedulesafunctiontobecalledeveryframe. on_enter() Thisiscalledwhenaddedasachildwhiletheparentisintheactivescene,orifthescene goesactive. on_exit() Thisiscalledwhenremovedfromitsparentwhiletheparentisintheactivescene,orifthe scenegoesactive. Withthesedefinitionsinmind,wecanbuildourfirstcocos2dapplication.Itwillbea simpleapplicationinwhichtheplayermustpickupfourobjectsplacedaroundthescene. Theplayercharactercanbemovedwiththearrowkeys,andthereareneithermenusnor displayinformation. Theapplicationwilllooklikethis: Ascanbeseen,thefunctionalitywillbequitebasic,sincethepurposeofthisexampleis tofamiliarizeyourselfwiththecocos2dmodules. Remembertosavethescriptinthesamedirectoryasthe'ball.png'image,sinceitis loadedbyyourgame.Thefirstiterationofourappcontainsthefollowingcode: importcocos classMainLayer(cocos.layer.Layer): def__init__(self): super(MainLayer,self).__init__() self.player=cocos.sprite.Sprite('ball.png') self.player.color=(0,0,255) self.player.position=(320,240) self.add(self.player) if__name__=='__main__': cocos.director.director.init(caption='Hello,Cocos') layer=MainLayer() scene=cocos.scene.Scene(layer) cocos.director.director.run(scene) Firstofall,weimportthecocosmodule.Wecanimportonlytherequiredmodules separately(cocos.sprite,cocos.layer,cocos.scene,andcocos.director),whichhas theadvantageofaquickerinitialization.However,forthisbasicapp,wewillfollowa simplerapproach. Wehavedefinedacustomlayerbyinheritingfromthecocos.layer.Layerclass.Inthis example,wecallitsparent__init__methodonly,butaswewillseelater,thereareother Layersubclassesthatcanbeusedforspecializedfunctionality,suchasColorLayerand Menu. Thelayercontainsasinglecocos.sprite.Spriteinstancewiththe'ball.png'image, whichwillrepresenttheplayercharacter.ThecolorattributeindicatestheRGBcolor appliedtothespriteimage,representedasatuplewhosevaluesrangefrom0to255.The positionattribute,declaredintheCocosNodeclass,setsthesprite’scentercoordinates.It isalsopossibletopassthesevaluesaskeywordargumentstothe__init__method. Themaincodeblockperformsthefollowingactions: InitializesthemainwindowwiththeDirectorinstance InstantiatesourLayersubclasswiththesprite Createsanemptyscenethatcontainsthelayer RunstheDirectormainloopwiththescene Sincedirector.initwrapsthecreationofaPygletwindow,thecompletelistof keywordparametersthatcanbepassedtothismethodisavailableatthePygletWindow APIReference. Forourcocos2dgames,onlyasubsetoftheseparameterswillbeused,andthemost relevantkeywordparametersarethefollowing: Parametername Description Defaultvalue fullscreen Aflagforcreatingthewindowthatfillstheentirescreen False resizable Indicateswhetherthewindowisresizable False vsync Syncwiththeverticalretrace True width Windowwidthinpixels 640 height Windowheightinpixels 480 caption Windowtitle(string) visible Indicateswhetherthewindowisvisibleornot True Oncewehavesetuptheapplication,weextenditsfunctionalitybyhandlinginputevents. Handlinguserinput EachLayerisresponsibleforhandlinginputeventsthroughtheis_event_handlerflag, whichissettoFalsebydefault.IfthisflagissettoTrue,theLayerwillreceiveinput eventsandcallthecorrespondinghandlermethods. TheseareeventlistenersfromtheLayerclass: on_mouse_motion(x,y,dx,dy):The(x,y)arethephysicalcoordinatesofthe mouse,and(dx,dy)isthedistancevectorcoveredbythemousesincethelastcall on_mouse_press(x,y,buttons,modifiers):Justasinon_mouse_motion,(x,y) arethephysicalcoordinatesofthemouse,butthisiscalledwhenamousebuttonis pressed on_mouse_drag(x,y,dx,dy,buttons,modifiers):Acombinationof on_mouse_pressandon_mouse_motion,thisiscalledwhenthemousemovesover withoneormorebuttonspressed on_key_press(key,modifiers):Thisiscalledwhenakeyispressed on_key_release(key,modifiers):Thisiscalledwhenakeyisreleased ThebuttonsparameterisabitwiseORofPygletbuttonconstantsthataredefinedinthe pyglet.window.mouseandpyglet.window.keymodulesfortheon_mouseandon_key events,respectively.ThemodifiersparameterisabitwiseORofthepyglet.window.key constants,suchasShift,Option,andAlt.Youcanfindalltheconstantsdefinedinthis moduleathttps://pythonhosted.org/pyglet/api/pyglet.window.key-module.html. WhenyoudefineoneofthesemethodsinyourLayersubclassandthecorrespondinginput eventoccurs,themethodiscalled. Inthisiterationofoursamplegame,wewillprintamessagewhenakeyispressedor released: Tip NotethatsincePygletisusedinternallyforeventhandling,weneedthesymbol_string functionfromthepyglet.window.keymoduletodecodethenameofthekey. importcocos frompyglet.windowimportkey classMainLayer(cocos.layer.Layer): is_event_handler=True def__init__(self): super(MainLayer,self).__init__() self.player=cocos.sprite.Sprite('ball.png') self.player.color=(0,0,255) self.player.position=(320,240) self.add(self.player) defon_key_press(self,k,m): print('Pressed',key.symbol_string(k)) defon_key_release(self,k,m): print('Released',key.symbol_string(k)) Tip Wehaveomittedtheif__name__=='__main__'block,butremembertoincludeitin yourscriptsinordertoexecutetheapplication. Whenyoulaunchit,thekeyboardeventsmustbeprintedontheconsole.Ournextstep willbetoreflecttheseeventsinthegamestatebymodifyingthesprite’sposition. Tip ThePygleteventframework Eventhandlingisimplementedincocos2dwiththePygleteventframework.Thisevent frameworkallowsyoutodefineemitterswithagiveneventname(suchason_key_press) andregisterlistenersovertheseevents. RememberthatPygletisthemultimedialibraryonwhichcocos2drelies,sowewillsee moreusagesofthisAPIsincemajorcocos2dapplicationsusePygletmodules. Updatingthescene Sofar,ourgameonlydisplaysthesamecontent,asobjectsarebeingmodified.Inaway similartotheTkintercallbackthatwasloopingduringtheexecutionofourlastgame,we needtorefreshourcocos2dapplicationperiodically. ThisisachievedwiththeschedulemethodfromtheCocosNodeclass.Itschedulesa functionthatiscalledoneveryframe,andthefirstargumentthatthisfunctionreceivesis theelapsedtimeinsecondssincethelastclocktick. Whileitispossibletopassadditionalargumentstothiscallbackfunction,wewilldefinea newmethodintheMainLayerclasswiththeelapsedtimeparameteronly: importcocos fromcollectionsimportdefaultdict frompyglet.windowimportkey classMainLayer(cocos.layer.Layer): is_event_handler=True def__init__(self): super(MainLayer,self).__init__() self.player=cocos.sprite.Sprite('ball.png') self.player.color=(0,0,255) self.player.position=(320,240) self.add(self.player) self.speed=100.0 self.pressed=defaultdict(int) self.schedule(self.update) defon_key_press(self,k,m): self.pressed[k]=1 defon_key_release(self,k,m): self.pressed[k]=0 defupdate(self,dt): x=self.pressed[key.RIGHT]-self.pressed[key.LEFT] y=self.pressed[key.UP]-self.pressed[key.DOWN] ifx!=0ory!=0: pos=self.player.position new_x=pos[0]+self.speed*x*dt new_y=pos[1]+self.speed*y*dt self.player.position=(new_x,new_y) Wehavemodifiedon_key_pressandon_key_releasesothattheysavethekeystrokein defaultdict,aspecialdictionaryfromthecollectionsmodule.Thisdatastructure returnsthecorrespondingvalueifthekeyispresentorthedefaultvalueofthetypeifthe keydoesnotexist.Inourcase,sinceitisdefaultdict(int),thedefaultvalueforthe absentkeysis0. Wehavescheduledtheupdatemethod,andforeachframe,itchecksthevaluesofthe pressedarrowkeys.Ifthedifferencebetweenthesevaluesisdistincttozero,thespriteis moved. Processingcollisions Incocos2d,proximityandcollisionsamongactorsarecalculatedwithaninterfaceofthe cocos.collision_modelpackagecalledCollisionManager.Thisinterfacecanbeusedto determinewhethertwoobjectsareoverlapping,orwhichobjectsarecloserthanacertain distancefromagivenobject. Actorsshouldbeaddedfirsttothesetofobjectsthataremanagedbythecollision manager,alsocalledknownentities.Thesetofmanagedobjectsisinitiallyempty. TheCollisionManagerinterfaceisimplementedbytwoclasses: CollisionManagerBruteForce:Thisisastraightforwardimplementation.Init,all knownentitiesareusedtocalculateproximityandcollisions.Itisintendedfor debuggingpurposes,anditdoesnotscaleifthesetofknownentitiesgrowsinsize.It doesnotneedanyargumentsinitsinitialization. CollisionManagerGrid:Thisdividesthespaceintorectangularcellswithagiven widthandheight.Whencalculatingthespatialrelationsbetweenobjects,itonly considerstheobjectsfromtheknownentitiesthatoverlapthesamecell.Its performanceisbettersuitedforalargenumberofknownentities.The__init__ methodrequirestheminimumandmaximumcoordinatesofthespace,thecellwidth, andthecellsize.Therecommendedcellsizeisthemaximumobjectwidthandheight multipliedby1.25.Notethatasthisfactorincreasesitsvalue,moreobjectsoverlap thesamecellandtheperformancestartstodegrade. ThereferenceoftheCollisionManagermethodsthatwearegoingtouseinthischapteris thefollowing: Method Description add(obj) Makesobjaknownobject clear() Emptiesthesetofknownobjects iter_colliding(obj) Returnsaniteratorofobjectscollidingwithobj knows(obj) Thisistrueifobjisaknownobject AnobjectcanbecollidedwithifithasamembercalledCshapeanditsvalueisan instanceofcocos.collision_model.Cshape.Therearetwoclassesincocos2dthatinherit fromCshape: CircleShape:Thisusesacircleasthegeometricspace.Whenaninstanceiscreated, thecenterofthecircleanditsradiusmustbespecified.Twocirclescollideifthe Euclideandistancebetweentheircentersislessthanthesumoftheirradius. AARectShape:Thisusesarectangleasthegeometricspace.Therectangleisaxisaligned,whichmeansthatitssidesareparalleltothexandyaxes.Thiscanbewell suitediftheactorsdonotrotate.InsteadoftheEuclideandistance,itusesthe Manhattandistancetocalculateacollisionbetweentworectangles. Inourexample,wewillusecircularshapesforouractors.Inordertotestthecollision manager,fourspritesareaddedtotheLayer,andtheywillactaspickups—theywillbe destroyedwhentheycollidewiththeplayer’ssprite. Besides,sinceCshaperequiresacocos.euclid.Vector2forthecentercoordinates,we willreplacethetuplesfortheVector2instances. Beforeaddingthecollisionmanager,wedefineanewclasstoreplaceoursprites.The actorsofourgamesharetheseattributes,sowewillavoidcodeduplication: importcocos importcocos.collision_modelascm importcocos.euclidaseu fromcollectionsimportdefaultdict frompyglet.windowimportkey classActor(cocos.sprite.Sprite): def__init__(self,x,y,color): super(Actor,self).__init__('ball.png',color=color) self.position=pos=eu.Vector2(x,y) self.cshape=cm.CircleShape(pos,self.width/2) Nowthatwehavethisbaseclass,wecanaddtheseobjectswiththerequiredcshape member.Thecollisionmanagerimplementationthatwewilluseinallourapplicationsis CollisionManagerGrid,sincethebrute-forceimplementationisonlyintendedfor referenceanddebugging: classMainLayer(cocos.layer.Layer): is_event_handler=True def__init__(self): super(MainLayer,self).__init__() self.player=Actor(320,240,(0,0,255)) self.add(self.player) forposin[(100,100),(540,380),\ (540,100),(100,380)]: self.add(Actor(pos[0],pos[1],(255,0,0))) cell=self.player.width*1.25 self.collman=cm.CollisionManagerGrid(0,640,0,480, cell,cell) self.speed=100.0 self.pressed=defaultdict(int) self.schedule(self.update) defon_key_press(self,k,m): self.pressed[k]=1 defon_key_release(self,k,m): self.pressed[k]=0 Nowtheupdatemethodmustperformthefollowingstepsinordertobeabletodetect collisionsofmovingobjects:clearthecollisionmanager,addtheactorstothesetof knownentities,anditerateovertheobjects’collisions: defupdate(self,dt): self.collman.clear() for_,nodeinself.children: self.collman.add(node) forotherinself.collman.iter_colliding(self.player): self.remove(other) x=self.pressed[key.RIGHT]-self.pressed[key.LEFT] y=self.pressed[key.UP]-self.pressed[key.DOWN] ifx!=0ory!=0: pos=self.player.position new_x=pos[0]+self.speed*x*dt new_y=pos[1]+self.speed*y*dt self.player.position=(new_x,new_y) self.player.cshape.center=self.player.position Notethestatementinwhichthecshapecenteroftheplayerspriteisupdatedwiththenew position.Withoutthisline,thecollisionmanagerwillnotdetectanycollisionbetweenthe playerandthepickups,sincetheentitycenterwillalwaysbeintheinitialposition. Inthechapter2_01.pyscript,youcanfindthecompletecode.Forthisapplication,we onlyneededasinglesprite.However,mostgamesuseseveralimagestorepresent differentgamecharactersandtheiranimations. Inthenextsection,wewillseethespritesthatourSpaceInvadersgamewillhave. Creatinggameassets Wewillneedafewbasicimagesforourcocos2dsprites.Theywillbethevisual representationsoftheplayer’scannonandthedifferenttypesofaliens. Thefollowingimageshowshowthesespritescanbedrawnwithanimageeditorprogram, withthehelpofagrid: Unfortunately,theusageofimagemanipulationtoolsisbeyondthescopeofthisbook. Thereisawidevarietyintheseprograms,andforbasicsprites(suchastheonesshownin thepreviousimage),youcanuseMSPaintonWindowssystems. Undertheimgfolder,youcanfindthedifferentimagesthatwewilluseinourgame.Feel freetoeditthemandchangetheircolors.However,donotmodifytheirsizeasitisusedto determinethewidthandtheheightoftheSpriteanditsCshapemember. Tip Creatingyourown“pixelart” Therearemoreadvancedapplicationsthatcanberunonanyplatform.Thisisthecaseof GIMP,anopensourceprogramthatispartoftheGNUproject.Thespritesusedinthis chapterhavebeendrawnwithGIMP2,thecurrentversion. Youcandownloaditforfreefromhttp://www.gimp.org/downloads/. SpaceInvadersdesign Beforewritingourgamewithcocos2d,weshouldconsiderwhichactorsweneedtomodel inordertorepresentourgameentities.InourSpaceInvadersversion,wewillusethe followingclasses: PlayerCannon:Thisisthecharactercontrolledbytheplayer.Thespacebarisusedto shoot,andthehorizontalmovementiscontrolledwiththerightandleftarrowkeys. Alien:Eachoneofthedescendingenemies,withdifferentlooksandscores dependingonthealientype. AlienColumn:Therearecolumnsoffivealiensintowhicheverygroupofaliensis divided. AlienGroup:Awholegroupofenemies.Itmoveshorizontallyordescendsuniformly. Shoot:Asmallprojectilelaunchedbytheenemiestowardstheplayercharacter. PlayerShoot:AShootsubclass.Itislaunchedbytheplayerratherthanthealien group. Apartfromtheseclasses,weneedacustomLayerforthegamelogic,whichwillbe namedGameLayer,andabaseclassforthelogicthatissharedamongtheactors—similar totheGameObjectclassfromourpreviousgame. WewillcallitActor,aswedidinourpreviouscocos2dapplication.Infact,itisquite similartothatone,exceptthatthisimplementationaddsthedefinitionoftwomethods: updateandcollide.Theywillbecalledwhenourgameloopupdatesthepositionofthe nodesandwhenaknownentityhitstheactor,respectively. Thismoduleisnew,apartfromthecodewesawintheprevioussection,anditwillcontain thefollowingcode: importcocos.sprite importcocos.collision_modelascm importcocos.euclidaseu classActor(cocos.sprite.Sprite): def__init__(self,image,x,y): super(Actor,self).__init__(image) self.position=eu.Vector2(x,y) self.cshape=cm.AARectShape(self.position, self.width*0.5, self.height*0.5) defmove(self,offset): self.position+=offset self.cshape.center+=offset defupdate(self,elapsed): pass defcollide(self,other): pass ThePlayerCannon,Alien,Shoot,andPlayerShootclasseswillextendfromthisclass. Therefore,withthislittlepieceofcode,wehavesetuptheinterfaceforcollisiondetection andthemovementsofallourgameobjects. ThePlayerCannonandGameLayerclasses Aswesawearlier,PlayerCannonisoneofourgameactors.Itmustrespondtotheleftand rightkeystrokestocontrolthehorizontalmovementofthesprite,andtheobjectis destroyedifanenemy’sshothitsit. Wewillimplementthisbyoverridingbothupdateandcollide: fromcollectionsimportdefaultdict frompyglet.windowimportkey classPlayerCannon(Actor): KEYS_PRESSED=defaultdict(int) def__init__(self,x,y): super(PlayerCannon,self).__init__('img/cannon.png',x,y) self.speed=eu.Vector2(200,0) defupdate(self,elapsed): pressed=PlayerCannon.KEYS_PRESSED movement=pressed[key.RIGHT]-pressed[key.LEFT] w=self.width*0.5 ifmovement!=0andw<=self.x<=self.parent.width-w: self.move(self.speed*movement*elapsed) defcollide(self,other): other.kill() self.kill() Todeterminewhetherakeyispressedornot,wehavedefineddefaultdict,asinour basiccocos2dapplication.Inthiscase,itisaclassattributeofPlayerCannon.The horizontalmovementislimitedsothatthecharactercannotleavethecollisionmanager grid. TheGameLayerclasswillbethemainlayerofourgame,anditwillberesponsiblefor doingthefollowing: Keepingtrackofthenumberofplayerlivesleftandthecurrentscore Handlinginputkeyeventsbysettingitsis_event_handlerflagtotrue Creatingthegameactorsandaddingthemaschildnodes Runningthegameloopbyexecutingascheduledfunctionforeachframewherethe collisionsareprocessedandtheobjectpositionsareupdated Itsinitialimplementationisthefollowing: importcocos.layer classGameLayer(cocos.layer.Layer): is_event_handler=True defon_key_press(self,k,_): PlayerCannon.KEYS_PRESSED[k]=1 defon_key_release(self,k,_): PlayerCannon.KEYS_PRESSED[k]=0 def__init__(self): super(GameLayer,self).__init__() w,h=cocos.director.director.get_window_size() self.width=w self.height=h self.lives=3 self.score=0 self.update_score() self.create_player() self.create_alien_group(100,300) cell=1.25*50 self.collman=cm.CollisionManagerGrid(0,w,0,h, cell,cell) self.schedule(self.update) Thecreate_playermethodaddsaPlayerCannoninthecenterofthescreen.Itwillbe calledeachtimetheplayercharacterneedstoberespawned,whileupdate_score incrementsthescorebyaddingthepointsforeachalienthatisdestroyed.Thedefault valueofthescore=0argumentinthemethodsignaturemeansthatifnoargumentsare passed,theargument’svaluewillbe0: defcreate_player(self): self.player=PlayerCannon(self.width*0.5,50) self.add(self.player) defupdate_score(self,score=0): self.score+=score Thecreate_alien_groupmethodinitializestherowsofdescendingaliens.Sincethe requiredclasseshavenotbeenimplementedyet,wewillleaveitwiththepassstatement fornow: defcreate_alien_group(self,x,y): pass Theupdatemethodisacallbackthatwillbescheduledtobeexecutedforeachframe: defupdate(self,dt): self.collman.clear() for_,nodeinself.children: self.collman.add(node) ifnotself.collman.knows(node): self.remove(node) for_,nodeinself.children: node.update(dt) Wedidasmalltrickhere.AswesawintheCollisionManagerreference,theknows methodcheckswhetheranobjectisinthesetofknownentities—whichshouldbealways truesinceallofthelayer’schildrenhavebeenadded. However,whenanobjectisoutsideofthesurfacecoveredbythecollisionmanager,it doesnotoverlapwithanycellofthegridanditisconsideredtobe“notknown.”Inthis way,objectsthatmoveoutofthisareaareautomaticallyremoved: defcollide(self,node): ifnodeisnotNone: forotherinself.collman.iter_colliding(node): node.collide(other) returnTrue returnFalse Finally,thecollidemethodencapsulatesthecalltoiter_collidingandcheckswhether thenodeisareferencetoavalidobject—thePlayerShootinstancecanbeNoneifthereis nocurrentshoot. Asusual,wewritethemainblockforthemomentwhenourscriptisrunasthemain module: if__name__=='__main__': cocos.director.director.init(caption='CocosInvaders', width=800,height=650) game_layer=GameLayer() main_scene=cocos.scene.Scene(game_layer) cocos.director.director.run(main_scene) Invaders! Therearethreeclassesusedtorepresentourinvaders:Alien,AlienColumn,and AlienGroup. Outoftheseclasses,onlyAlieninheritsfromActorbecauseitistheonlyentitythatis drawnandcollideswithotherobjects.Insteadofastaticimage,thespritewillbeabasic animationwhereineachimagewillbeshownfor0.5seconds.Thisisachievedbyloading anImageGridandcreatinganAnimationfromthisspritegrid.Sincetheseclassesbelong tothepyglet.imagemodule,weneedtoimportthemfirst. Anotherfunctionofouralienswillbenotifyingitscolumnthattheobjecthasbeen removed.Thankstothis,thecolumnofaliensknowswhatthebottomoneisandstarts shootingfromitsposition. YoulearnedfromtheCocosNodereferencethattheon_exitmethodiscalledwhenanode isremoved,soyouwillbeoverridingittoinformitscorrespondingcolumn.Notethata referencetothecolumnispassedtothe__init__method.Wecouldhaveimplemented thesamefunctionalitywithaPygleteventhandlermechanism,butthatwouldrequire pushingthehandlerstoeveryAlieninstance: frompyglet.imageimportload,ImageGrid,Animation classAlien(Actor): defload_animation(imgage): seq=ImageGrid(load(imgage),2,1) returnAnimation.from_image_sequence(seq,0.5) TYPES={ '1':(load_animation('img/alien1.png'),40), '2':(load_animation('img/alien2.png'),20), '3':(load_animation('img/alien3.png'),10) } deffrom_type(x,y,alien_type,column): animation,score=Alien.TYPES[alien_type] returnAlien(animation,x,y,score,column) def__init__(self,img,x,y,score,column=None): super(Alien,self).__init__(img,x,y) self.score=score self.column=column defon_exit(self): super(Alien,self).on_exit() ifself.column: self.column.remove(self) TheTYPESclassattributehelpusloadtheanimationsonlyonce,atthebeginningofthe game.Thescoreisthenumberofpointsearnedwhentheenemyisdestroyed,whilethe columnattributecontainsareferencetothecolumntowhichthealienbelongs. TheAlienColumnclassisresponsibleforinstantiatingthecolumnsofalienswithagiven pattern,likethis: SincewehavedefinedtheAlien.from_typeutilitymethod,wecallitintheproperorder toplaceeachtypeofalien,asseeninthepreviousfigure: classAlienColumn(object): def__init__(self,x,y): alien_types=enumerate(['3','3','2','2','1']) self.aliens=[Alien.from_type(x,y+i*60,alien,self) fori,alieninalien_types] defremove(self,alien): self.aliens.remove(alien) defshoot(self):pass Theshould_turnmethodcheckswhether,giventhecurrentdirection,thecolumnhas reachedthesideofthescreenornot.ItwillreturnFalseiftherearenoaliensleftinthe column: defshould_turn(self,d): iflen(self.aliens)==0: returnFalse alien=self.aliens[0] x,width=alien.x,alien.parent.width returnx>=width-50andd==1orx<=50andd==-1 AlltheAlienColumninstancesformtheAlienGroup.Itismoveduniformly,andit delegatestheshootinglogictoeachcolumn.Thismovementisimplementedwiththe speedanddirectionobjectattributes. Whilethealiengroupisbeingupdated,itsumstheelapsedtimesbetweenframes.Whena certainperiodhaselapsed,thewholegroupismoveddownorlaterally.Thedirectionwill dependonwhetheranycolumnhasreachedthelateralsidesornot: classAlienGroup(object): def__init__(self,x,y): self.columns=[AlienColumn(x+i*60,y) foriinrange(10)] self.speed=eu.Vector2(10,0) self.direction=1 self.elapsed=0.0 self.period=1.0 defupdate(self,elapsed): self.elapsed+=elapsed whileself.elapsed>=self.period: self.elapsed-=self.period offset=self.direction*self.speed ifself.side_reached(): self.direction*=-1 offset=eu.Vector2(0,-10) foralieninself: alien.move(offset) defside_reached(self): returnany(map(lambdac:c.should_turn(self.direction), self.columns)) def__iter__(self): forcolumninself.columns: foralienincolumn.aliens: yieldalien Here,wealsodefinea__iter__method,whichisaspecialmethodthatisinvokedwhen youiterateoveranobject.Inthisway,wecancallforalieninalien_groupintherestof ourcode. Nowthatwehaveimplementedthelogicofourenemiesandthemovementofthewhole group,wecanadditsinstantiationtotheGameLayerclass: defcreate_alien_group(self,x,y): self.alien_group=AlienGroup(x,y) foralieninself.alien_group: self.add(alien) Thiscreatesanewaliengroupandaddsalltheenemiestothelayeraschildnodes.The chapter2_02.pyscriptcontainsthecodethatwehavewrittensofar,anditsexecutionwill looklikethis: Shoot’emup! TheShootactor,byitself,isquitebasic;itonlyrequiresaspeedattributeandoverrides theupdatemethodsothattheobjectismovedbythedistancedeterminedbythisspeed andtheelapsedtimebetweenframes: classShoot(Actor): def__init__(self,x,y,img='img/shoot.png'): super(Shoot,self).__init__(img,x,y) self.speed=eu.Vector2(0,-400) defupdate(self,elapsed): self.move(self.speed*elapsed) ThePlayerShootclassrequiresabitmorelogic,sincetheplayercannotshootuntilthe previousbeamhashitanenemyorreachedtheendofthescreen. Aswewanttoavoidglobalvariables,wewilluseaclassattributetoholdthereferenceto thecurrentshot.Whentheshotleavesthescene,thisreferencewillbesettoNoneagain. WewilloverridethecollidemethodfromtheActorclasssothatboththebeamandthe alienaredestroyedwhenacollisionoccurs.Thisisdonebycallingthekillmethod definedintheSpriteclass.ItinternallyremovestheCocosNodefromitsparent: classPlayerShoot(Shoot): INSTANCE=None def__init__(self,x,y): super(PlayerShoot,self).__init__(x,y,'img/laser.png') self.speed*=-1 PlayerShoot.INSTANCE=self defcollide(self,other): ifisinstance(other,Alien): self.parent.update_score(other.score) other.kill() self.kill() defon_exit(self): super(PlayerShoot,self).on_exit() PlayerShoot.INSTANCE=None Withthisclass,wecanaddthisfunctionalitytotheupdatemethodofPlayerCannon: defupdate(self,elapsed): pressed=PlayerCannon.KEYS_PRESSED space_pressed=pressed[key.SPACE]==1 ifPlayerShoot.INSTANCEisNoneandspace_pressed: self.parent.add(PlayerShoot(self.x,self.y+50)) movement=pressed[key.RIGHT]-pressed[key.LEFT] ifmovement!=0: self.move(self.speed*movement*elapsed) TheshootmethodinAlienColumn—whichweleftunimplemented—canbemodified withournewShootclass. Torandomlyshootanewbeam,wewillcallrandom.random(),whichreturnsarandom floatbetweenthesemi-openrangeof(0.0,1.0).Wewillsetalowprobability.Thisis becausethisfunctionwillbecalledseveraltimespersecond.Therandommoduleispart ofthePythonstandardlibrary,soyoucanstartplayingaroundwithitbyaddingthe importrandomstatementatthebeginningofthescript: defshoot(self): ifrandom.random()<0.001andlen(self.aliens)>0: pos=self.aliens[0].position returnShoot(pos[0],pos[1]-50) returnNone NowtheupdatemethodoftheGameLayerclasscancalculatewhichobjectscollidewith thecannonandtheplayer’scurrentshot,aswellasrandomlyshootfromthealien columns: defupdate(self,dt): self.collman.clear() for_,nodeinself.children: self.collman.add(node) ifnotself.collman.knows(node): self.remove(node) self.collide(PlayerShoot.INSTANCE) ifself.collide(self.player): self.respawn_player() forcolumninself.alien_group.columns: shoot=column.shoot() ifshootisnotNone: self.add(shoot) for_,nodeinself.children: node.update(dt) self.alien_group.update(dt) Here,respawn_player()decrementsthenumberoflives,anditunschedulesupdateif therearenolivesleft.Thisstopsthemainloopandrepresentsthegameoversituation: defrespawn_player(self): self.lives-=1 ifself.lives<0: self.unschedule(self.update) else: self.create_player() Inthisiteration,thegamewehavebuiltisverysimilartothefinalversion.Theplayable charactercanbemovedwiththearrowkeysandisabletoshoot.Theenemiesmove uniformly,andtheloweralienofeachrowcanshootaswell.Thecodeforthis implementationcanbefoundinthechapter2_03.pyscript. Now,theonlydetailleftisdisplayingthegameinformation:thecurrentscoreandthe numberoflivesleft. AddinganHUD Our“heads-updisplay”willbeanewLayerthatwillbedrawnoverourGameLayer: classHUD(cocos.layer.Layer): def__init__(self): super(HUD,self).__init__() w,h=cocos.director.director.get_window_size() self.score_text=cocos.text.Label('',font_size=18) self.score_text.position=(20,h-40) self.lives_text=cocos.text.Label('',font_size=18) self.lives_text.position=(w-100,h-40) self.add(self.score_text) self.add(self.lives_text) defupdate_score(self,score): self.score_text.element.text='Score:%s'%score defupdate_lives(self,lives): self.lives_text.element.text='Lives:%s'%lives defshow_game_over(self): w,h=cocos.director.director.get_window_size() game_over=cocos.text.Label('GameOver',font_size=50, anchor_x='center', anchor_y='center') game_over.position=w*0.5,h*0.5 self.add(game_over) OurGameLayerwillholdareferencetotheHUDlayer,anditsmethodswillbemodified sothattheycancalltheHUDdirectly: def__init__(self,hud): super(GameLayer,self).__init__() w,h=cocos.director.director.get_window_size() self.hud=hud self.width=w self.height=h #... Thehudattributewillbeusedwhentheplayerneedstoberespawnedandwhenthescore isupdated: defcreate_player(self): self.player=PlayerCannon(self.width*0.5,50) self.add(self.player) self.hud.update_lives(self.lives) defrespawn_player(self): self.lives-=1 ifself.lives<0: self.unschedule(self.update) self.hud.show_game_over() else: self.create_player() defupdate_score(self,score=0): self.score+=score self.hud.update_score(self.score) Themainblockshouldalsobemodifiedtocreatethescenewiththetwolayers.Here,we passthezindextoindicatetheorderingofthechildrenlayers: if__name__=='__main__': cocos.director.director.init(caption='CocosInvaders', width=800,height=650) main_scene=cocos.scene.Scene() hud_layer=HUD() main_scene.add(hud_layer,z=1) game_layer=GameLayer(hud_layer) main_scene.add(game_layer,z=0) cocos.director.director.run(main_scene) Extrafeature–themysteryship Tocompleteourgame,wewilladdafinalingredient:themysteryshipthatrandomly appearsatthetopofthescreen: classMysteryShip(Alien): SCORES=[10,50,100,200] def__init__(self,x,y): score=random.choice(MysteryShip.SCORES) super(MysteryShip,self).__init__('img/alien4.png',x,y, score) self.speed=eu.Vector2(150,0) defupdate(self,elapsed): self.move(self.speed*elapsed) Inanapproachsimilartotheshootinglogicofourenemies,wewillrandomlyaddthis alientoourscheduledfunction: defupdate(self,dt): self.collman.clear() #... self.alien_group.update(dt) ifrandom.random()<0.001: self.add(MysteryShip(50,self.height-50)) Youcanfindthecompleteimplementationofthisgameinthechapter2_04.pyscript. Enjoyyourfirstcocos2dgame! Summary Inthischapter,weintroducedcocos2danditsmostrelevantmodules.Wedevelopedour firstapplicationtogetstartedwiththelibrary,andlaterwebuiltasimplifiedversionof SpaceInvaders. Thisversioncanbeextendedbyaddingdefensebunkersormorerandomvaluestothe possiblescoresofthemysteryship.Besides,ifyouwanttochangethevisualappearance oftheinvaders,youcaneditthespritesandcreateyourownenemies! Inthenextchapter,wewilldevelopacompletetowerdefensegame,withtransitions betweenscenes,enhanceddisplayinformation,andcomplexmenus. Chapter3.BuildingaTowerDefense Game Inthepreviouschapter,youlearnedthefundamentalsofcocos2d,andnowyouareableto developabasicgamewiththislibrary.However,mostgamesusemorethanasingle scene,andtheircomplexityitisnotlimitedtocollisionsandinputdetection. Withournextgame,wewilldiveintothecocos2dmodulesthatprovideustheadvanced functionalitywearelookingfor:menus,transitions,scheduledactions,andanefficient wayofstoringlevelgraphics. Inthischapter,wewillcoverthesetopics: Howtomanipulatespriteswithcocos2dactions Usingtilemapsasbackgroundlayers Animatedtransitionsbetweengamescenes Howtocreatescenesthatactasmenusandcutscenes Buildingafull-fledgedcocos2dapplicationwithalltheingredientsyouhavelearned sofar Thetowerdefensegameplay Thegenreoftowerdefensechallengestheplayertostopenemycharactersfromreaching acertainpositionbyplacingstrategicallydifferenttowerssothattheycandefeatthe enemiesbeforetheyarriveatthatpoint.Thetowersshootautonomouslytowardsthe enemiesthatarewithintheirfiringrange.Thegameisoverwhenaconcretenumberof enemiesreachtheendpoint. Inourversion,thescenariowillbeameanderingroadinthedesert,andwehavetoprotect abunkerthatisplacedattheendofthisroad,asshowninthefollowingscreenshot.Atthe farend,enemytankswillbespawningrandomly,andwemustplaceturretsthatdestroy thembeforetheyreachourbunker.Theturretscanbeplacedinspecificslots,andeach turretspendspartofourlimitedresources. Cocos2dactions Inourpreviousgame,wemanipulatedourspritesdirectlythroughtheirmembers, especiallythepositionattribute.Thegameloopupdatedeachactorwiththeelapsedtime fromthepreviousframe. However,ourtowerdefensegamewillbebasedmainlyoncocos2dactions,whichare orderstomodifyobjectattributessuchastheposition,rotation,orscale.Theyare executedbycallingthedo()methodoftheCocosNodeclass.Therefore,anysprite,layer, orscenecanbeavalidtargetofanaction. Theactionsthatwewillcoverinthissectioncanbedividedintotwomaingroups: intervalactionsandinstantactions. Intervalactions Theseactionshaveaduration,andtheirexecutionendsafterthatcertainduration.For instance,ifwewanttomoveaspritetoacertainposition,wedonotwantittohappen immediatelybutlastafixedamountoftime,givingtheimpressionthatitmoveswitha determinedspeed.ThiscanbeachievedwiththeMoveToaction: importcocos importcocos.actionsasac if__name__=='__main__': cocos.director.director.init(caption='Actions101') layer=cocos.layer.Layer() sprite=cocos.sprite.Sprite('tank.png',position=(200,200)) sprite.do(ac.MoveTo((250,300),3)) layer.add(sprite) scene=cocos.scene.Scene(layer) cocos.director.director.run(scene) Inthissample,wemoveourtanksprite,initiallyplacedatposition(200,200),to position(250,300).ThesecondargumentofMoveToindicatestheduration,whichis3 seconds.Ifwewantedtousearelativeoffsetinsteadoftheabsolutecoordinates,wecould haveusedtheMoveByaction.TheequivalentinthisexamplewouldbeMoveBy((50, 100),3). Anothercommonactionisrotatinganode,andforthispurpose,cocos2doffersthe RotateByandRotateToactions,whichtaketheangleindegreesandthedurationin seconds: #... sprite=cocos.sprite.Sprite('tank.png',position=(200,200)) sprite.do(ac.RotateBy(180,5))#Rotate180degreesin5sec LikeMoveToandMoveBy,thedifferencebetweenRotateToandRotateByistheuseof absoluteandrelativerotations. Tip Themathmodule ThePythonstandardlibrarycontainsamodulewithcommonlyusedmathematical functions.Sincecocos2dworkswithdegreesforrotationactions,the math.degrees(radians)andmath.radians(degrees)conversionutilitiesincludedinthe mathmodulecanbeextremelyuseful. Seetheentirefunctionalityprovidedbythismoduleat https://docs.python.org/3.4/library/math.html. Thecompletereferenceofinstantactionsincludedinthe cocos.actions.interval_actionsmoduleisthefollowing: Intervalaction Description Lerp Interpolatesbetweenvaluesforaspecifiedattribute MoveTo Movesthetargettotheposition(x,y) MoveBy Movesthetargetbyanoffsetof(x,y) JumpTo Movesthetargettoapositionsimulatingajumpmovement JumpBy Movesthetargetsimulatingajumpmovement Bezier MovesthetargetthroughaBézierpath Blink Blinksthetargetbyhidingandshowingitanumberoftimes RotateTo Rotatesthetargettoacertainangle RotateBy Rotatesatargetclockwisebyanumberofdegrees ScaleTo Scalesthetargettoazoomfactor ScaleBy Scalesthetargetbyazoomfactor FadeOut Fadesoutthetargetbymodifyingitsopacityattribute FadeIn Fadesinthetargetbymodifyingitsopacityattribute FadeTo Fadesthetargettoaspecificalphavalue Delay Delaystheactionbyacertainnumberofseconds RandomDelay Delaystheactionrandomlybetweenaminimumvalueandamaximumvalueofseconds Instantactions Wehavejustintroducedintervalactions.Theseareappliedoveradurationoftime.Now wewillcoverinstantactions,whichareappliedimmediatelytotheCocosNodeinstance. Actually,theyareinternallyimplementedasintervalactionswithzeroduration.One exampleofaninstantactionisCallFunc,whichinvokesafunctionwhentheactionis executed: importcocos.actionsasac defupdate_score(): print('Updatingthescore…') sprite=cocos.sprite.Sprite('tank.png',position=(200,200)) sprite.do(ac.CallFunc(update_score)) Aswewillseelater,thisactionwillbehandywhenwewanttocallaspecificfunction duringasequenceofactions. Notethattheupdate_scoreargumentinthelastlineisnotfollowedby().Thisisasubtle butimportantdifference;itmeansthatwearecreatingCallFuncwithareferencetothe update_scorefunction,insteadofactuallyinvokingit. Thefollowingtablecontainsalltheactionsdefinedinthe cocos.actions.instant_actionsmodule: Instantaction Description Place Placesthetargetintheposition(x,y) CallFunc Callsafunction CallFuncS Callsafunctionwiththetargetasthefirstargument Hide HidesthetargetbysettingitsvisibilitytoFalse Show ShowsthetargetbysettingitsvisibilitytoTrue ToggleVisibility Togglesthevisibilityofthetarget Combiningactions Bynow,youknowhowtoapplyactionsseparately,butyouneedtocombinethemin ordertoachieveamorecomplexbehavior. TheActionclass,definedinthecocos.actions.base_actionsmodule,isthebaseclass thatInstantActionandIntervalActioninheritfrom.Itimplementsthe__add__special method,whichiscalledinternallywhenweusethe+operator. Thisoperatorcreatesasequenceofactionsthatareappliedseriallytothetarget: sprite=cocos.sprite.Sprite('tank.png',position=(200,200)) sprite.do(ac.MoveBy((80,0),3)+ac.Delay(1)+\ ac.CallFunc(sprite.kill)) Thissnippetmovesourtank80pixelstotherightin3seconds.Itstandsfor1second,and finally,itisremovedbycallingthekill()method. Apartfrom__add__,the__or__specialmethodisalsooverridden.Itisinvokedwhenthe |operatorisusedandrunstheactionsinparallel: sprite=cocos.sprite.Sprite('tank.png',position=(200,200)) sprite.do(ac.MoveTo((500,150),3)|ac.RotateBy(90,2)) Thismovesthespritetothe(500,150)positionin3seconds,andduringthefirst2 seconds,itrotates90degreesclockwise. The__add__and__or__specialmethodscanbecombinedtoproduceasequenceof actionsthatrunbothinparallelandsequentially: sprite=cocos.sprite.Sprite('tank.png',position=(200,200)) sprite.do((ac.MoveTo((500,150),3)|ac.RotateBy(90,2))+\ ac.CallFunc(sprite.kill)) Thisexampleperformscombinedmovementandrotationasthepreviousone,butwhen theparallelactionsaredone,itremovesthesprite. Tip Python’sspecialmethods Cocos2dimplementsactionsequencesbyemulatingnumericoperators,butthesecould havebeenimplementedwithnormalmethods.Inprograming,thiskindofshortcutoffered byalanguageiscalledsyntacticsugar,anditsuccinctlyexpressesafunctionalitythatcan alsobeimplementedinamoreverbosemanner. Customactions Wehaveafulllistofactionsincludedinthelibrary,butwhatifnoneoftheseactions performthevisualeffectthatwedesireandwewanttodefineanewaction? Wecancreateinstantorintervalactionsbyextendingthecorrespondingclass.Forour game,wewantanactionthatindicatesvisuallythatanenemytankhasbeenhitby applyingaredfiltertothetank,anditshouldgraduallyreturntotheoriginalcolor. Inthiscase,wedefineanIntervalActionsubclasscalledHit,keepinginmindthatthe followingstepswillbeperformedinternallybycocos2d: Theinit(*args,**kwargs)iscalled.Oneofthesekeywordargumentsshouldbe thedurationoftheaction,soyoucansetitasyourdurationattribute.Donotconfuse thiswiththe__init__specialmethod. Acopyoftheinstanceismade;usually,thisshouldnothaveanysideeffect. Thestart()iscalled.Fromhere,theself.targetattributecanbeused. Theupdate(t)iscalledseveraltimes,wheretisthetimeinthe(0,1)range. Then,update(1)iscalled. Thestop()iscalled. Itisnotnecessarytooverrideallofthesemethods,butonlythosethatarerequiredto implementyouraction.Forinstance,weonlyneedtostorethedurationandupdatethe colorofthespritedependingontheelapsedtime,t: classHit(ac.IntervalAction): definit(self,duration=0.5): self.duration=duration defupdate(self,t): self.target.color=(255,255*t,255*t) Whenupdate(0)isinvoked,thesprite’scolorwillbe(255,0,0),whichlooksasifared filterisbeingappliedtotheimage.Theredtonewilldecreaseastheelapsedtime,t,rises monotonically. Sinceweknowthatupdate(1)willbecalled,thefinalcolorofthetargetwillbe(255, 255,255),andthereisnoneedtoresettheinitialcolorofthesprite. Thefollowingsnippetshowshowthisnewactioncanbeused: sprite=cocos.sprite.Sprite('tank.png',position=(200,200)) sprite.do(ac.MoveBy((100,0),3)+Hit()+ac.MoveBy((50,0),2)) IntheChapter3_01.pyscript,youcanseethissnippetwithalloftheusualcoderequired torunthescene. Addingamainmenu TheSpaceInvadersversionthatwedevelopedinthepreviouschapterstartsanewgame assoonastheapplicationisloaded.Inmostgames,aninitialscreenisdisplayedandthe playercanchoosebetweendifferentoptionsapartfromstartinganewgame,suchas changingthedefaultcontrolsortakingalookatthehighscores. Thecocos.menucocos2dmoduleoffersaLayersubclassnamedMenu,whichserves exactlythispurpose.Byextendingit,youcanoverrideits__init__methodandsetthe styleofthetitle,themenuitems,andtheselectedmenuitem. TheseitemsarerepresentedasalistofMenuIteminstances.Oncethislistisinstantiated, youcancallthecreate_menumethod,whichbuildsthefinalmenuwiththeactionsthat areexecutedwhenamenuitemisselected. WhilethebasicMenuItemonlydisplaysastaticlabel,thereareseveralMenuItem subclassesfordistinctinputmodes: ToggleMenuItem:TogglesaBooleanoption MultipleMenuItem:Switchesbetweenmultiplevalues EntryMenuItem:Thisisthemenuitemforenteringatextinput ImageMenuItem:Showsaselectableimageinsteadofatextlabel ColorMenuItem:Thisisthemenuitemforselectingacolor AlloftheseclassesexceptImageMenuIteminvokeacallbackfunctionwhenitsvalue changestothisnewvalueasthefirstargument. Atypicalusagecouldbethefollowing: importcocos fromcocos.menuimport* importpyglet.app classMainMenu(Menu): def__init__(self): super(MainMenu,self).__init__('Samplemenu') self.font_title['font_name']='TimesNewRoman' self.font_title['font_size']=60 self.font_title['bold']=True self.font_item['font_name']='TimesNewRoman' self.font_item_selected['font_name']=\ 'TimesNewRoman' self.difficulty=['Easy','Normal','Hard'] m1=MenuItem('NewGame',self.start_game) m2=EntryMenuItem('Playername:',self.set_player_name, 'JohnDoe',max_length=10) m3=MultipleMenuItem('Difficulty:',self.set_difficulty, self.difficulty) m4=ToggleMenuItem('ShowFPS:',self.show_fps,False) m5=MenuItem('Quit',pyglet.app.exit) self.create_menu([m1,m2,m3,m4,m5], shake(),shake_back()) Whenself.create_menuiscalled,wecanpassacocos2dactionthatisappliedwhena menuitemisactivatedandwhenitisdeactivated.Thecocos.menumoduleincludesafew actions,andweimportedtheshake()andshake_back()actions,whichperformalittle shakemovementatthecurrentmenuitemandreturntotheoriginalstylewhentheoption isdeselected,respectively. Therestofthecodehasbeenomittedforbrevity;youcanfindthecompletescriptin Chapter3_02.pywiththeclassmethodsthatarecalledwhenamenuitemisactivated. Notethereferencetothepyglet.app.exitfunction,whichfinalizesthecocos2d application. Withjustafewlinesofcode,wehavebuiltamenuwheretheplayercaninputtheirname, setthegamedifficulty,andtoggletheFPSdisplay.Themenuitemscanbeselectedwith thearrowkeysorthemousepointer. Thisisthetypeofmainmenuwearelookingforforourtowerdefensegame.Sincethisis onlyonelayer,wecanlateraddabackgroundlayerthatimprovesthevisualappearanceof themenu. NowthatyouhaveunderstoodthemenuAPI,let’smoveontothedesignofthemain scene. Tilemaps Thetechniqueoftilemapsbecameasuccessfulapproachtostoringlargeamountsof informationaboutgameworldswithsmall,reusablepiecesofgraphics.In2Dgames,tile mapsarerepresentedbyatwo-dimensionalmatrixthatreferencestoatileobject.This objectcontainstherequireddataabouteachcelloftheterrain. Theinitialsheetusedbythetilemapcontainsthe“buildingblocks”ofourscenario,andit lookslikewhatisshowninthefollowingscreenshot: Startingfromthissimpleimage,wecanbuildagridmapinwhicheachcellisoneofthe squaresthesheetisdividedinto. TiledMapEditor WewilluseTiledMapEditor,ausefultoolformanipulatingtiledmaps.Itcanbe downloadedforfreefromhttp://www.mapeditor.org/,anditcanberunonmostoperating systems,includingWindowsandMacOS. Thissoftwareisalsowell-suitedforleveldesign,sinceyoucaneditandvisualizethe resultingworldinasimpleway. Oncewehaveinstalledandlaunchedtheprogram,wecanloadtheimagethatwewilluse forourtilemap.Forthisgame,wehaveusedaPNGimagethatisalreadybundledwith theMapEditorinstallation. Inthemenubar,gotoFile|New…tocreateanewmap.Whenanewmapiscreated,you willbepromptedforsomebasicinformation,suchasthemaporientation,mapsize,size ofthepattern,andspacingbetweenthecells.Inourgame,wewilluseanorthogonalmap of640x480pixelsand32x32pixelsforeachcell,asshowninthefollowingscreenshot: NowgotoMap|NewTileset…,enterthenamemap0,andloadthe tmw_desert_spacing.pngimagefromtheexamplesfolder,asshowninthisscreenshot: YoucanusethetilesshownintheTilesetviewtodrawamaplikethisone: Oncethemapisdrawn,wecansaveitinseveralfileformats,suchasCSV,JSON,or TXT.WewillchoosetheTMXformat,whichisanXMLfilethatcanbeloadedby cocos2d.GotoFile|Saveas…tostorethetiledmapwiththisformat. Loadingtiles Thankstothecocos.tilesmodule,wecanloadourtilemapsaslayersandmanipulate themlikeanyotherCocosNodeinstance.IfourTMXfileandthecorrespondingPNG imagearestoredintheassetsfolder,andthenameofthemapwewanttoloadis'map0', thiswouldbethecodenecessarytoloadit: importcocos tmx_file=cocos.tiles.load('assets/tower_defense.tmx') my_map=tmx_file('map0') my_map.set_view(0,0,my_map.px_width,my_map.px_height) scene=cocos.scene.Scene(my_map) Thescenariodefinition Oncewehaveloadedthetilemap,weneedtolinktheresultingimagewiththegame information.Ourscenarioclassshouldcontainthefollowing: Thepositionswheretheturretscanbeplaced Thepositionofthebunker Theinitialpositionforenemyspawning Thepaththattheenemiesmustfollowtoreachthebunker Inthefollowingscreenshot,wecanseethisdataoverlaidontopofourTMXmap: Therectanglesrepresenttheslotsinwhichtheplayercanplacetheturrets.Thescenario storesonlythecentersofthesesquares,becausethegamelayerwilltranslatethese positionsintoclickablesquares. Thelinesovertheroadrepresentthepaththattheenemytanksmustfollow.This movementwillbeimplementedbychainingtheMoveByandRotateByactions.Wewill definetwoconstantsforrotationtowardtheleftortheright,andanauxiliaryfunctionthat returnsaMoveByactionwhosedurationmakestheenemiesmoveuniformly: importcocos.actionsasac RIGHT=ac.RotateBy(90,1) LEFT=ac.RotateBy(-90,1) defmove(x,y): dur=abs(x+y)/100.0 returnac.MoveBy((x,y),duration=dur) Withtheseconstantsandtheauxiliaryfunction,wecaneasilycreatealistofactionsthat willbechainedtorepresentthecompletelistofstepsrequiredtofollowthegreenpath showninthepreviousscreenshot.Forinstance,thiscouldbeahypotheticalusageofthese utilitiestocomposeachainofactions: steps=[move(610,0),LEFT,move(0,160),LEFT,move(-415,0), RIGHT,move(0,160),RIGHT,move(420,0)] forstepinsteps: actions+=step sprite.do(actions) Thissolutionnotonlyavoidstheneedtowriteallthestepsonebyone,butalsomakesour codemoreexpressiveandreadable.Nowlet’sencapsulatethislogicinaclassthatwill holdthisdata. Tip Domain-specificlanguages Thisuseofabstractionstorepresenthigh-levelconceptsiswidelyfoundinprogramming, andgamedevelopmentisnotanexception.Whenthissyntaxreachesawholedomainof specializedfeatures,theresultinglanguageiscalledaDomain-specificLanguage(DSL). WhilethissmalldomainonlycoversspritemovementsandcannotbeconsideredaDSL byitself,advancedgameenginesincludespecificscriptinglanguagesthatarefocusedon gamedevelopment. Thescenarioclass Let’sstartthedefinitionofthescenariomodulebycreatinganemptyfilecalled scenario.pyandopeningitwithourfavoritetexteditor.Thismodulewillcontainthe definitionoftheScenarioclass,whichgroupsthepreviouslydiscussedinformation,such asthebunker’sposition,thechainofactionsthattheenemieswillfollow,andsoon: classScenario(object): def__init__(self,tmx_map,turrets,bunker,enemy_start): self.tmx_map=tmx_map self.turret_slots=turrets self.bunker_position=bunker self.enemy_start=enemy_start self._actions=None defget_background(self): tmx_map=cocos.tiles.load('assets/tower_defense.tmx') bg=tmx_map[self.tmx_map] bg.set_view(0,0,bg.px_width,bg.px_height) returnbg Toretrievethesequenceofactions,[email protected] accesstoanattributebyinvokingfunctionsthatwillactasgettersandsetters: @property defactions(self): returnself._actions @actions.setter defactions(self,actions): self._actions=ac.RotateBy(90,0.5) forstepinactions: self._actions+=step Thisdecoratorwrapstheaccesstotheinternal_actionsmember.Givenaninstanceof thisclassnamedscenario,theretrievalofthescenario.actionsmemberwouldtrigger thegetterfunction,whileanassignmenttoscenario.actionswouldtriggerthesetter function. Nowwecandefineafunctionthatinstantiatesascenarioandsetitsmembersbasedonthe dispositionofour'map0'.Ifwehaddesignedmorelevels,wecouldhaveaddedmore functionsthatcreatethesenewscenarios: defget_scenario(): turret_slots=[(192,352),(320,352),(448,352), (192,192),(320,192),(448,192), (96,32),(224,32),(352,32),(480,32)] bunker_position=(528,430) enemy_start=(-80,110) sc=Scenario('map0',turret_slots, bunker_position,enemy_start) sc.actions=[move(610,0),LEFT,move(0,160), LEFT,move(-415,0),RIGHT, move(0,160),RIGHT,move(420,0)] returnsc Tousethismodulefromanotherone,wewillimportitwiththisstatement:from scenarioimportget_scenario.Thenextscriptthatwewillwriteisresponsiblefor definingourgame’smainmenu. Transitionsbetweenscenes Inaprevioussection,youlearnedhowtocreateamenuwithcocos2d’sbuilt-inclasses. Themenuofourtowerdefensegamewillbeverysimilar,sinceitfollowsthesamesteps. Itwillbeimplementedinaseparatemodule,namedmainmenu: classMainMenu(cocos.menu.Menu): def__init__(self): super(MainMenu,self).__init__('TowerDefense') self.font_title['font_name']='Oswald' self.font_item['font_name']='Oswald' self.font_item_selected['font_name']='Oswald' self.menu_anchor_y='center' self.menu_anchor_x='center' items=list() items.append(MenuItem('NewGame',self.on_new_game)) items.append(ToggleMenuItem('ShowFPS:',self.show_fps, director.show_FPS)) items.append(MenuItem('Quit',pyglet.app.exit)) self.create_menu(items,ac.ScaleTo(1.25,duration=0.25), ac.ScaleTo(1.0,duration=0.25)) Thismenudisplaysthreemenuitems: Anoptionforstartinganewgame AtoggleitemforshowingtheFPSlabel Aquitoption Tostartanewgame,wewillneedtheinstanceofthemainscene.Itwillbereturnedby callingthenew_gamefunctionofmainscene.py,whichhasnotbeendevelopedyet. Besides,wewilladdatransitionwiththeFadeTRTransitionclassfromthe cocos.scenes.transitionsmodule. Atransitionisascenethatperformsavisualeffectbeforesettingthecontrolofanew scene.Itreceivesthissceneasitsfirstargumentandsomeoptions,suchasthetransition durationinseconds: fromcocos.scenes.transitionsimportFadeTRTransition frommainsceneimportnew_game game_scene=new_game()#Instanceofcocos.scene.Scene transition=FadeTRTransition(game_scene,duration=2) Now,toreplacethecurrentscenewiththenewonedecoratedwithatransition,wecall director.push.Inthisway,theon_start_menumethodofourMainMenuclassis implemented: defon_new_game(self): director.push(FadeTRTransition(new_game(),duration=2)) Thevisualeffectproducedbythistransitionisthatthecurrentscene’stilesfadefromthe left-bottomcornertothetop-rightcorner,asshownhere: Youcanfindthecompletecodeofthismoduleinthemainmenu.pyscript. Gameovercutscene Tocreateacutscenewhenthegameisover,wewillneedasimplelayerandatextlabel. SinceasceneisaCocosNode,wecanapplyactionstoit.Toholdthisscreenforamoment, itwillperformaDelayactionthatlasts3seconds,andthenitwilltriggera FadeTransitiontothemainmenu. Wewillwrapthesestepsinaseparatefunction,whichwillbepartofanothernewmodule, namedgamelayer.py: defgame_over(): w,h=director.get_window_size() layer=cocos.layer.Layer() text=cocos.text.Label('GameOver',position=(w*0.5,h*0.5), font_name='Oswald',font_size=72, anchor_x='center',anchor_y='center') layer.add(text) scene=cocos.scene.Scene(layer) new_scene=FadeTransition(mainmenu.new_menu()) func=lambda:director.replace(new_scene) scene.do(ac.Delay(3)+ac.CallFunc(func)) returnscene Thisfunctionwillbecalledfromthegamelayerwhenthebunker’shealthpointsdecrease tozero. Thetowerdefenseactors Wewilldefineseveralclassesintheactors.pymoduletorepresentthegameobjects. Aswithourpreviousgame,wewillincludeabaseclass,fromwhichtherestoftheactor classeswillinherit: importcocos.sprite importcocos.euclidaseu importcocos.collision_modelascm classActor(cocos.sprite.Sprite): def__init__(self,img,x,y): super(Actor,self).__init__(img,position=(x,y)) self._cshape=cm.CircleShape(self.position, self.width*0.5) @property defcshape(self): self._cshape.center=eu.Vector2(self.x,self.y) returnself._cshape IntheActorimplementationinthepreviouschapter,whenthespritewasdisplacedby callingthemove()method,bothcshapeandpositionwereupdatedatthesametime. However,actionssuchasMoveByonlymodifythespriteposition,andtheCShapecenteris notupdated. Tosolvethisissue,wewraptheaccesstotheCShapememberthroughthecshape property.Withthisconstruct,whentheactor.cshapevalueisread,theinternal_cshape isupdatedbysettingitscenterwiththecurrentspriteposition. Thissolutionispossiblebecauseanobjectonlyneedsacshapememberinordertobea validentityforaCollisionManager,andapropertyisexposedinthesamewayasany otherattributethatisdirectlyaccessed. Nowthatouractorbaseclasshasbeendefined,wecanstartimplementingtheclassesfor theturretsandtheenemytanks. Turretsandslots Turretsaregameobjectsthatactautonomously;thatis,thereisnoneedtobindinput eventswiththeiractions.Thecollisionshapeisnotthespritesizebutthefiringrange. Therefore,acollisionbetweenaturretandatankmeansthattheenemyisinsidethisrange andcanbeconsideredavalidtarget: classTurret(Actor): def__init__(self,x,y): super(Turret,self).__init__('turret.png',x,y) self.add(cocos.sprite.Sprite('range.png',opacity=50, scale=5)) self.cshape.r=125.0 self.target=None self.period=2.0 self.reload=0.0 self.schedule(self._shoot) Theshootinglogicisimplementedbyschedulingafunctionthatincrementsthereload counter.Whenthesumofelapsedsecondsreachestheperiodvalue,thecounteris decreasedandtheturretcreatesashootspritewhoseaimisatthecurrenttarget: def_shoot(self,dt): ifself.reload<self.period: self.reload+=dt elifself.targetisnotNone: self.reload-=self.period offset=eu.Vector2(self.target.x-self.x, self.target.y-self.y) pos=self.cshape.center+offset.normalized()*20 self.parent.add(Shoot(pos,offset,self.target)) Apartfromsettingthetarget,acollisionwiththecircularshapethatrepresentsthefiring rangealsorotatestheturret,givingtheimpressionthatitisaimingatthetarget. Tocalculatetheanglebywhichthespritemustrotate,wecalculatethevectorthatruns fromtheturrettothetarget.Withtheatan2functionfromthemathmodule,wecalculate theanglebetweentheπand-πradiansformedbythepositivexaxisandthisvector. Finally,wechangethesignoftheangleandconvertitfromradianstodegrees: defcollide(self,other): self.target=other ifself.targetisnotNone: x,y=other.x-self.x,other.y-self.y angle=-math.atan2(y,x) self.rotation=math.degrees(angle) Ashootisnotanactor,sinceitisnotrequiredtobeabletocollide.Throughasequenceof actions,itwillmovefromtheturrettothetank’spositionandhitthetargetinstance: classShoot(cocos.sprite.Sprite): def__init__(self,pos,offset,target): super(Shoot,self).__init__('shoot.png',position=pos) self.do(ac.MoveBy(offset,0.1)+ ac.CallFunc(self.kill)+ ac.CallFunc(target.hit)) ApartfromTurretandShoot,wewillneedaclasstorepresenttheslotsinwhichthe turretscanbeplaced.SincewewilltakeadvantageoftheCollisionManagerfunctionality todetectwhetheraCShapehasbeenclickedon,thisclassonlyneedsacshapemember: classTurretSlot(object): def__init__(self,pos,side): self.cshape=cm.AARectShape(eu.Vector2(*pos),side*0.5,side*0.5) InstancesofthisclasswillbeaddedtoadifferentCollisionManagersothattheydonot conflictwiththerestofthegameobjects. Enemies Anenemytankisanactorthatfollowsthescenariopathuntilitreachesthebunkeratthe endoftheroad.Itisdestroyediftheturretshotsreduceitshealthpointstozero. Theplayerincrementstheirscoreeachtimeatankisdestroyed,soapartfromhealth,we willneedascoreattributetoindicatehowmanypointstheplayerearns: classEnemy(Actor): def__init__(self,x,y,actions): super(Enemy,self).__init__('tank.png',x,y) self.health=100 self.score=20 self.destroyed=False self.do(actions) Sincewemustdifferentiatebetweenwhetherthetankhasexplodedbecauseithasbeen defeatedbytheturretsorbecauseithasreachedtheendpoint,wewilluseadestroyed flag.Thisflagwillbesettotrueonlyifthetankisdestroyedbyaturret. WealsochecktheCocosNodeflagcalledis_running,sinceitissettofalsewhenthe nodeisremoved.Thus,wecanpreventthetankfrombeingremovedwhenithasalready beenkilled: defhit(self): self.health-=25 self.do(Hit()) ifself.health<=0andself.is_running: self.destroyed=True self.explode() defexplode(self): self.parent.add(Explosion(self.position)) self.kill() LiketheanimationsofourSpaceInvadersgame,anexplosionwillbesimulatedwitha fastsequenceofspritesthatlastsfor0.07seconds.Toavoidhavingtoomanysprite instancesinourlayer,thekill()methodiscalled1secondaftertheobjectinstantiation: raw=pyglet.image.load('explosion.png') seq=pyglet.image.ImageGrid(raw,1,8) explosion_img=Animation.from_image_sequence(seq,0.07,False) classExplosion(cocos.sprite.Sprite): def__init__(self,pos): super(Explosion,self).__init__(explosion_img,pos) self.do(ac.Delay(1)+ac.CallFunc(self.kill)) Bunker Youmightrememberthedescriptionofthegameplay.Weneedtokeeptheenemiesaway fromaspecificarea.Inourversion,itisrepresentedbyabunkerthatisplacedattheend oftheroad. Thebunkerinstanceonlyneedstoprocesstheenemycollisionsanddecreaseitshealth points.Theinitialnumberofhealthpointsis100,andeachcollisionsubtracts10points fromthistotal: classBunker(Actor): def__init__(self,x,y): super(Bunker,self).__init__('bunker.png',x,y) self.hp=100 defcollide(self,other): ifisinstance(other,Enemy): self.hp-=10 other.explode() ifself.hp<=0andself.is_running: self.kill() AswedidwiththeTankclass,wecheckwhethertheCocosNodeflagis_runningissetto truetoavoidcallingkill()whenthebunkerhasalreadybeenremoved. Gamescene ThegamelayercontainsseveralattributesforholdingthereferencetotheHUDlayer,the scenario,andthegameinformation,suchasthescoreorthenumberofpointsthatcanbe spenttobuildnewturrets. Theseclassesareaddedtothegamelayermodule,whichhascontainedonlythegame overtransitionsofar: classGameLayer(cocos.layer.Layer): def__init__(self,hud,scenario): super(GameLayer,self).__init__() self.hud=hud self.scenario=scenario self.score=self._score=0 self.points=self._points=40 self.turrets=[] w,h=director.get_window_size() cell_size=32 self.coll_man=cm.CollisionManagerGrid(0,w,0,h, cell_size, cell_size) self.coll_man_slots=cm.CollisionManagerGrid(0,w,0,h, cell_size, cell_size) forslotinscenario.turret_slots: self.coll_man_slots.add(actors.TurretSlot(slot, cell_size)) self.bunker=actors.Bunker(*scenario.bunker_position) self.add(self.bunker) self.schedule(self.game_loop) Anotherdifferencefromourpreviouscocos2dgameistheusageoftwocollision managers:coll_manforthegameactorsandcoll_man_slotsfortheturretslots.While thefirstoneisupdatedduringeachiterationofthegameloop,thesecondonecontains staticshapesthatdonotconflictwiththeactors’collisions.Asaresult,weavoid unnecessaryadditionsandremovalsfromtheturretslotshapes;thus,improvingthe collisioncheckingperformance. Bothpointsandscorearepropertiesthat,apartfromaccessingthe_pointsand_score internalattributes,updatetheHUDwiththenewnumericvalue.Here,wewillshowthe pointsproperty,butscorehasasimilarimplementation: @property defpoints(self): returnself._points @points.setter defpoints(self,val): self._points=val self.hud.update_points(val) Thegameloopisquitesimplesincethelogicofthegameismainlyimplementedwith actions: defgame_loop(self,_): self.coll_man.clear() forobjinself.get_children(): ifisinstance(obj,actors.Enemy): self.coll_man.add(obj) forturretinself.turrets: obj=next(self.coll_man.iter_colliding(turret),None) turret.collide(obj) forobjinself.coll_man.iter_colliding(self.bunker): self.bunker.collide(obj) ifrandom.random()<0.005: self.create_enemy() Thesearethestatementsperformedforeachframe: Itupdatesthecollisionmanagerwiththepositionsoftheenemytanks. Foreachturret,wecheckwhetherthereisanyenemywithinthefiringrange.Ifso, wecallthecollide()methodoftheTurretclass. Wealsocheckwhetheracollisionwiththebunkerhasoccurred.Sincetheonly entitiesmanagedbyself.coll_manaretheenemytanks,wedonothavetoworry aboutverifyingthattheotherobjectisatankandnotaturret. Enemiesarerandomlyspawnedwithagivenprobability.Weleftastaticvalue,butit couldhavebeencalculateddependingonthenumberofenemiesdefeatedsothatthe gamebecomesincreasinglymoredifficult. Thecreate_enemy()methodplacesanewtankattheinitialposition.Topreventthem fromalwaysspawningatthesamecoordinates,wewillapplyarandomoffsetof+/-10 pixelsinboththexandycomponents: defcreate_enemy(self): enemy_start=self.scenario.enemy_start x=enemy_start[0]+random.uniform(-10,10) y=enemy_start[1]+random.uniform(-10,10) self.add(actors.Enemy(x,y,self.scenario.actions)) Thelayerwillprocessuserinputasusual,andwewillregisteronlyonemethodtoprocess mouseevents.Withtheobjs_touching_point()method,wewillknowwhetheranyslot hasbeenclickedon.Iftheplayerhasenoughpoints,anewturretinstanceisplacedatthe centeroftheslot’sposition: is_event_handler=True defon_mouse_press(self,x,y,buttons,mod): slots=self.coll_man_slots.objs_touching_point(x,y) iflen(slots)andself.points>=20: self.points-=20 slot=next(iter(slots)) turret=actors.Turret(*slot.cshape.center) self.turrets.append(turret) self.add(turret) Finally,weoverridetheremove()methodtodetectwhichtypeofobjecthasbeen removed: defremove(self,obj): ifobjisself.bunker: director.replace(SplitColsTransition(game_over())) elifisinstance(obj,actors.Enemy)andobj.destroyed: self.score+=obj.score self.points+=5 super(GameLayer,self).remove(obj) Ifthenodeisthebunker,itmeansthattheplayerhaslostthegame,andthedirector replacesthecurrentscenewiththeGameOvercutscene.Ifthenodeisanenemytank, thescoreandthepointsareupdatedbeforeactuallyremovingtheobject. TheHUDclass Thislayerisresponsiblefordisplayingthegameinformation,afunctionalitysimilartothe HUDwebuiltinourpreviousgame: classHUD(cocos.layer.Layer): def__init__(self): super(HUD,self).__init__() w,h=director.get_window_size() self.score_text=self._create_text(60,h-40) self.score_points=self._create_text(w-60,h-40) def_create_text(self,x,y): text=cocos.text.Label(font_size=18,font_name='Oswald', anchor_x='center',anchor_y='center') text.position=(x,y) self.add(text) returntext defupdate_score(self,score): self.score_text.element.text='Score:%s'%score defupdate_points(self,points): self.score_points.element.text='Points:%s'%points Assemblingthescene Toconcludeourmainscene.pymodule,wewilldefinethenew_game()function.This functionisusedbythemainmenuwhenanewgameisstarted.Itreturnsthescenewith thetilemap,theHUDlayer,andthegamelayerinitializedanddisplayedinthecorrect order: defnew_game(): scenario=get_scenario() background=scenario.get_background() hud=HUD() game_layer=GameLayer(hud,scenario) returncocos.scene.Scene(background,game_layer,hud) Thegamewillbestartedwiththeconditionalblockthatwesawinourpreviousgames. Apartfrominitializingthedirectorandrunningthescene,wewillloadtheOswaldfont andaddtheimagestotheresourcepathwiththePygletAPI: fromcocos.directorimportdirector importpyglet.font importpyglet.resource frommainmenuimportnew_menu if__name__=='__main__': pyglet.resource.path.append('assets') pyglet.resource.reindex() pyglet.font.add_file('assets/Oswald-Regular.ttf') director.init(caption='TowerDefense') director.run(new_menu()) Thisissavedinthetowerdefense.pyscript.Thefollowingscreenshotshowsthefinal projectlayout: Summary Withthisproject,wesawhowtoincludemenus,transitions,andactionsinourcocos2d applications.Ourcodeissplitintomultiplemodules,enhancingabetterorganizationof ourgames. Thisgamecanbethebaseforamorecomplextowerdefensegame.Youcancustomize thenumberofpointsrequiredtocreateanewturret,orchangetheprobabilityofspawning enemies.Asanexercise,trytocreateanewTMXmapwithMapEditoranddefinea customscenariobasedonthisbackground.Yourimaginationisthelimit! Chapter4.SteeringBehaviors Ourpreviouscocos2dgamewasbasedonactions,somovementsandrotationswere predefinedandthecharacterswerenotinfluencedbythecurrentstateofthegame. However,arecurringproblemingamedevelopmentishowtorecreatelife-like animations,suchaspursuingatargetoravoidingmovingobstacles. Now,youwilllearnhowtoapplysteeringbehaviors,atechniqueusedtocreate seeminglyintelligentmovementsforautonomouscharacters.Theimplementationofthese strategiesachievestheabilitytonavigatethroughthegameworldwithimprovised patterns. Finally,wewillputthesestrategiesinpracticewithparticlesystems,aCocos2dmodule thatwehavenotworkedwithsofar.Sincewewillusesimpleshapes,theseparticle systemswillrepresentourcharacters,withtheadvantageofusnotrequiringexternal assetsforourapplications. Inthischapter,wewillcoverthesetopics: Basicconceptsofsteeringbehaviors Behaviorsforindividualsandgroups Mixingthesestrategies Howtorenderparticlesystemswithcocos2d NumPyinstallation Tousetheparticlesystemsupportofcocos2d,itisnecessarytoinstallNumPy,aPython packageusedtooperatewithlargearraysandmatricesinanefficientway.Sinceit containsseveralCmodules,itmightbedifficulttoinstallitonWindowssystemsbecause youmightnothavetheappropriatecompiler. YoucandownloadtheofficialbinariesforWindowsandMacOSXfromtheNumPysite athttp://sourceforge.net/projects/numpy/files/NumPy/1.9.2/. AnotheroptionistodownloadtheunofficialcompiledbinariesfromChristophGohlke’s websiteathttp://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy.Here,thepackagesare uploadedas.whlfiles.Thisistheextensionofthewheelformatandcanbeinstalledwith pip: $pipinstallnumpy‑1.9.2+mkl‑cp34‑none‑win32.whl Inbothcases,remembertoinstallthebinariesforPython3.4,sincetheversionsfor Python2.7and3.3areavailablefordownloadaswell. Youcancheckwhethertheinstallationwassuccessfulbyrunningthiscommand: $python-c"importnumpy;print(numpy.version.version)" 1.9.2 OnceNumPyisinstalled,youhavealltherequirementsforsupportingparticlesystemsin cocos2d. TheParticleSystemclass Thebaseclassforcocos2dparticlesystemsisParticleSystem,asdefinedinthe cocos.particlemodule.Thefollowingtablelistssomeofthemostrelevantclass members.Notethattheonesendingin_varindicatethatrandomvariancecanbeapplied tothebasevalue. Classmember Description Active Indicateswhethertheparticlesystemisspawningnewparticlesornot. Duration Thedurationofthesysteminseconds.Thisvalueis-1forinfinite duration. Gravity Gravityoftheparticles. angle,angle_var Theangulardirectionoftheparticlesmeasuredindegrees. speed,speed_var Thespeedoftheparticles. tangential_accel, tangential_accel_val Tangentialacceleration. radial_accel,radial_accel_var Radialacceleration. size,size_var Thesizeoftheparticles. life,life_var Thetimeinsecondsforwhicheachparticlewilllive. start_color,start_color_var Thestartcoloroftheparticles. end_color,end_color_val Theendcoloroftheparticles. total_particles Themaximumnumberofparticles. Youcancreateyourownparticlesystemsbyextendingthisclassandredefiningthevalues oftheseclassmembers. Cocos2dincludesanothermodule,cocos.particles_systems,withsomepredefined ParticleSystemsubclasses,eachproducingadifferentvisualeffect.Thenamesofthese classesareFireworks,Spiral,Meteor,Sun,Fire,Galaxy,Flower,Explosion,andSmoke. Aquickdemonstration ThiscodeisabasicexamplethatshowshowtoaddastaticParticleSystemtoourlayer withoutprocessinganyuserinputoractions: importcocos importcocos.particle_systemsasps classMainLayer(cocos.layer.Layer): def__init__(self): super(MainLayer,self).__init__() particles=ps.Spiral() particles.position=(320,240) self.add(particles) if__name__=='__main__': cocos.director.director.init(caption='Particlesexample') scene=cocos.scene.Scene(MainLayer()) cocos.director.director.run(scene) ParticleSysteminheritsfromCocosNode.Therefore,theinstancehastheusualmembers’ position,rotation,andscale,aswellasthedo()andkill()methods. Runningthisscriptshowsthefollowingpredefinedparticlesystem: Youcanreplaceps.Spiral()withotherbuilt-inparticlesystems,suchasps.Galaxy()or ps.Fireworks().ManyIntegratedDevelopmentEnvironments(IDE),suchasPyCharm orPyDevm,offercodecompletionandwilllistallthenamesthataredefinedinamodule aftertypingitsname. Implementingsteeringbehaviors ThestrategiesthatwewillcoverherehavebeentakenfromCraigReynolds’spaper SteeringBehaviorsforAutonomousCharacters,writtenin1999.Ithasbecomeawellknownreferenceforimplementingautonomousmotioninaneasymannerfornonplayablecharacters. Youcancheckouttheonlineversionathttp://www.red3d.com/cwr/steer/gdc99/.Inthis section,youwilllearnhowtoimplementthefollowingkindsofbehavior: Seekandflee Arrival Pursuitandevade Wander Obstacleavoidance Seekandflee Theseekbehaviormovesthecharactertowardsaspecificpositioninthespace.This behaviorisbasedonthecombinationoftwoforces:thecharacter’svelocityandthe steeringforce.Thisforceiscalculatedasthedifferencebetweenthedesiredvelocity(the directionfromthecharactertothetarget)andthecharacter’scurrentvelocity. Notethatthisisnotaforceinthestrictphysicsdefinition;itisjustanothervelocityvector. Youcanthinkofitasacorrectionofthecharacter’svelocity,andtheresultingpathwillbe asmoothcurvethatadjuststhevelocityuntilitisalignedtowardsthetarget. Fleeistheinverseofseek,anditmakesthecharactermoveawayfromthetarget.Itmeans thatthesteeringforcepushesthecharacterawayfromthetarget. Youcanseebothseekandfleerepresentedgraphicallyinthefollowingdiagram.The curvedpathsshowthefinaldirectiononcetheforcesarecombined,therightonetowards thetarget(seek),andtheleftoneawayfromthetarget(flee). Forourimplementation,weneedaclassthatrepresentstheautonomouscharacterwiththe followingmembers: velocity:Atwo-dimensionalvectorrepresentingtheactor’svelocity speed:Theactor’sspeed,measuredinthenumberofpixelsperframe max_force:Themaximummagnitudeofthesteeringforce max_velocity:Themaximummagnitudeofthevelocityvector target:Thepositionthattheactortriestoreach Besides,toputourparticlesystemsintopractice,wewillextendCocosNodeinsteadof SpriteandrepresentouractorwiththeSunparticlesystem: importcocos importcocos.euclidaseu importcocos.particle_systemsasps classActor(cocos.cocosnode.CocosNode): def__init__(self,x,y): super(Actor,self).__init__() self.position=(x,y) self.velocity=eu.Vector2(0,0) self.speed=2 self.max_force=5 self.max_velocity=200 self.target=None self.add(ps.Sun()) self.schedule(self.update) Theupdate()methodwillcomputethenewpositionofthecharacterforeachframe, basedonthecurrentvelocityandthetargetthatitisseeking: defupdate(self,dt): ifself.targetisNone: return distance=self.target-eu.Vector2(self.x,self.y) steering=distance*self.speed-self.velocity steering=truncate(steering,self.max_force) self.velocity=truncate(self.velocity+steering, self.max_velocity) self.position+=self.velocity*dt Stepbystep,weperformthefollowingoperations: Calculatethedistancetothetarget. Wescalethisdistanceagainstthespeedandsubtractthecurrentvelocity.Thisgives usthesteeringforce. Truncatethisforcewiththemaximumforce. Sumthesteeringforcewiththecurrentvelocityandlimitittothemaximumvelocity thatcanbereached. Updatethepositionwiththevelocityperframe. Thisfigureshowshowthesevectorsareaddedtoachievetheresultingpathtothetarget: Thetruncate()functionlimitsthemagnitudeofavector,preventingthevelocityfrom reachingagreatermodulethanallowed: deftruncate(vector,m): magnitude=abs(vector) ifmagnitude>m: vector*=m/magnitude returnvector Ifthevectormoduleisgreaterthanthemaximumvalue,thevectorisnormalizedand scaledbythisvalue. Thecharacter’stargetwillbethemousepointer,andthetargetcoordinatesmustbe updatedwhenthemouseismoved.Todoso,wewilldefineacustomlayerthathandles themousemotioneventsandsetstheactor’starget: classMainLayer(cocos.layer.Layer): is_event_handler=True def__init__(self): super(MainLayer,self).__init__() self.actor=Actor(320,240) self.add(self.actor) defon_mouse_motion(self,x,y,dx,dy): self.actor.target=eu.Vector2(x,y) if__name__=='__main__': cocos.director.director.init(caption='SteeringBehaviors') scene=cocos.scene.Scene(MainLayer()) cocos.director.director.run(scene) Whenyourunthiscode,whichispresentintheseek.pyscript,thecharacterwillseekthe mousepointerwhenyoumoveitoverthewindow. Forthefleebehavior,simplychangethesignofthevelocitywhenthepositionisupdated: defupdate(self,dt): ifself.targetisNone: return #... self.position+=self.velocity*dt*-1 Thiswillmakethecharacterfleefromthemousepointer,andeventuallyleavethewindow ifthemouseisclosertothecenterthanthecharacter.Intheseek_and_flee.pyscript,you canfindacombinationoftheseimplementations,whereitispossibletoswitchthemwith themouseclick. Arrival Youmaynoticethatwiththeseekbehavior,ifthecharacter’svelocityistoohigh,itwill passthroughthetargetandcomebacksmoothly. Thearrivalbehaviorlowersthevelocityoncethecharacterhasreachedaminimum distanceclosetothetarget,decreasingitgradually.Thisdistancegivesustheradiusofthe slowingarea,acirclecenteredatthetargetpositionthatcausesthecharacter’svelocityto decrease. Theimplementationofthisisquitesimilartothatoftheseekbehavior.Weneedtoaddthe slow_radiusmembertoindicatetheradiusoftheslowingarea: classActor(cocos.cocosnode.CocosNode): def__init__(self,x,y): super(Actor,self).__init__() self.position=(x,y) self.slow_radius=200 self.velocity=eu.Vector2(0,0) #... Withthisattribute,wecanmodifyourupdate()methodsothatthesteeringforceis decreasedlinearlybytherampfactor.Whenthedistanceisgreaterthantheslowradius, therampfactorisataminimumof1.0,sothesteeringforceremainsunmodified: defupdate(self,dt): ifself.targetisNone: return distance=self.target-eu.Vector2(self.x,self.y) ramp=min(abs(distance)/self.slow_radius,1.0) steering=distance*self.speed*ramp-self.velocity steering=truncate(steering,self.max_force) self.velocity=truncate(self.velocity+steering, self.max_velocity) self.position+=self.velocity*dt Notethatwhenthedistanceapproaches0,therampfactorequals0andthesteeringforce becomes-self.velocity. Checkoutthearrival.pyfilewiththecompleteimplementationofthebehavior. Pursuitandevade Thepursuitbehaviorissimilartoseek,withthedifferencebeingthatthecharacterwill movetowardsthepositionatwhichthetargetwillbeinthefuturebasedonthetarget’s currentvelocity. Thefuturepositionisthesumofthetargetpositionplusthevelocityvectormultipliedby aunitoftime.Itgivesthecoordinateswherethetargetwillbeplacedwithin1secondifit doesnotmodifyitsvelocity: defupdate(self,dt): ifself.targetisNone: return pos=self.target.position future_pos=pos+self.target.velocity*1 distance=future_pos-eu.Vector2(self.x,self.y) steering=distance*self.speed-self.velocity steering=truncate(steering,self.max_force) self.velocity=truncate(self.velocity+steering, self.max_velocity) self.position+=self.velocity*dt Ifyouwanttosetthefuturepositionasthecoordinateswherethetargetwillbewithin2 seconds,multiplythetarget’svelocityby2. Inthisexample,wewillreplaceourmousepointertargetwithamovingnode.Itwillhave alinearvelocity,andouractorcanusethisvelocitytocalculatethefutureposition: classMainLayer(cocos.layer.Layer): def__init__(self): super(MainLayer,self).__init__() self.target=ps.Sun() self.target.position=(40,40) self.target.start_color=ps.Color(0.2,0.7,0.7,1.0) self.target.velocity=eu.Vector2(50,0) self.add(self.target) self.actor=Actor(320,240) self.actor.target=self.target self.add(self.actor) self.schedule(self.update) defupdate(self,dt): self.target.position+=self.target.velocity*dt Ontheotherhand,fortheevadebehavior,weneedtochangeonlythelaststatementofthe update()methodtoself.position+=self.velocity*dt*-1,exactlyaswedid forthefleebehavior. Thecodeofthisbehaviorisincludedinthepursuit.pyscript. Wander Thewandersteeringsimulatesarandomwalkwithoutanyspecifictarget.Itproducesa casualmovementwithoutsharpturnsorpredictablepaths. Itcanbeimplementedwithaseekbehaviorwithtargetscalculatedrandomly.However,a moreorganicsolutionistogeneratearandomsteeringforcetowardsapointona circumferenceplacedaheadofthecharacter. Thisforceiscalculatedperframe,givingsmallrandomdisplacementsthatproducethe visualeffectofthecharacterwanderingaround.Thisbehaviorcanbeparameterizedwith thefollowingvalues: wander_angle:Thecurrentangleofdisplacement,towhichthesmallvariationswill beadded circle_distance:Thedistanceofthecharacter’spositionfromthewandercircle’s center circle_radius:Theradiusofthewandercircle angle_change:Thefactorbywhichtherandomvaluewillbemultipliedtoproducea changeinthewanderangle Nowthatwehaveseenhowtheseforcesarecalculated,wecanimplementourwander behavior. Firstofall,wewilladdtheseattributestoourActorclass: importmath importrandom classActor(cocos.cocosnode.CocosNode): def__init__(self,x,y): super(Actor,self).__init__() self.position=(x,y) self.velocity=eu.Vector2(0,0) self.wander_angle=0 self.circle_distance=50 self.circle_radius=10 self.angle_change=math.pi/4 self.max_velocity=50 self.add(ps.Sun()) self.schedule(self.update) Withthesemembers,wecanmodifyourupdate()method: defupdate(self,dt): circle_center=self.velocity.normalized()*\ self.circle_distance dx=math.cos(self.wander_angle) dy=math.sin(self.wander_angle) displacement=eu.Vector2(dx,dy)*self.circle_radius self.wander_angle+=(random.random()-0.5)*\ self.angle_change self.velocity+=circle_center+displacement self.velocity=truncate(self.velocity, self.max_velocity) self.position+=self.velocity*dt self.position=(self.x%640,self.y%480) Thesenewstatementsperformthefollowingoperations: Thecircle’scenterisplacedatthegivendistanceaheadofthecurrentcharacter’s velocity. Wecalculatethedisplacementwithwander_angle,anditisscaledaspertheradius ofthecircle. Givenarandomvaluebetween0.0and1.0,wesubtract0.5sothatthevalueis between-0.5and0.5,thuspreventingtheanglefromalwayschangingbyapositive value.Wemultiplythisvaluebyangle_changeandaddittothewander_angle member. Youmaynoticeanotherstatementafterthepositionisupdatedwiththevelocity.Thislast statementofthemethodisusedtorelocatethecharacter,sinceitisverylikelythatthe characterwillleavethescreenduetotherandomnessofitsmovement. Feelfreetotweakthecircle_distance,circle_radius,andangle_changevalues,and observethedifferencesthattheycausetotheplayer’smovement. Ourmainlayerissimplifiedsinceitonlyneedstoaddtheactor: classMainLayer(cocos.layer.Layer): def__init__(self): super(MainLayer,self).__init__() self.actor=Actor(320,240) self.add(self.actor) Thewander.pyscriptcontainsthecompleteimplementationofthisbehavior. Obstacleavoidance Sofar,wehavenotplacedanybarrierinourworld,sothecharactercanfreelymove aroundwithoutavoidinganyarea. However,mostgamesplacesomeobstacles,andthecharacterscannotpassthroughthem. Theobstacleavoidancebehaviorgeneratesasteeringforcethatcausesthecharactertotry anddodgetheseblockingitems.Notethatthisdoesnotperformcollisiondetections,soif thecurrentspeedishighandthesteeringforcemoduleisnotenoughtocounteractthe character’svelocity,thecharactermightoverlapwiththeshapeoftheobstacle. Todetectthepresenceofobstaclesahead,thecharacterwillkeepanimaginaryvector alongitsforwardaxis,whichrepresentsthedistanceuptowhichitcanseeahead.The closestobstaclethatcollideswiththissegment,ifany,willbethethreattoavoid. WewilldefineanObstacleclasstorepresentthecircularobstaclesthatwewilluse.Each ofthemcanhaveadifferentradius,andwewillkeeptrackofallthecreatedinstanceswith aclassmemberlist: classObstacle(cocos.cocosnode.CocosNode): instances=[] def__init__(self,x,y,r): super(Obstacle,self).__init__() self.position=(x,y) self.radius=r particles=ps.Sun() particles.size=r*2 particles.start_color=ps.Color(0.0,0.7,0.0,1.0) self.add(particles) self.instances.append(self) TheActorclasswillhavetwonewattributes:max_ahead,themaximumdistanceat whichthecharactercandetectthepresenceofanobstacle,andmax_avoid_force,the maximumforcethatcanbeappliedtododgeanobstacle: classActor(cocos.cocosnode.CocosNode): def__init__(self,x,y): super(Actor,self).__init__() self.position=(x,y) self.velocity=eu.Vector2(0,0) self.speed=2 self.max_velocity=300 self.max_force=10 self.target=None self.max_ahead=200 self.max_avoid_force=300 self.add(ps.Sun()) self.schedule(self.update) Thesteeringforcekeepsseekingthetarget,withthedifferencethattheavoidforceis appliedaswell: defupdate(self,dt): ifself.targetisNone: return distance=self.target-eu.Vector2(self.x,self.y) steering=distance*self.speed-self.velocity steering+=self.avoid_force() steering=truncate(steering,self.max_force) self.velocity=truncate(self.velocity+steering, self.max_velocity) self.position+=self.velocity*dt Theavoidforceiscalculatedwiththeclosestobstacle.Tocheckwhethertheaheadvector intersectseachobstacle,wewillcalculatetheminimumdistancefromthecircle’scenterto thevector.Ifthisdistanceislowerthanthecircle’sradiusanditistheminimumdistance, theobstacleistheclosestone. Tofindthedistancebetweenavectorandapoint,wemustprojectthatpointtothevector andseeifitfallsintoitslength.Ifitdoes,wecalculatethedistancebetweenthesetwo points,whichisthedistancebetweenthevectorandthepoint.Otherwise,itmeansthatthe pointisbehindorbeyondtheprojectionofthepointonthevector. Withtheseoperationsinmind,wecandefinetheavoid_force()method: defavoid_force(self): avoid=eu.Vector2(0,0) ahead=self.velocity*self.max_ahead/self.max_velocity l=ahead.dot(ahead) ifl==0: returnavoid closest,closest_dist=None,None forobjinObstacle.instances: w=eu.Vector2(obj.x-self.x,obj.y-self.y) t=ahead.dot(w) if0<t<l: proj=self.position+ahead*t/l dist=abs(obj.position-proj) ifdist<obj.radiusand\ (closestisNoneordist<closest_dist): closest,closest_dist=obj.position,dist ifclosestisnotNone: avoid=self.position+ahead-closest avoid=avoid.normalized()*self.max_avoid_force returnavoid Thesearethestepsperformedbythismethod: Wecalculatetheaheadvectoranditssquaredlength.Ifitiszero,itmeansthatitdoes notseeanythingaheadandtheavoidanceforceis(0,0). Foreachobstacle,wekeeptrackofthedistancebetweentheaheadvectorandthe circle’scenter.Ifthisdistanceislowerthanitsradiusanditistheclosestobstacle,we setitastheobstacletoavoid. Finally,ifthereisanyobstacleselected,wescaletheavoidforcebythe max_avoid_forcefactor. WewilladdsomeobstaclestoourMainLayer,andtheactorwillavoidthemwithoutany furtherreferencetothesenewobjects,sincetheyareautomaticallyaddedtothe Obstacle.instanceslist: classMainLayer(cocos.layer.Layer): is_event_handler=True def__init__(self): super(MainLayer,self).__init__() self.add(Obstacle(200,200,40)) self.add(Obstacle(240,350,50)) self.add(Obstacle(500,300,50)) self.actor=Actor(320,240) self.add(self.actor) defon_mouse_motion(self,x,y,dx,dy): self.actor.target=eu.Vector2(x,y) Youcancheckouttheentirescriptintheobstacles.pyfile.Runitandmovethemouse pointerabovethescreentoseehowthecharactertriestoavoidtheobstaclesrepresented bythegreencircles. Gravitationgame Toputintopracticewhatyouhavelearnedaboutsteeringbehaviors,wewillbuildabasic game.Theplayer’sobjectiveistocollectsomepickupitemsthatrotatearounddifferent planetsplacedintheworld,whileescapingfromenemies.Theenemiesarenon-playable charactersthatseektheplayablecharacterandavoidtheplanets. Wewillrepresentthecharactersasparticlesystems,andthefinalversionofthegamewill looklikethis: Basicgameobjects Asusual,wewilldefineabaseclassthatwrapstheaccesstotheCircleShapeattribute withtheupdatedcenter: fromcocos.cocosnodeimportCocosNode fromcocos.directorimportdirector importcocos.collision_modelascm classActor(CocosNode): def__init__(self,x,y,r): super(Actor,self).__init__() self.position=(x,y) self._cshape=cm.CircleShape(self.position,r) @property defcshape(self): self._cshape.center=eu.Vector2(self.x,self.y) returnself._cshape Sincesomecharacterswillrotatearoundtheplanets,wewilldefineanotherclassto updatethepositionoftheactorbasedonarotationalmovement.Thisclasswillhavea referencetotheplanetitisrotatingaround,theangularspeed,andthecurrentangleof rotation: classMovingActor(Actor): def__init__(self,x,y,r): super(MovingActor,self).__init__(x,y,r) self._planet=None self._distance=0 self.angle=0 self.rotationSpeed=0.6 self.schedule(self.update) AccesstothePlanetmemberwillbeimplementedthroughaproperty,becausewhenwe setplanet,weneedtocalculatethedistanceandthecurrentangle: @property defplanet(self): returnself._planet @planet.setter defplanet(self,val): ifvalisnotNone: dx,dy=self.x-val.x,self.y-val.y self.angle=-math.atan2(dy,dx) self._distance=abs(eu.Vector2(dx,dy)) self._planet=val Thescheduledmethodincrementstherotationanglewiththeelapsedtimeandupdatesthe actor’spositionwiththecosineandsineofthisangle: defupdate(self,dt): ifself.planetisNone: return dist=self._distance self.angle+=self.rotationSpeed*dt self.angle%=math.pi*2 self.x=self.planet.x+dist*math.cos(self.angle) self.y=self.planet.y-dist*math.sin(self.angle) Planetsandpickups OurPlanetclasswillrepresentthestaticactorsaroundwhichthepickupsandtheplayer rotate.Wewillkeeptrackofalltheinstances,aswedidwiththeObstacleclass previously: classPlanet(Actor): instances=[] def__init__(self,x,y,r=50): super(Planet,self).__init__(x,y,r) particles=ps.Sun() particles.start_color=ps.Color(0.5,0.5,0.5,1.0) particles.size=r*2 self.add(particles) self.instances.append(self) Pickupswillbemovingactors,sowewillinheritfromMovingActor,whichhas implementedthisfunctionality: classPickupParticles(ps.Sun): size=20 start_color=ps.Color(0.7,0.7,0.2,1.0) classPickup(MovingActor): def__init__(self,x,y,planet): super(Pickup,self).__init__(x,y,10) self.planet=planet self.gravity_factor=50 self.particles=PickupParticles() self.add(self.particles) Playerandenemies Theplayercharacterrevolvesaroundtheplanetslikethepickupsdo,butwiththe differencethatitcanswitchfromoneplanettoanotherwhenthespacebarispressed. Thisisthecontrolthattheplayerhasovertheactor,sowewillneedtoaddalinearspeed toimplementthiskindofmovement,whichextendsthelogicofferedbytheMovingActor class: classPlayer(MovingActor): def__init__(self,x,y,planet): super(Player,self).__init__(x,y,16) self.planet=planet self.rotationSpeed=1 self.linearSpeed=80 self.direction=eu.Vector2(0,0) self.particles=ps.Meteor() self.particles.size=50 self.add(self.particles) Nowtheupdate()methodperformsalinearmovementwhentheplayerisnotrevolving aroundanyplanet: defupdate(self,dt): ifself.planetisnotNone: super(Player,self).update(dt) gx=20*math.cos(self.angle) gy=20*math.sin(self.angle) self.particles.gravity=eu.Point2(gx,-gy) else: self.position+=self.direction*dt Finally,weneedtoimplementthemethodthattriggerstheswitchfromoneplanetto another.Itcalculatesthedirectionvectordependingonthecurrentangleofrotationwith respecttotheplanet: defswitch(self): new_dir=eu.Vector2(self.y-self.planet.y, self.planet.x-self.x) self.direction=new_dir.normalized()*self.linearSpeed self.planet=None self.particles.gravity=eu.Point2(-self.direction.x, -self.direction.y) Youmaynoticethatthegravityoftheparticlesisupdatedwiththeplayer’smovement. Thisgivesvisualfeedbackofthecurrentdirectionofthecharacterandthepositionitis pointingto. TheimplementationoftheEnemyclassisalmostthesameastheonewesawwiththe obstacleavoidancebehavior.Themaindifferencesarethatthisclassusesthe Planet.instances:listandtheradiusoftheobstaclesisretrievedfromthecshape memberofeachplanet: defavoid_force(self): #... closest,closest_dist=None,None forobjinPlanet.instances: w=eu.Vector2(obj.x-self.x,obj.y-self.y) t=ahead.dot(w) if0<t<l: proj=self.position+ahead*t/l dist=abs(obj.position-proj) ifdist<obj.cshape.rand\ (closestisNoneordist<closest_dist): closest,closest_dist=obj.position,dist ifclosestisnotNone: avoid=self.position+ahead-closest avoid=avoid.normalized()*self.max_avoid_force returnavoid Explosions Therearesomecollisionsthatcantriggeragameevent.Forinstance,whenanitemis pickedupbytheplayer,itmustberemoved,orwhentheplayablecharacterhitsanenemy oraplanet,itisdestroyedandspawnsagaininitsoriginalposition. Onewaytoenhancethesesituationsisthroughexplosions;theygivevisualfeedbackto theplayer.Wewillimplementthemwithacustomparticlesystem: classActorExplosion(ps.ParticleSystem): total_particles=400 duration=0.1 gravity=eu.Point2(0,0) angle=90.0 angle_var=360.0 speed=40.0 speed_var=20.0 life=3.0 life_var=1.5 emission_rate=total_particles/duration start_color_var=ps.Color(0.0,0.0,0.0,0.2) end_color=ps.Color(0.0,0.0,0.0,1.0) end_color_var=ps.Color(0.0,0.0,0.0,0.0) size=15.0 size_var=10.0 blend_additive=True def__init__(self,pos,particles): super(ActorExplosion,self).__init__() self.position=pos self.start_color=particles.start_color Thegamelayer Allthegameobjectsthatwehavedevelopedwillbeaddedtothemaingamelayer.As usual,itwillcontainacollisionmanagerthatcoverstheentirewindow: classGameLayer(cocos.layer.Layer): def__init__(self): super(GameLayer,self).__init__() x,y=director.get_window_size() cell_size=32 self.coll_man=cm.CollisionManagerGrid(0,x,\ 0,y,cell_size,cell_size) self.planet_area=400 planet1=self.add_planet(450,280) planet2=self.add_planet(180,200) planet3=self.add_planet(270,440) planet4=self.add_planet(650,480) planet5=self.add_planet(700,150) self.add_pickup(250,250,planet2) self.add_pickup(740,480,planet4) self.add_pickup(700,60,planet5) self.player=Player(300,350,planet3) self.add(self.player) self.add(Enemy(600,100,self.player)) self.schedule(self.game_loop) Wealsodefinedacoupleofhelpermethodstoavoidrepeatingthesamecodeduring initialization: defadd_pickup(self,x,y,target): pickup=Pickup(x,y,target) self.add(pickup) defadd_planet(self,x,y): planet=Planet(x,y) self.add(planet) returnplanet Oncethescenarioissetupwiththepositionsofthecharacters,andwhatactorsare rotatingaroundeachplanet,wecanimplementthegameloop: defgame_loop(self,_): self.coll_man.clear() fornodeinself.get_children(): ifisinstance(node,Actor): self.coll_man.add(node) ifself.player.is_running: self.process_player_collisions() defprocess_player_collisions(self): player=self.player forobjinself.coll_man.iter_colliding(player): ifisinstance(obj,Pickup): self.add(ActorExplosion(obj.position, obj.particles)) obj.kill() else: self.add(ActorExplosion(player.position, player.particles)) player.kill() Thelayermustprocesstheinputevents,specificallythespacebarpressthattriggerswhen theplayer’scharacterstopsrotatingaroundaplanetandleavestheorbitinaperpendicular direction. Whenthespacebarispressedagain,thecollisionmanagercalculateswhattheclosest planetiswithinaspecificdistance—whichwesetupinthe__init__method—andif thereisany,itissetastheplanetaroundwhichthetargetmustrotate: is_event_handler=True defon_key_press(self,k,_): ifk!=key.SPACE: return ifself.player.planetisNone: self.player.planet=self.find_closest_planet() else: self.player.switch() deffind_closest_planet(self): ranked=self.coll_man.ranked_objs_near(self.player, self.planet_area) planet=next(filter(lambdax:isinstance(x[0],Planet), ranked)) returnplanet[0]ifplanetisnotNoneelseNone Finally,donotforgettoaddtheconditionalblocktorunthescriptasthemainmodule. Here,wesetthewindow’swidthandheight: if__name__=='__main__': director.init(width=850,height=600,caption='Gravitation') director.run(cocos.scene.Scene(GameLayer())) Youcancheckoutthecompletegameinthegravitation.pyscript.Thegamedoesnot requireanyextraassets,sincealltherenderedelementsarebasedonthecocos2dparticle systems. Summary Inthischapter,youlearnedseveralkindsofsteeringbehaviorthatproducerealisticand autonomousnavigationsaroundyourgameworld.Thesestrategiesmightbemixed, causingcomplexpatternsandseeminglymoreintelligentactions. Trytochangethevaluesoftheconstantsusedineachbehaviortoseehowtoachieve differentvelocitiesandforces.Keepinmindtheimportanceofgeneratingsmooth movementsinsteadofpronouncedtwistssothattheexperienceisvisuallyengaging. Finally,weappliedthisknowledgeinabasicgame.Sinceitisademonstrationofaddinga non-playablecharacter,thegamecanbecomplementedwithalltheingredientsthatwe coveredinthepreviouscharacter(suchasmenus,transitions,andsoon). Inthenextchapter,wewilljumpinto3DgamedevelopmentwithOpenGL,anewtopic withmoreadvancedfeaturesanddetailsofalowerlevel. Chapter5.Pygameand3D Inourpreviouschapters,wedevelopedour2DgameswithPythonmodulesthatarebuilt ontopofagraphicaluserinterfacelibrary,suchasTkinterandPyglet.Thisallowedus tostartcodingourgameswithoutworryingaboutthelower-leveldetails. Nowwewilldevelopourfirst3DgamewithPython,whichwillrequireanunderstanding ofsomebasicprinciplesofOpenGL,apopularmultiplatformAPIforbuilding2Dand3D applications.YouwilllearnhowtointegratetheseprogramswithPygame,aPython librarycommonlyusedtocreatesprite-basedgames. Inthischapter,wewillcoverthefollowingtopics: AsteadyapproachtoPyOpenGLandPygame InitializinganOpenGLcontext UnderstandingthedifferentmodesthatcanbeenabledwithOpenGL Howtorenderlightsandsimpleshapes IntegratingOpenGLwithPygame Drawingprimitivesandperformanceimprovements Installingpackages PyOpenGLisapackagethatoffersPythonbindingstoOpenGLandrelatedAPIs,suchas GLUandGLUT.ItisavailableonthePythonpackageIndex,soyoucaneasilyinstallit viapip: $pipinstallPyOpenGL However,wewillneedfreeglutforourfirstexamples,beforeweintegrateOpenGLwith Pygame.Freeglutisathird-partylibrarythatisnotincludedifyouinstallthepackage fromPyPI. OnWindows,analternativeistodownloadandinstallthecompiledbinariesfrom http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyopengl.Remembertoinstalltheversionfor Python3.4. Pygameistheotherpackagethatwewillneedinthischapter.Itcanbedownloadedfrom theofficialwebsiteathttp://www.pygame.org/download.shtml.Youcaninstallitfrom sourceifyouwantto;thecompilationpagecontainsthestepsforbuildingPygameon differentplatforms. WindowsuserscandirectlyusetheMSIforPython3.2ordownloadUnofficialWindows BinariesfromtheChristophGohlke’swebsite (http://www.lfd.uci.edu/~gohlke/pythonlibs/). Macintoshuserscanfindtheinstructionsrequiredtocompileitfromsourceonthe Pygamewebsiteathttp://pygame.org/wiki/macintosh. GettingstartedwithOpenGL OpenGLisabroadtopicinitself,anditispossibletofindplentyoftutorials,books,and otherresources,usuallytargetedatCorC++. Sincethischapterisnotintendedtobeacomprehensiveguideforthisspecification,we willtakeadvantageofGLUT,whichstandsforOpenGLUtilityToolkit.Itiswidelyused insmallapplicationsbecauseofitssimplicityandportability,andthebindingsare implementedinPyOpenGL. GLUTwillhelpusperformsomebasicoperations,suchascreatingwindowsandhandling inputevents. Tip GLUTlicensing Unfortunately,GLUTisnotinthepublicdomain.Thecopyrightismaintainedbyits author,MarkKilgard,whowroteitforthesampleprogramsincludedinRedBook,the officialOpenGLprogrammingguide. Thisisthereasonweareusingfreeglut,oneoftheopensourcealternativesthat implementtheGLUTAPI. Initializingthewindow Thefirstlinesofourscriptwillbetheimportstatementsaswellasthedefinitionofour Appclassandits__init__method. ApartfromtheOpenGLAPIandGLUT,weimporttheOpenGLUtilityLibrary(GLU). GLUisusuallydistributedwiththebasicOpenGLpackage,andwewilluseacoupleof functionsofferedbythislibraryinourexample: importsys importmath fromOpenGL.GLimport* fromOpenGL.GLUimport* fromOpenGL.GLUTimport* classApp(object): def__init__(self,width=800,height=600): self.title=b'OpenGLdemo' self.width=width self.height=height self.angle=0 self.distance=20 Youmaywonderwhatthebbeforethe'OpenGLdemo'stringmeans.Itrepresentsabinary string,anditisoneofthedifferencesbetweenPython2and3.Therefore,ifyoufinda GLUTprogramwritteninPython2,rememberthatthestringtitleofthewindowmustbe definedasabinarystringinordertoworkwithPython3. Withtheseinstancemembers,wecancallourOpenGLinitializationfunctions: defstart(self): glutInit() glutInitDisplayMode(GLUT_DOUBLE|GLUT_DEPTH) glutInitWindowPosition(50,50) glutInitWindowSize(self.width,self.height) glutCreateWindow(self.title) glEnable(GL_DEPTH_TEST) glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) Stepbystep,ourstartmethodperformsthefollowingoperations: glutInit():ThisinitializestheGLUTlibrary.Whileitispossibletopassparameters tothisfunction,wewillleavethiscallwithoutanyarguments. glutInitDisplayMode():Thissetsthedisplaymodeofthetop-levelwindowthatwe willcreate.ThemodeisthebitwiseORofafewGLUTdisplaymodemasks. GLUT_DOUBLEisthemodeforthedoublebuffer,whichcreatesseparatefrontandback buffers.Whileoneofthesebuffersisbeingdisplayed,theotheroneisbeing rendered.Ontheotherhand,GLUT_DEPTHrequestsadepthbufferforthewindow.It storesthezcoordinateofeachgeneratedpixel,andifthesamepixelisrenderedfora secondtimebecausetwoobjectsoverlap,itdetermineswhichobjectisclosertothe camera,thatis,reproducingthedepthperception. glutInitWindowPosition()andglutInitWindowsSize():Thesesettheinitialposition ofthewindowanditssize.Accordingtoourwidthandheightinstancemembers,it indicatestocreateawindowof800x600pixelswithanoffsetof50pixelsinthex andyaxesfromthetop-leftcornerofthescreen. glutCreateWindow():Thiscreatesthetop-levelwindowofourapplication.The argumentpassedtothisfunctionisabinarystringforuseasthewindowtitle. glEnable():ThisisthefunctionusedtoenabletheGLcapabilities.Inourapp,we callitwiththefollowingvalues: GL_DEPTH_TEST:Thisperformsdepthcomparisonsandupdatesthedepthbuffer. GL_LIGHTING:Thisenableslighting. GL_LIGHT0:ThisenablesLight0.PyOpenGLdefinesaspecificnumberoflight constants—fromGL_LIGHT0toGL_LIGHT8—buttheparticularimplementationof OpenGLthatyouarerunningmightallowmorethanthisnumber. Tip Lightingandcolors Whenlightingisenabled,thecolorsarenotdeterminedbytheglColorfunctionsbutby thecombinationofthelightingcomputationandthematerialcolorssetbyglMaterial.To combinelightingwithglColor,itisrequiredthatyouenableGL_COLOR_MATERIALfirst: glEnable(GL_COLOR_MATERIAL) #... glColor4f(r,g,b,a) #Drawpolygons OncewehaveinitializedGLUTandenabledtheGLcapabilities,wecompleteour start()methodbyspecifyingtheclearcolor,settingtheperspective,andstartingthe mainloop: defstart(self): #... glClearColor(.1,.1,.1,1) glMatrixMode(GL_PROJECTION) aspect=self.width/self.height gluPerspective(40.,aspect,1.,40.) glMatrixMode(GL_MODELVIEW) glutDisplayFunc(self.display) glutSpecialFunc(self.keyboard) glutMainLoop() defkeyboard(self,key,x,y): pass Thesestatementsperformthefollowingoperations: glClearColor():Thisdefinestheclearvaluesforthecolorbuffer;thatis,eachpixel willhavethisvalueifnoothercolorisrenderedinthispixel. glMatrixMode():Thissetsthematrixstackmodeformatrixoperations,inthiscase totheprojectionmatrixstack.OpenGLconcatenatesmatrixoperationsfor hierarchicalmodes,makingiteasytocomposethetransformationofachildobject relativetoitsparent.WithGL_PROJECTION,wesetthematrixmodefortheprojection matrixstack. gluPerspective():Thepreviousstatementsetstheprojectionmatrixstackasthe currentstack.Withthisfunction,wecangeneratetheperspectiveprojectionmatrix. Theparametersthatgeneratethismatrixareasfollows: fovy:Theviewangleindegreesintheydirection. aspect:Thisistheaspectratioofthefieldofview.Itistheratiooftheviewport widthtotheviewportheight. zNear:ThedistancefromtheviewertotheNearplane. zFar:ThedistancefromtheviewertotheFarplane. WithglMatrixMode(GL_MODELVIEW),wesetthemodelviewmatrixstack,whichisthe initialvalue,asthecurrentmatrixmode. ThelastthreeGLUTcallsdothefollowing: glutDisplayFunc():Thisreceivesthefunctionthatwillbeinvokedtodisplaythe window. glutSpecialFunc():Thissetsthekeyboardcallbackforthecurrentwindow.Notethat thiscallbackwillbetriggeredonlywhenthekeysrepresentedbytheGLUT_KEY_* constantsarepressed. glutMainLoop():Thisstartsthemainloopoftheapplication. WiththeOpenGLcontextinitialized,weareabletocalltheOpenGLfunctionsthatwill renderourscene. Tip TheOpenGLandGLUTreference Asyoumayhavealreadynoticed,theOpenGLandGLUTspecificationsdefinealarge numberoffunctions.YoucanfindthebindingsoftheseAPIsimplementedbyPyOpenGL ontheofficialwebsiteathttp://pyopengl.sourceforge.net/documentation/manual3.0/index.html. Drawingshapes Ourdisplay()functionperformstheverycommontasksofamaingameloop. Itfirstclearsthescreen,thensetsupaviewingtransformation(wewillseewhatthis meansafterthesnippet),andfinallyrendersthelightanddrawsthegameobjects: defdisplay(self): x=math.sin(self.angle)*self.distance z=math.cos(self.angle)*self.distance glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) glLoadIdentity() gluLookAt(x,0,z, 0,0,0, 0,1,0) glLightfv(GL_LIGHT0,GL_POSITION,[15,5,15,1]) glLightfv(GL_LIGHT0,GL_DIFFUSE,[1.,1.,1.,1.]) glLightfv(GL_LIGHT0,GL_CONSTANT_ATTENUATION,0.1) glLightfv(GL_LIGHT0,GL_LINEAR_ATTENUATION,0.05) glPushMatrix() glMaterialfv(GL_FRONT,GL_DIFFUSE,[1.,1.,1.,1.]) glutSolidSphere(2,40,40) glPopMatrix() glPushMatrix() glTranslatef(4,2,0) glMaterialfv(GL_FRONT,GL_DIFFUSE,[1.,0.4,0.4,1.0]) glutSolidSphere(1,40,40) glPopMatrix() glutSwapBuffers() Thesearetheoperationsthatdisplay()performs: glClear():WiththeGL_COLOR_BUFFER_BITandGL_DEPTH_BUFFER_BITmasks,this clearsthecoloranddepthbuffers. glLoadIdentity():Thisloadstheidentitymatrixasthecurrentmatrix.Theidentity matrixisa4x4matrixwithonesinthemaindiagonalandzeroseverywhereelse. Thismakesthestackmatrixstartoverattheorigin,whichisusefulifyouhave previouslyappliedsomematrixtransformations. gluLookAt():Thiscreatesaviewingmatrix.Thefirstthreeparametersarethex,y, andzcoordinatesoftheeyepoint.Thenextthreeparametersarethex,y,andz coordinatesofthereferencepoint,thatis,thepositionthecameraislookingat. Finally,thelastthreeparametersspecifythedirectionoftheupvector(usually,itis0, 1,0). glLightfv():Thissetstheparametersoflightsource0(GL_LIGHT0).Thefollowing parametersarespecifiedinourexample: GL_POSITION:Thisdefinesthepositionofthelight GL_DIFFUSE:ThissetstheRGBAintensityofthelight GL_CONSTANT_ATTENUATION:Thisspecifiestheconstantattenuationfactor GL_LINEAR_ATTENUATION:Thisspecifiesthelinearattenuationfactor Oncethelightingattributesareset,wecanstartrenderingbasicshapeswithGLUT.Ifwe drawtheobjectsfirst,lightingwillnotbeappliedcorrectly: glPushMatrix():Thispushesanewmatrixintothecurrentmatrixstack,identicalto theonebelowit.Whilewedothis,wecanapplytransformationssuchas glTranslateandglRotate,tothismatrix.Wewillrenderourfirstsphereatthe origin,butthesecondonewillbetransformedwithglTranslate. glTranslate():Thismultipliesthecurrentmatrixbythetranslationmatrix.Inour example,thetranslationvaluesforthesecondsphereare4forthexaxis,and2for theyaxis. glMaterialfv():Thissetsthematerialparametersofthefrontface,asitiscalledwith GL_FONT.WithGL_DIFFUSE,wespecifythatwearesettingtheRGBAreflectanceof thematerial. glutSolidSphere():ThroughGLUT,thisroutineallowsustoeasilydrawasolid sphere.Itreceivesthesphere’sradiusasthefirstargument,andthenumberofslices andstacksintowhichthespherewillbesubdivided.Thegreaterthesevaluesare,the rounderthespherewillbe. glPopMatrix():Thispopsthecurrentmatrixfromthestack.Ifwedidnotdothis, eachnewobjectrenderedwouldbeachildofthepreviousone. Finally,weswitchthebufferswithglutSwapBuffers().Ifdoublebufferingwasnot enabled,weshouldcallthesinglebufferequivalent—glFlush(). Runningthedemo Asusual,wecheckwhetherthemoduleisthemainscriptforstartingtheapplication: if__name__=='__main__': app=App() app.start() Ifyourunthecompleteapplication,theresultwilllooklikewhatisshowninthe followingscreenshot: RefactoringourOpenGLprogram Asyoumayhaveseen,thisexampleusesenoughOpenGLcallstogrowoutofcontrolif wedonotstructureourcode.That’swhywearegoingtoapplysomeobject-oriented principlestoachieveabetterorganization,withoutmodifyingtheorderofthecallsor losinganyfunctionality. ThefirststepwillbetodefineaLightclass.Itwillholdtheattributesneededtorenderthe light: classLight(object): enabled=False colors=[(1.,1.,1.,1.),(1.,0.5,0.5,1.), (0.5,1.,0.5,1.),(0.5,0.5,1.,1.)] def__init__(self,light_id,position): self.light_id=light_id self.position=position self.current_color=0 Besides,thismodularizationwillhelpusimplementanewfunctionality:changingthe colorofthelight.Wesetthecurrentcolorindexto0,andwewilliterateoverthedifferent colorsdefinedinLight.colorseachtimetheswitch_color()methodiscalled. Therender()methodrespectstheoriginalimplementationoflightingfromournonrefactoredversion: defrender(self): light_id=self.light_id color=Light.colors[self.current_color] glLightfv(light_id,GL_POSITION,self.position) glLightfv(light_id,GL_DIFFUSE,color) glLightfv(light_id,GL_CONSTANT_ATTENUATION,0.1) glLightfv(light_id,GL_LINEAR_ATTENUATION,0.05) defswitch_color(self): self.current_color+=1 self.current_color%=len(Light.colors) Finally,wewrapthecalltoenablelightingwiththeenable()methodandaclass attribute: defenable(self): ifnotLight.enabled: glEnable(GL_LIGHTING) Light.enabled=True glEnable(self.light_id) AnotherimprovementisthecreationofaSphereclass.Thisclasswillallowusto customizetheradius,position,andcolorofeachinstance: classSphere(object): slices=40 stacks=40 def__init__(self,radius,position,color): self.radius=radius self.position=position self.color=color defrender(self): glPushMatrix() glTranslatef(*self.position) glMaterialfv(GL_FRONT,GL_DIFFUSE,self.color) glutSolidSphere(self.radius,Sphere.slices,Sphere.stacks) glPopMatrix() Withtheseclasses,wecanadaptourAppclassandcreatetheinstancesthatwewillrender inthemainloop: classApp(object): def__init__(self,width=800,height=600): #... self.light=Light(GL_LIGHT0,(15,5,15,1)) self.sphere1=Sphere(2,(0,0,0),(1,1,1,1)) self.sphere2=Sphere(1,(4,2,0),(1,0.4,0.4,1)) Rememberthatbeforerenderingthelightobject,weneedtoenableOpenGLlighting throughthelight.enable()method: defstart(self): #... glEnable(GL_DEPTH_TEST) self.light.enable() #... Nowthedisplay()methodbecomessuccinctandexpressive,sincetheapplication delegatestheOpenGLcallstotheobjectinstances: defdisplay(self): x=math.sin(self.angle)*self.distance z=math.cos(self.angle)*self.distance glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) glLoadIdentity() gluLookAt(x,0,z, 0,0,0, 0,1,0) self.light.render() self.sphere1.render() self.sphere2.render() glutSwapBuffers() Tocompleteourfirstsampleapplication,wewilladdinputhandling.Itallowstheplayer torotatethecameraaroundthespheresandmoveforwardorawayfromthecenterofthe scene. Processingtheuserinput Aswesawearlier,glutSpecialFunctakesacallbackfunctionthatreceivesthepressedkey asthefirstargument,andthexandycoordinatesofthemousewhenthekeywaspressed. Wewillusetherightandleftarrowkeystomovearoundthespheres,andtheupanddown arrowkeystoapproximateormoveawayfromthespheres.Besidesallofthis,thecolorof thelightwillchangeifwepressF1,andtheapplicationwillbeclosediftheInsertkeyis pressed. Todoso,wewillcheckthevaluesofthekeyargumentwiththerespectiveGLUT constants: defkeyboard(self,key,x,y): ifkey==GLUT_KEY_INSERT: sys.exit() ifkey==GLUT_KEY_UP: self.distance-=0.1 ifkey==GLUT_KEY_DOWN: self.distance+=0.1 ifkey==GLUT_KEY_LEFT: self.angle-=0.05 ifkey==GLUT_KEY_RIGHT: self.angle+=0.05 ifkey==GLUT_KEY_F1: self.light.switch_color() self.distance=max(10,min(self.distance,20)) self.angle%=math.pi*2 glutPostRedisplay() Notethatwetrimmedthevalueoftheself.distancemember,soitsvalueisalways between10and20,andself.angleisalsoalwaysbetween0and2π.Tonotifythatthe currentwindowneedstoberedisplayed,wecallglutPostRedisplay(). YoucancheckouttheChapter5_02.pyscript,whichcontainsthisrefactoredversionof ourapplication. Whenyourunit,pressthearrowkeystorotatearoundthespheresandF1toseehowthe lightingaffectsthespheres’materials. AddingthePygamelibrary WithGLUT,wecanwriteOpenGLprogramsquickly,primarilybecauseitwasaimedto provideroutinesthatmakelearningOpenGLeasier.However,theGLUTAPIwas discontinuedin1998.Nonetheless,therearesomepopularsubstitutesinthePython ecosystem. Pygameisoneofthesealternatives,andwewillseethatitcanbeseamlesslyintegrated withOpenGL,evensimplifyingtheresultingcodeforthesameprogram. Pygame101 BeforeweintegratePygameintoourOpenGLprogram,wewillwriteasample2D applicationtogetstartedwithPygame. WewillimportPygameanditslocalsmodule,whichincludestheconstantsthatwewill needinourapplication: importsys importpygame frompygame.localsimport* classApp(object): def__init__(self,width=400,height=300): self.title='Hello,Pygame!' self.fps=100 self.width=width self.height=height self.circle_pos=width/2,height/2 Pygameusesregularstringsforthewindowtitle,sowewilldefinetheattributewithout addingb.Anotherchangeisthenumberofframespersecond(FPS),whichwewilllater findouthowtocontrolviaPygame’sclock: defstart(self): pygame.init() size=(self.width,self.height) screen=pygame.display.set_mode(size,DOUBLEBUF) pygame.display.set_caption(self.title) clock=pygame.time.Clock() whileTrue: dt=clock.tick(self.fps) foreventinpygame.event.get(): ifevent.type==QUIT: pygame.quit() sys.exit() pressed=pygame.key.get_pressed() x,y=self.circle_pos ifpressed[K_UP]:y-=0.5*dt ifpressed[K_DOWN]:y+=0.5*dt ifpressed[K_LEFT]:x-=0.5*dt ifpressed[K_RIGHT]:x+=0.5*dt self.circle_pos=x,y screen.fill((0,0,0)) pygame.draw.circle(screen,(0,250,100), (int(x),int(y)),30) pygame.display.flip() WeinitializethePygamemoduleswithpygame.init(),andthenwecreateascreenwitha givenwidthandheight.TheDOUBLEBUFflagispassedsoastoenabledoublebuffering, whichhasthebenefitswementionedpreviously. Themaineventloopisimplementedwithawhileblock,andwiththeClockinstance,we cancontroltheframerateandcalculatetheelapsedtimebetweenframes.Thisvaluewill bemultipliedbythespeedofmovement,sothecirclewillmoveatthesamespeedifthe FPSvaluechanges. Withpygame.event.get(),weretrievetheeventqueue,andifaQUITeventoccurs,the windowisclosedandtheapplicationfinishesitsexecution. Thepygame.key.get_pressed()returnsalistwiththepressedkeys,andwiththekey constants,wecancheckwhetherthearrowkeysarepressed.Ifso,thecircle’spositionis updatedanditisdrawnonthenewcoordinates. Finally,pygame.display.flip()updatesthescreen’ssurface. TheChapter5_03.pyscriptcontainsthefullcodeofthisexample. Tip ThePygamedocumentation SincePygameisdividedintoseveralmodules,eachonewithvariousfunctions,classes, andconstants,theofficialdocumentationisausefulreference. Weareusingsomefunctionsfromthekeymodule;youcanfindfurtherinformationabout itathttps://www.pygame.org/docs/ref/key.html.Thesameappliesforthedisplayand timemodules. Pygameintegration Let’sseehowitispossibletoimplementthesamefunctionalitywithPygame.Thefirst stepistoreplacetheOpenGL.GLUTimportwiththeonesweusedinourpreviousexample: importsys importmath importpygame frompygame.localsimport* fromOpenGL.GLimport* fromOpenGL.GLUimport* Thetitlestringisnowaregularstring,andtheFPSattributecanbeaddedaswell: classApp(object): def__init__(self,width=800,height=600): self.title='OpenGLdemo' self.fps=60 self.width=width self.height=height #... WeremovetheGLUTcallsfromourstart()method,andtheyarereplacedbythe Pygameinitialization.ApartfromDOUBLEBUF,wewilladdtheOPENGLflagtocreatean OpenGLcontext: defstart(self): pygame.init() pygame.display.set_mode((self.width,self.height), OPENGL|DOUBLEBUF) pygame.display.set_caption(self.title) glEnable(GL_CULL_FACE) #... glMatrixMode(GL_MODELVIEW) clock=pygame.time.Clock() whileTrue: dt=clock.tick(self.fps) self.process_input(dt) self.display() Thenewprocess_input()methodupdatesthesceneandtheinstanceattributesby retrievingtheeventsfromtheeventqueueandprocessingthepressedkeys. IfaQUITeventoccursortheEsckeyispressed,thePygameprogramisexecuted. Otherwise,thecamerapositionisupdatedwiththedistanceandangleofrotation, controlledbythearrowkeys: defprocess_input(self,dt): foreventinpygame.event.get(): ifevent.type==QUIT: self.quit() ifevent.type==KEYDOWN: ifevent.key==K_ESCAPE: self.quit() ifevent.key==K_F1: self.light.switch_color() pressed=pygame.key.get_pressed() ifpressed[K_UP]: self.distance-=0.01*dt ifpressed[K_DOWN]: self.distance+=0.01*dt ifpressed[K_LEFT]: self.angle-=0.005*dt ifpressed[K_RIGHT]: self.angle+=0.005*dt self.distance=max(10,min(self.distance,20)) self.angle%=math.pi*2 TheglutSwapBuffers()isreplacedbypygame.display.flip(),andthenewquit() methodquitsPygameandexitsPythongracefully: defdisplay(self): #... self.light.render() self.sphere1.render() self.sphere2.render() pygame.display.flip() defquit(self): pygame.quit() sys.exit() AnotherconsequenceofremovingGLUTisthatwecannotuseglutSolidSphereto renderourspheres. Fortunately,wecansubstituteitwiththegluSphereGLUfunction.Theonlydifferenceis thatweneedtocreateaGLUquadraticobjectfirst,andthencallthefunctionwiththis argumentandtheusualradius,numberofslices,andnumberofstacksintowhichthe sphereisdivided: classSphere(object): slices=40 stacks=40 def__init__(self,radius,position,color): self.radius=radius self.position=position self.color=color self.quadratic=gluNewQuadric() defrender(self): glPushMatrix() glTranslatef(*self.position) glMaterialfv(GL_FRONT,GL_DIFFUSE,self.color) gluSphere(self.quadratic,self.radius, Sphere.slices,Sphere.stacks) glPopMatrix() Withthesechanges,theGLUTAPIisnowcompletelyreplacedbyPygame.Checkoutthe chapter5_04.pyscripttoseethecompleteimplementation. Tip OpenGLandSDL ByincludingPygame,wereplacetheGLUTAPIwithSimpleDirectMediaLayer (SDL),whichisthelibrarythatPygameisbuiltover.Likefreeglut,itisanothercrossplatformalternativetoGLUT. DrawingwithOpenGL Untilnow,wehavealwaysrenderedourobjectswithautilityroutine,butmostOpenGL applicationsrequiretheuseofsomedrawingprimitives. TheCubeclass Wewilldefineanewclasstorendercubes,andwewillusethefollowingrepresentationto betterunderstandthevertices’positions.From0to7,theverticesareenumeratedand representedina3Dspace. Thesidesofacubecannowberepresentedastuples:thebackfaceis(0,1,2,3),the rightfaceis(4,5,1,0),andsoon. Notethatwearrangetheverticesincounterclockwiseorder.Aswewilllearnlater,this willhelpusenableanoptimizationcalledfaceculling,whichconsistsofdrawingonlythe visiblefacesofapolygon: classCube(object): sides=((0,1,2,3),(3,2,7,6),(6,7,5,4), (4,5,1,0),(1,5,7,2),(4,0,3,6)) The__init__methodwillstorethevaluesofthepositionandcolor,aswellasthe vertexcoordinateswithrespecttothecenterpositionofthecube: def__init__(self,position,size,color): self.position=position self.color=color x,y,z=map(lambdai:i/2,size) self.vertices=( (x,-y,-z),(x,y,-z), (-x,y,-z),(-x,-y,-z), (x,-y,z),(x,y,z), (-x,-y,z),(-x,y,z)) Therender()methodpushesanewmatrix,transformsitaccordingtoitscurrentposition, andcallsglVertex3fv()foreachvertexofthesixfacesofcube. TheglVertex3fvtakesalistofthreefloatvaluesthatspecifythevertexposition.This functionisexecutedbetweentheglBegin()andglEnd()calls.Theydelimitthevertices thatdefineaprimitive.TheGL_QUADSmodetreatseachgroupoffourverticesasan independentquadrilateral. Thelaststatementpopsthecurrentmatrixfromthematrixstack: defrender(self): glPushMatrix() glTranslatef(*self.position) glBegin(GL_QUADS) glMaterialfv(GL_FRONT,GL_DIFFUSE,self.color) forsideinCube.sides: forvinside: glVertex3fv(self.vertices[v]) glEnd() glPopMatrix() Enablingfaceculling Eventhoughacubehassixfaces,wecanseeamaximumofonlythreefacesatonce,and onlytwooronefromcertainangles.Therefore,ifwediscardthefacesthatarenotgoing tobevisible,wecanavoidrenderingatleast50percentofthefacesofourcubes. Byenablingfaceculling,OpenGLcheckswhichfacesarefacingthevieweranddiscards thefacesthatarefacingbackwards.Theonlyrequirementistodrawthefacesofthecube inthecounterclockwiseorderofthevertices,whichisthedefaultfrontfaceinOpenGL. Theimplementationpartiseasy;weaddthefollowinglinetoourglEnablecalls: #... glEnable(GL_LIGHTING) glEnable(GL_CULL_FACE) #... Inournextapplication,wewilladdsomecubesandenablefacecullingtoseethis optimizationinpractice. Basiccollisiondetectiongame Withalloftheseingredients,youarenowabletowriteasimplegamethatdetectssimple collisionsbetweenshapes. Thesceneconsistsofaninfinitelane.Blocksappearrandomlyattheendofthelaneand movetowardstheplayer,representedasthesphereinthefollowingscreenshot.Heorshe mustavoidhittingtheblocksbymovingthespherefromrighttoleftinthehorizontalaxis. Thegameisoverwhentheplayer’scharactercollideswithoneoftheblocks. Thisgameplayisdirectanduncomplicated,anditwillallowustodevelopa3Dgame withoutworryingtoomuchaboutmorecomplicatedphysicscalculations. SincewearegoingtoreusetheLight,Cube,andSphereclasses,weneedtodefineanew classonlytorepresentourgameblocks: classBlock(Cube): color=(0,0,1,1) speed=0.01 def__init__(self,position,size): super().__init__(position,(size,1,1),Block.color) self.size=size defupdate(self,dt): x,y,z=self.position z+=Block.speed*dt self.position=x,y,z Itsupdate()methodsimplymovestheblocktowardstheplayerbyupdatingitsz coordinatewithuniformspeed. OurAppclasssetstheinitialvaluesoftheattributesthatwewillneedduringtheexecution ofourgame,andcreatestheLightandthegameobjectinstancesasinourprevious examples: classApp(object): def__init__(self,width=800,height=600): #... self.game_over=False self.random_dt=0 self.blocks=[] self.light=Light(GL_LIGHT0,(0,15,-25,1)) self.player=Sphere(1,position=(0,0,0), color=(0,1,0,1)) self.ground=Cube(position=(0,-1,-20), size=(16,1,60), color=(1,1,1,1)) Thestart()methodhassmallvariations,onlyaddingglEnable(GL_CULL_FACE),aswe mentionedpreviously: defstart(self): pygame.init() #... glMatrixMode(GL_MODELVIEW) glEnable(GL_CULL_FACE) self.main_loop() Themain_loop()methodisnowaseparatemethodandincludestherandomgenerationof blocks,collisiondetection,aswellastheupdatingofthepositionsoftheblocks: defmain_loop(self): clock=pygame.time.Clock() whileTrue: foreventinpygame.event.get(): ifevent.type==QUIT: pygame.quit() sys.exit() ifnotself.game_over: self.display() dt=clock.tick(self.fps) forblockinself.blocks: block.update(dt) self.clear_past_blocks() self.add_random_block(dt) self.check_collisions() self.process_input(dt) Wewillimplementcollisiondetectionbycomparingtheboundariesoftheclosestblocks withtheextremesofthesphere.Sincethesphere’swidthissmallerthantheblocksize,if oneoftheseextremesisbetweentherightandleftboundariesofablock,itwillbe consideredasacollision: defcheck_collisions(self): blocks=filter(lambdax:0<x.position[2]<1, self.blocks) x=self.player.position[0] r=self.player.radius forblockinblocks: x1=block.position[0] s=block.size/2 ifx1-s<x-r<x1+sorx1-s<x+r<x1+s: self.game_over=True print("Gameover!") Topreventthespawningoftoomanyblocks,wedefinedacountercalledrandom_dt.It accumulatestheelapsedtimeinmillisecondsbetweenframes,anditwilltrytospawna newblockonlyifthesumisgreaterthan800milliseconds: defadd_random_block(self,dt): self.random_dt+=dt ifself.random_dt>=800: r=random.random() ifr<0.1: self.random_dt=0 self.generate_block(r) defgenerate_block(self,r): size=7ifr<0.03else5 offset=random.choice([-4,0,4]) self.blocks.append(Block((offset,0,-40),size)) Ifthegeneratedrandomnumberislowerthan0.1,anewblockisaddedtotheblocklist andtherandom_dtcounterisresetto0.Inthisway,theminimumelapsedtimebetween twoblockscanbe0.8seconds,givingenoughtimetoleaveatolerabledistancefromone blocktoanother. Anotheroperationthatthemainloopperformsisremovingtheblocksthatarelocated behindthecameras’viewingarea,avoidingthecreationoftoomanyBlockinstances: defclear_past_blocks(self): blocks=filter(lambdax:x.position[2]>5, self.blocks) forblockinblocks: self.blocks.remove(block) delblock Thecodefordisplayingthegameobjectsstaysassuccinctasusual,thankstothetransfer ofthedrawingprimitivestotherespectiverender()methods: defdisplay(self): glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) glLoadIdentity() gluLookAt(0,10,10, 0,0,-5, 0,1,0) self.light.render() forblockinself.blocks: block.render() self.player.render() self.ground.render() pygame.display.flip() Tofinishourgame,wewillmodifytheinputhandlingofourprogram.Thischangeis straightforward,sinceweonlyneedtoupdatethexcomponentofthecharacter’sposition andtrimitsothatitcannotmoveoutofthelane: defprocess_input(self,dt): pressed=pygame.key.get_pressed() x,y,z=self.player.position ifpressed[K_LEFT]: x-=0.01*dt ifpressed[K_RIGHT]: x+=0.01*dt x=max(min(x,7),-7) self.player.position=(x,y,z) Inthechapter5_05.pyscript,youcanfindthefullimplementationofthegame.Runit andfeelfreetomodifyandimproveit!Youcanaddpickupitemsandkeeptrackofthe score,orgivetheplayeranumberoflivesbeforethegameisover. Summary Inthischapter,youlearnedhowitispossibletoworkwithPythonandOpenGL,andwith basicknowledgeaboutOpenGLAPIs,wewereabletodevelopasimple3Dgame. Wesawtwocross-platformalternativesforcreatinganOpenGLcontext:GLUTand Pygame.Youcandecidewhichonebettersuitsyour3Dgames,dependingonthetradeoffsofeachoption.Keepthisinmind:anadvantageofusingbothisthatyoumayadapt existingexamplesfromonelibrarytotheother! Withthesefoundationsof3Dcovered,inthenextchapter,wewillseehowtodevelopa 3Dplatformerbasedonthesetechnologies. Chapter6.PyPlatformer Sofar,youhavelearnedhowtoimplementgamesinPythonwithdifferentlibraries,with theprimaryfocusbeingonthepracticeofgamedevelopment. Now,wewillintroducesometheoreticalconceptsthatwillnotonlycomplementthe practice,butwillalsohelpusdevelopgamesefficiently.Thistheorywillaidusin understandingwhygamesareconceptuallydesignedthewaytheyare. Inthischapter,wewillcoverthesetopics: Foundationsofthegametheory Object-orientedprinciplesappliedtogamedevelopment Howtoimplementasmall3Dgameframework Modularizingourfunctionalitywithreusablecomponents Addingaphysicsenginetosimulaterigidbodies’interactions Creatingthebuildingblocksofaplatformergame Anintroductiontogamedesign Thereareseveralacademicdefinitionsofwhatagameis;however,mostofthemsharethe keyterms,suchasrules,objectives,andplayers.Assumingthatallgamessharethese concepts,wemayasksomeinterestingquestionswhileanalyzingagame:whatisthe game’smainobjective?Whataretherulesthattheplayermustfollow?Isitdifficultto recognizethegoalandtherulesofthesystem? Otherdefinitionsmakereferencestoconceptssuchasresourcemanagementand inefficiencies,becausethedecisionsoftheplayerareusuallyconditionedbythe limitationsofsomeusefultokensinthegame. Thedecisionswemakewhenwecreateagamearedeeplyrelatedwiththeseconcepts,and aswewillseelater,itisagoodexercisetothinkaboutthemevenwhenwearestarting withthedevelopment. Leveldesign Inordertosuccessfullyengageourplayers,ourgameneedstograduallyaddnew challengesthatpreservetheirinterest.However,theseingredientsshouldbeintroducedin acoherentordersothattheplayerdoesnotbecomeconfusedbecausesheorhedoesnot knowhowtoreacttothegame’soutput. Thismeansthattheplayerisnotonlyplayingyourgamebutalsolearninghowtoplayit. Thislearningcurveshouldbecarefullyconsideredintutorialsandthefirstlevel,because incorrectguidancecanleadtofrustration. Platformerskills Inourplatformergame,thefirstactionthattheplayerwilllearnishowtomovetheir character,whichisintuitivelyperformedbypressingthearrowkeys.Sincethereisnotany threatnearby,theplayercanexperimentmovingaroundandcanbecomefamiliarwiththe controlkeys. Next,theplayerwillfaceanobstacle,andsheorheneedstolearnhowtojumpoveritto continue.Thereisnogapbetweentheobstacleandtheground,sothereisnoriskof fallingfromtheplatformifthecharacter’sjumpistooshort,asshowninthefollowing screenshot: Oncethefirstobstacleiscleared,theplayerfacesacoupleofplatformswithgapsin between,asshowninthenextscreenshot.Nowitisrequiredtomeasurethejumping forces,orelsethecharacterwillfallintothevoid. Ifthecharacterdoesnotreachtheplatformandfalls,itwillrespawnattheinitialposition. That’sanotherlessonthattheplayerwilllearn:movewithcare,orelseyouwillhaveto startoveragain! Onthenextplatform,theplayerwillencounteranewcharacter.Ifthecharactercollides withit,itwillmovebacktothespawnposition.Therefore,theplayerlearnsthatthese objectsshouldbeavoidedandtheymustjumpoverittoadvance. Afterthis,theplayercanreachaplatformonwhichthereisaspinningbox.Itisplacedin themiddleofanarrowplatform,soitisveryeasytocollidewith,asshowninthis screenshot: Whenthishappens,theplayer’sammoisincreased,sonowthecharacterisabletoshoot bypressingthespacebar.Onthenextplatform,theplayercanfindanotherenemytotest theirshootingability.Withthepickup,theplayercanshootuptofivetimes,sotheycan tryoutthisabilityseveraltimesbeforerunningoutofammo. Nowthatwehaveanalyzedtheskillsthatourplayerswillneedtolearn,wecanmoveon tothearchitecturaldesignofourgame. Component-basedgameengines Whenwestartdevelopingagamefromscratch,thefirststepmaybetodefineabasic classwithcommonattributesforallthegameobjects,suchasitspositioncoordinates,the color,thespeed,andsoon.ThiscouldbethebasicGameObjectorActorclasswedeclared inpreviouschapters. Thenyouneedtoaddothergameobjectswithmoreconcretekindsofbehavior,suchas thecharacterthatwillbecontrolledbytheplayer,ortheenemiesthatrandomlyshootthe playablecharacter.Ifyourepresenttheseentitieswithseparateclasses,eachone implementsthatfunctionalitywithacustommethodinthecorrespondingclass. Consequently,everyspecializationofanexistingentitymightbetranslatedintoanew subclass.Supposewewanttoaddaspecialtypeofenemythatmovesalongaparticular route,whichwewillcallPatrolEnemy.Inourclassdiagramshownhere,thisclasswill extendourEnemyclass: Whilethisapproachmightworkforsmallgames,itbecomesmoredifficulttoscalethe organizationoftheprojectwhentheinheritancehierarchygrows.Imaginethatwewantto addanothertypeofenemythatshootsonlywhenitislocatedwithinacertaindistance fromtheplayer,andathirdenemythatcombinesthisnewbehaviorwithPatrolEnemy. Thisisillustratedinthefollowingdiagram: Ifsomegameobjectsfollowthebehavioroftheirancestorsandsharethefunctionality withotherentities,youmayneedtousemultipleinheritanceordefineintermediateclasses thatwouldbeunnecessaryotherwise. Ontheotherhand,acomponent-baseddesignfollowstheprincipleoffavoringobject compositionoverclassinheritance.Thismeansthatthefunctionalityiscontainedinother classes,insteadofreusingitwithasubclass. Translatedintoourexample,thisarchitectureleavesuswiththefollowingdiagram. Besides,aswewillseelater,acomponentbaseclassisalsoadded. TheclassesundertheGameObjectsareaareGameObjectsubclasses,buttheirbehavioris determinedbythecompositionoftheirComponentssubclasses. Inthisway,wedonotintroducemultipleinheritancesolutionsandkeepthesepiecesof functionalityseparatedinsmallclasses,withtheadvantageofbeingabletoaddorremove themdynamically. Inlatersections,wewillseehowthesecomponentsarealsousedtorenderobjectsinour OpenGLcontext,butnowwewillmoveontothelibrarythatwewillusetosimulate physicsinourgame. IntroducingPymunk WewillimplementphysicsinourgamewithPymunk,a2Dphysicsenginebuiltontopof Chimpunk2D. EventhoughweareusingOpenGLfor2Dgraphics,ourplatformergamewillrecreatea two-dimensionalspace,sowewillworkontheplaneinwhichthezaxisequals0. YoucaninstallPymunkdirectlyfrompip: $pipinstallpymunk TheChimpunk2DlibraryiswritteninC,soyoumightneedtocompileitonyourplatform ifthedistributiondoesnotshipwiththeprecompiledlibrary.On32-bitWindowsand32bitand64-bitLinuxversions,PymunkwillincludetheChimpunk2Dbinaries. Forotherplatforms,orifyouwanttocompilethelibraryyourself,youcancheckoutthe installationguideandthestepsforcompilingChimpunk2Dat http://pymunk.readthedocs.org/en/latest/installation.html. Tip Don’treinventthewheel! Implementingarigidbodylibraryisalaborioustask,especiallyifyouaredevelopinga casualgamefromscratch—aswearedoinginthischapter. Fortunately,thePymunkAPIissimpleandwell-documented,soyoucangetstarted quicklyandavoidlosingtimecraftingacustomphysicsengine. Themainclassesofthepymunkpackagearethefollowing: Space:Atwo-dimensionalspaceinwhichthephysicssimulationwilloccur.Wewill useaSpaceinstancefortheentiregame,andwewillupdateeachframethroughits step()method. Body:Thisrepresentstherigidbody,thebasicunitofsimulation.Ithasaposition member,whichwillalsorepresentthepositionofthegameobjectthatcontainsthe rigidbody.Theothermembersthatwewillcoverarethevelocityandforce vectors. Shape:Thebaseclassforallshapes.Wewilllimitourselvestocircleandbox shapes,definedbytheCircleandPolyclassesrespectively. Arbiter:Thisrepresentsacollisionpairbetweenshapesandisusedincollision callbacks.Italsocontainsinformationaboutthecontactpointsofthecollision. Inthenextsection,wewillrelyontheseclassesandtheirattributestoholdinformation aboutgameobjectsandtheircollisions. Buildingagameframework TheComponentandGameObjectclassesformthecoreofourgameengine.Theinterface offeredbytheComponentclassisaseasyasthis: classComponent(object): __slots__=['gameobject'] defstart(self): pass defupdate(self,dt): pass defstop(self): pass defon_collide(self,other,contacts): pass Thesemethodsdefinethelifecycleofourcomponents: start():Thisiscalledwhenthecomponentisaddedtothegameobjectinstance.The componentkeepsareferencetothegameobjectasself.gameobject. update(dt):Thisiscalledforeachframe,wherethedtargumentistheelapsedtime insecondssincethepreviousframe. on_collide(other,contacts):Thisiscalledwhenthecomponent’sgameobject collideswithanotherrigidbody.Theotherargumentistheothergameobjectofthe collisionpair,andcontactsincludesthecontactpointsbetweentheobjects.Thislast argumentisdirectlypassedfromthePymunkAPI,andwewillseelaterhowtowork withit. stop():Thisiscalledwhenthecomponentisremovedfromthegameobjectinstance. ThesewillbeinvokedfromourGameObjectclass.Aswementionedearlier,agameobject internallyusesarigidbodytorepresentitspositioninthexandyaxesandanextra variableforthezaxis.Bydefault,wewillworkinthez=0plane. TheGameObjectclassisdefinedasfollows: importpymunkaspm classGameObject(object): instances=[] def__init__(self,x=0,y=0,z=0,scale=(1,1,1)): self._body=pm.Body() self._body.position=x,y self._shape=None self._ax=0 self._ay=0 self._z=z self.tag='' self.scale=scale self.components=[] GameObject.instances.append(self) Thetwo-dimensionalpositioniscomplementedwithaninner_zmember,andtherotation isextendedwiththeangleofrotationinthexandyaxiswiththe_axand_aymembers, respectively.Thevelocityvectorisdirectlyretrievedfromtherigidbody: @property defposition(self): pos=self._body.position returnpos.x,pos.y,self._z @position.setter defposition(self,pos): self._body.position=pos[0],pos[1] self._z=pos[2] @property defrotation(self): returnself._ax,self._ay,self._body.angle @rotation.setter defrotation(self,rot): self._ax=rot[0] self._ay=rot[1] self._body.angle=rot[2] @property defvelocity(self): returnself._body.velocity @velocity.setter defvelocity(self,vel): self._body.velocity=vel Thegameobject’spositioncanbemodifiedbyapplyinganimpulseoraforcevectorto theunderlyingrigidbody: defmove(self,x,y): self._body.apply_impulse((x,y)) defapply_force(self,x,y): self._body.apply_force((x,y)) Theinstancecomponentscanbemanipulated—added,removed,andretrievedbytype—in aflexiblewaywiththefollowingmethods: defadd_components(self,*components): forcomponentincomponents: self.add_component(component) defadd_component(self,component): self.components.append(component) component.gameobject=self component.start() defget_component_by_type(self,cls): forcomponentinself.components: ifisinstance(component,cls): returncomponent defremove_component(self,component): component.stop() self.components.remove(component) Todisplaythegameobject,wepayspecialattentiontotheRenderablecomponents.The interfaceofthisComponentsubclassincludearender()method,whichiscalledinthe displayloopforeachactivegameobject. Theupdate()methoddirectlydelegatestheexecutiontoeveryattachedcomponent: defrender(self): forcomponentinself.components: ifisinstance(component,Renderable): component.render() defupdate(self,dt): forcomponentinself.components: component.update(dt) Finally,whenweremoveagameobject,weneedtodeletetheshapeofitsrigidbodyfrom thePymunkspace;otherwise,thevisualresultwillbesuchthattheentitiesstillkeep collidingwithaninvisibleobject: defremove(self): forcomponentinself.components: self.remove_component(component) ifself._shapeisnotNone: Physics.remove(self._shape) GameObject.instances.remove(self) defcollide(self,other,contacts): forcomponentinself.components: component.on_collide(other,contacts) Youcanfindtheimplementationofthesetwoclassesinthe pyplatformer/enginecomponents.pyandpyplatformer/engine/__init__.pyscripts. Tip Savingspaceconsumption Bydefault,eachobjectinstancehasaninternaldictionaryforattributestorage.The __slots__classvariabletellsPythontoallocatespaceonlyforeachvariable,saving spaceiftheinstancehasfewattributes. Sincecomponentsaresmallclasses,weaddedthisoptimizationtoavoidinstantiating unnecessarydictionaries. Nowwewillmoveontothephysicsmodule,wheretheInputclassandtheshape componentsaredefined. Addingphysics ThismoduleisresponsibleforinitializingthePymunkSpace.Thishappensatthe beginningofthefile,andthegravityandthecollisionhandleraresettodefaultvalues: importpymunk defcoll_handler(_,arbiter): iflen(arbiter.shapes)==2: obj1=arbiter.shapes[0].gameobject obj2=arbiter.shapes[1].gameobject obj1.collide(obj2,arbiter.contacts) obj2.collide(obj1,arbiter.contacts) returnTrue space=pymunk.Space() space.gravity=0,-10 space.set_default_collision_handler(coll_handler) Thecollisionhandlerreceivesthespacewherethecollisionoccursasthefirstparameter andanArbiterinstancewiththecollisioninformation.Here,weobtainthegameobject attachedtotheshapeandtriggerthecollide()method. ThePhysicsclassissimilartoInput,sinceitcontainsacoupleofclassmethodsthat wrapthebasicfunctionalitywearelookingfor: classPhysics(object): @classmethod defstep(cls,dt): space.step(dt) @classmethod defremove(cls,body): space.remove(body) TheRigidbodyclassisabasecomponentthatreplacestheinitialstaticbodythatevery gameobjecthaswithanon-staticone.Italsoaddsareferencetothegameobjectfromthe shape,soitispossibletoretrievethegameobjectfromthePymunkcollisioninformation: classRigidbody(Component): __slots__=['mass','is_static'] def__init__(self,mass=1,is_static=True): self.mass=mass self.is_static=is_static defstart(self): ifnotself.is_static: #Replacethestaticbody pos=self.gameobject._body.position body=pymunk.Body(self.mass,1666) body.position=pos self.gameobject._body=body defadd_shape_to_space(self,shape): self.gameobject._shape=shape shape.gameobject=self.gameobject ifself.is_static: space.add(shape) else: space.add(self.gameobject._body,shape) Note Notethatstaticbodiesarenotaddedtothespace;onlytheirshapesare. TheBoxColliderandSphereCollidersubclassescreatethecorrespondingshapeby callingthePymunkAPI: classBoxCollider(Rigidbody): __slots__=['size'] def__init__(self,width,height,mass=1,is_static=True): super(BoxCollider,self).__init__(mass,is_static) self.size=width,height defstart(self): super(BoxCollider,self).start() body=self.gameobject._body shape=pymunk.Poly.create_box(body,self.size) self.add_shape_to_space(shape) classSphereCollider(Rigidbody): __slots__=['radius'] def__init__(self,radius,mass=1,is_static=True): super(SphereCollider,self).__init__(mass,is_static) self.radius=radius defstart(self): super(SphereCollider,self).start() body=self.gameobject._body shape=pymunk.Circle(body,self.radius) self.add_shape_to_space(shape) Eachimplementationonlydiffersintheinformationthatispassedtobuildtheshape,but youcaneasilyaddanotherrigidbodycomponentbyfollowingthesamepattern. Renderablecomponents ThecomponentsmoduleincludesthedefinitionoftheRenderableclass,whichwe mentionedearlierwhenwelookedathowtodisplaygameobjects. Itpushesamatrixtothecurrentstackandperformssomebasicoperationsbefore delegatingtherenderingtothe_render()method,savingusfromrepeatingthis boilerplatecode: classRenderable(Component): __slots__=['color'] def__init__(self,color): self.color=color defrender(self): pos=self.gameobject.position rot=self.gameobject.rotation scale=self.gameobject.scale glPushMatrix() glTranslatef(*pos) ifrot!=(0,0,0): glRotatef(rot[0],1,0,0) glRotatef(rot[1],0,1,0) glRotatef(rot[2],0,0,1) ifscale!=(1,1,1): glScalef(*scale) ifself.colorisnotNone: glColor4f(*self.color) self._render() glPopMatrix() def_render(self): pass TheCubeandSpheresubclassesareanadaptationoftheimplementationwecoveredin Chapter5,PyGameand3D,andalongwiththeLightclass,theyhavebeenomittedfor brevity. TheCameracomponent YoumayrememberhowtosetupthecameraperspectivewithGLU,whichwasoneofthe firstactionsofthedisplayloopinthepreviouschapter. Nowwewilluseacomponenttoperformthesametask,butitwilldifferfromother componentsinthewayinwhichitiscalledinthemainloop.Thishappensbecausethese matrixoperationsneedtobethefirstonesoftheOpenGLmatrixstackifwewanttosetup thecamerapositioncorrectly: classCamera(Component): instance=None def__init__(self,dy,dz): self.dy=dy self.dz=dz Camera.instance=self defrender(self): pos=self.gameobject.position glLoadIdentity() gluLookAt(pos[0],self.dy,self.dz, pos[0],pos[1],pos[2], 0,1,0) Thecamerawilllookatthegameobjectwithwhichitisattachedandthedistancealong theyandzaxesareparameterizedinits__init__method. Thisimplementationtakesthelastcamerathathasbeeninstantiatedasthecurrentone.In ourgame,wewillonlyuseasinglecameracomponent,andthiscanbeconsidereda commonscenarioinbasicgames. However,ifyouneedtoswitchbetweenmultiplecameras,youcandefineamorecomplex component.Asyoumaynotice,theseimplementationsareverycleanandconcisethanks totheassumptionofthesesimplifications. TheInputManagermodule Tohandleinputevents,wecreateaseparatemodulethatwillwraptheprocessingofour pygameevents.SincewewillregisterkeystrokesandthespecialQUITeventonly,thisclass isquiteuncomplicated: fromcollectionsimportdefaultdict importpygame classInput(object): quit_flag=False keys=defaultdict(bool) keys_down=defaultdict(bool) @classmethod defupdate(cls): cls.keys_down.clear() foreventinpygame.event.get(): ifevent.type==pygame.QUIT: cls.quit_flag=True ifevent.type==pygame.KEYUP: cls.keys[event.key]=False ifevent.type==pygame.KEYDOWN: cls.keys[event.key]=True cls.keys_down[event.key]=True @classmethod defget_key(cls,key): returncls.keys[key] @classmethod defget_key_down(cls,key): returncls.keys_down[key] Notethatwiththisinterface,itisstraightforwardtogetthepressedkeyswithinany component,likethisforinstance: classHorizontalMovement(Component): defupdate(self,dt): direction=Input.get(K_RIGHT)-Input.get(K_RIGHT) self.gameobject.move(dt*direction*5,0) Wecansubtractbooleanvaluesbecauseboolisasubclassofint,andTruecorrespondsto 1,whileFalsecorrespondsto0. TheGameclass TheGameclassisresponsibleforbootstrappingtheapplication—settingtheinitialvalues ofthewindowattributes,initializingtheOpenGLcontext,andenteringthegameloop: classGame(object): def__init__(self,caption,width=800,height=600): self.caption=caption self.width=width self.height=height self.fps=60 defmainloop(self): self.setup() clock=pygame.time.Clock() whilenotInput.quit_flag: dt=clock.tick(self.fps) dt/=1000 Physics.step(dt) self.update(dt) self.render() pygame.quit() sys.exit() Someoftheseinitialoperationshavebeenextractedintothesetup()method,so mainloop()staysclearandunderstandable: defsetup(self): pygame.init() pygame.display.set_mode((self.width,self.height), pygame.OPENGL|pygame.DOUBLEBUF) pygame.display.set_caption(self.caption) glEnable(GL_LIGHTING) glEnable(GL_COLOR_MATERIAL) glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE) glEnable(GL_DEPTH_TEST) glClearColor(0.5,0.7,1,1) glMatrixMode(GL_PROJECTION) aspect=self.width/self.height gluPerspective(45,aspect,1,100) glMatrixMode(GL_MODELVIEW) Theupdate()andrender()callsdelegatetheexecutiontothegameobjectinstances, whichinturn,willinvoketheircomponents’methods: defupdate(self,dt): Input.update() forgameobjectinGameObject.instances: gameobject.update(dt) defrender(self): glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) ifCamera.instanceisnotNone: Camera.instance.render() forgameobjectinGameObject.instances: gameobject.render() pygame.display.flip() Thefinalarrangementofourenginepackagelookslikethis: Thepyplatformer/game.pyscriptcontainsthegamelogic,andthankstothemicroframeworkthatwehavedeveloped,itwillconsistofadozenshortclassesonly. DevelopingPyPlatformer Withthisarchitecturalbackground,wecanstartcraftingthecustomcomponentsofour platformergame.TheComponentAPImaylooksimple,butitallowsustoimplementthe functionalityinasuccinctmanner. Creatingtheplatforms Eachplatformofourgameisnothingbutacubewithastaticrigidbody.However,since wewillcreateseveralinstanceswiththesecomponents,itisconvenienttodefineaclassto avoidrepeatingthiskindofinstantiation: classPlatform(GameObject): def__init__(self,x,y,width,height): super(Platform,self).__init__(x,y) color=(0.2,1,0.5,1) self.add_components(Cube(color,size=(width,height,2)), BoxCollider(width,height)) Addingpickups Inplatformergames,itiscommonthattheplayerisabletocollectsomeitemsthatgive valuableresources.Inourgame,whenthecharacterhitsoneofthesepickups,itsammois incrementedbyfiveunits. Todecoratethistypeofgameobject,wewilladdacomponentthatrotatestheattached gameobjectarounditsyaxis: classRotating(Component): speed=50 defupdate(self,dt): ax,ay,az=self.gameobject.rotation ay=(ay+self.speed*dt)%360 self.gameobject.rotation=ax,ay,az Finally,wecandefineaGameObjectsubclassthatwrapstheinstantiationofthese components: classPickup(GameObject): def__init__(self,x,y): super(Pickup,self).__init__(x,y) self.tag='pickup' color=(1,1,0.5,1) self.add_components(Cube(color,size=(1,1,1)), Rotating(),BoxCollider(1,1)) Weusethetagattributeasawayofidentifyingitstype.Thus,ifweneedtocheckthe gameobject’stype,wedon’tneedtorelyontheisinstancefunctionandthehierarchy model. Shooting! Whentheplayercollideswithoneofthesepickups,itisdestroyedanditisnolonger displayedonthescene.Withaphysicsengine,thisalsomeansremovingthegameobject anddisablingitsrigidbody.RememberthatwedefinedtheGameObject.remove()method forthispurpose. Tosimulatethegradualdisappearanceoftheobject,wewilldecreaseitsscaleuntilitdoes notbecomevisible,andthenwewillremoveit: classDisappear(Component): defupdate(self,dt): self.gameobject.velocity=0,0 s1,s2,s3=map(lambdas:s-dt*2, self.gameobject.scale) self.gameobject.scale=s1,s2,s3 ifs1<=0:self.gameobject.remove() Thiscomponentisattacheddynamicallytothepickupandforcesitsremoval.Theability toshootandcollecttheseitemsisdefinedintheShootercomponent: classShoot(Component): defon_collide(self,other,contacts): self.gameobject.remove() classShooter(Component): __slots__=['ammo'] def__init__(self): self.ammo=0 defupdate(self,dt): ifInput.get_key_down(K_SPACE)andself.ammo>0: self.ammo-=1 d=1ifself.gameobject.velocity.x>0else-1 pos=self.gameobject.position shoot=GameObject(pos[0]+1.5*d,pos[1]) shoot.tag='shoot' color=(1,1,0,1) shoot.add_components(Sphere(0.3,color),Shoot(), SphereCollider(0.3,mass=0.1, is_static=False)) shoot.apply_force(20*direction,0) defon_collide(self,other,contacts): ifother.tag=='pickup': self.ammo+=5 other.add_component(Disappear()) Eachtimetheplayershoots,agameobjectisinstantiatedandmovestowardsthecurrent gameobject’sdirection.IthasanauxiliaryShootcomponent.Thiscomponentremoves theshootinstancewhenitcollideswithotherrigidbodies. Todecidewhichentitiesareaffectedbyaplayer’sshooting,weaddacomponentthat checksthecollidingentity’staganddisappearsifitisashoot: classShootable(Component): defon_collide(self,other,contacts): ifother.tag=='shoot': self.gameobject.add_component(Disappear()) Noweachenemyhasthefollowingcomponents: classEnemy(GameObject): def__init__(self,x,y): super(Enemy,self).__init__(x,y) self.tag='enemy' color=(1,0.2,0.2,1) self.add_components(Sphere(1,color),Shootable(), SphereCollider(1,is_static=False)) ThePlayerclassanditscomponents ApartfromtherigidbodyandtheShooter,ourplayercharacterhastwomain components: Respawn:Thisspawnstheplayerinitsinitialpositionifitfallsintothevoidor collideswithanenemy.ThefollowingisanexampleoftheRespawnclass: classRespawn(Component): __slots__=['limit','spawn_position'] def__init__(self,limit=-15): self.limit=limit self.spawn_position=None defstart(self): self.spawn_position=self.gameobject.position defupdate(self,dt): ifself.gameobject.position[1]<self.limit: self.respawn() defon_collide(self,other,contacts): ifother.tag=='enemy': self.respawn() defrespawn(self): self.gameobject.velocity=0,0 self.gameobject.position=self.spawn_position PlayerMovement:Thisqueriestheinputstateandcheckswhatforcescanbeapplied tothecharacter’srigidbodytomoveithorizontallyormakeitjump.Hereisan exampleofthePlayerMovementclass: classPlayerMovement(Component): __slots__=['can_jump'] def__init__(self): self.can_jump=False defupdate(self,dt): d=Input.get_key(K_RIGHT)-Input.get_key(K_LEFT) self.gameobject.move(d*5*dt,0) ifInput.get_key(K_UP)andself.can_jump: self.can_jump=False self.gameobject.move(0,8) defon_collide(self,other,contacts): self.can_jump|=any(c.normal.y<0forcincontacts) Note Notehowweusethelistofcontactpointstocheckwhethertheplayerhastouched theground,andthenenablethejumpmovementagain. ThePlayerclasscombinesallofthesecomponentsintoasingleentity: classPlayer(GameObject): def__init__(self,x,y): super(Player,self).__init__(x,y) self.add_components(Sphere(1,(1,1,1,1)), PlayerMovement(),Respawn(), Shooter(),Camera(10,20), SphereCollider(1,is_static=False)) ThePyPlatformerclass Finally,wedefineaGamesubclassthatinstantiatesallofourgameobjectsandplacesthem inthescene: classPyPlatformer(Game): def__init__(self): super(PyPlatformer,self).__init__('PyPlatformer') self.player=Player(-2,0) self.light=GameObject(0,10,0) self.light.add_component(Light(GL_LIGHT0)) self.ground=[ #Platform1 Platform(3,-2,30,1), Platform(-11,3,2,9), Platform(8,0,2,3), #Platform2&3 Platform(23,0,6,1), Platform(40,2,24,1), #Platform4&5 Platform(60,3,8,1), Platform(84,4,26,1) ] self.pickup=Pickup(60,5) self.enemies=[Enemy(40,4),Enemy(90,6)] if__name__=='__main__': game=PyPlatformer() game.mainloop() Youcancheckoutallthegameobjectsandcomponentsinthepyplatformer/game.py script. Summary Inthischapter,youlearnedaboutthebenefitsofacomponent-baseddesignandhowit allowsyoutobuildsmallpiecesthatcanbeadded,removed,andcombinedwithseveral gameobjects. Notethatthemostimportantexerciseofthischapterisnothowtoimplementaplatformer game,buthowitispossibletosetupacomponent-basedframeworkandprovidethebasic buildingblocksofagame. Withthesefoundations,youcanaddsomebasicfunctionality,suchasmovingtheenemies andtheplatforms,displayingmoreinformationabouttheammo,orkeepingtrackofa scorebasedonthenumberofliveslostandenemiesdestroyed. Asusual,thefinalversionofthischapter’sgameisnothingbutthestartingpointofa morecomplexapplication! Inthenextchapter,wewillinteractwithareal-wordcheckersgameviaawebcam.This applicationwillbedevelopedwithOpenCV,across-platformcomputervisionlibrary. Chapter7.AugmentingaBoardGame withComputerVision Computervisionisthescienceandengineeringofsmartcamerasystems(ormorebroadly, smartimagesystems,sinceimagescancomefromanothersourcebesidesacamera). Examplesofsubtopicsincomputervisionincludefacerecognition,licenseplate recognition,imageclassification(asusedinGoogle’sSearchbyimage),motioncapture (asusedinXboxKinectgames),and3Dscanning. Computervision,likegamedevelopment,hasbecomemoreaccessibleinrecentyearsand isnowalmostaubiquitoustopic.“Howcanweleveragepeople’sinterestincameras?”or “Howcanweleverageallthecamerasthatareinourbuilding,ourcity,orourcountry?”is asnaturalaquestionas“Howcanweleveragepeople’sinterestingamesand simulations?”andtheimplicationsextendbeyondentertainment. Camerasareeverywhere.Manyofthemareattachedtopowerfulhostcomputersand networks.Welive,work,andplayamidstanarmyofdigitaleyes,includingwebcams, cameraphones,camera-controlledgameconsolesandsmartTVs,securitycameras, drones,andsatellites.Imagesofyoumaybecaptured,processed,ortransmittedmany timesdaily. ThesongwriterJohnLennonaskedusto“imagineallthepeoplelivingfortoday”but,for amoment,let’simagineallpixelsinstead.Asingleimagemaycontainmillionsofpixels, amountingtomorebytesofdatathanLeoTolstoy’sWarandPeace(anepic1500-page novelaboutRussiansocietyduringtheNapoleonicWars).Asinglecameramaycapturea videostreamcontainingthousandsoftheseepic-sizedimagesperminute.Billionsof camerasareactiveintheworld.Networksanddiskdrivesarecongestedwiththe accumulationofimagedata,andonceanimageisonline,copiesofitmayremainin circulationindefinitely.Whenweusesoftwaretoacquire,edit,analyze,andreview streamsofimages,wemaynoticethatthecomputer’sCPUusagesoarswhileitsbattery powerplummets. Asgamedevelopers,weknowthatagoodgameenginesimplifiesalotofoptimization problems,suchasbatchingspritesor3DmodelstosendtotheGPUforrendering.Good computervisionlibraries(andmorebroadly,goodnumericandscientificlibraries)also simplifyalotofoptimizationproblemsandhelpusconserveCPUusageandbatterylife whileprocessingvideoinput(orotherlargestreamsofdata)inrealtime.Sincethe1990s, computervisionlibraries,likegameengines,havebecomemorenumerous,faster,easier touse,andoftenfree.Thischapterleveragesthefollowinglibrariestocapture,process, anddisplayimages: NumPy:Thisisanumericlibrary,whichwepreviouslyusedinChapter4,Steering Behaviors.Itsdocumentationisathttp://docs.scipy.org/doc/. OpenCV:Thisisacross-platformlibraryforcomputationalphotography,computer vision,andmachinelearning.OpenCV’sPythonversionrepresentsimagesasNumPy arrays.However,beneaththePythonlayer,OpenCVisimplementedinC++code withawiderangeofhardware-specificoptimizations.Thankstotheseoptimizations, OpenCVfunctionstendtorunfasterthantheirNumPyequivalents.TheOpenCV documentationisathttp://docs.opencv.org/. scikit-learn:Thisisamachinelearninglibrary.ItprocessesNumPyarrays.Forits documentation,refertohttp://scikit-learn.org/stable/documentation.html. WxPython:Thisisacross-platformGUIframework.Oneachplatform,WxPython iscertaintohaveanativelookandfeelbecauseitusesnativeGUIwidgets(in comparison,manycross-platformGUIframeworks,suchasTkinter,usetheirown non-nativewidgetswithaconfigurable“skin”thatmayemulateanativelook). WxPythonisawrapperaroundaC++librarycalledWxWidgets.ForWxPython’s documentation,refertohttp://wxpython.org/onlinedocs.php. Note Laterinthischapter,intheSettingupOpenCVandotherdependenciessection,we willdiscusstheversionrequirementsandsetupstepsforeachlibrary. Followingthepatternofotherchaptersinthisbook,wewillapplycomputervisiontoa classicgame:checkers,alsoknownasdraughts.Thisboardgamehasmanyvariantsfrom allculturesandallperiodsofhistoryinthepast5,000years.Itisthegrandfatherofall strategygames.Acrossmostvariants,thegamehasthefollowingfeatures: Therearetwoplayers,whoaresometimescalledlightanddark. Therearetwokindsofplayingpieces,sometimescalledpawnsandkings.Apawnis ashort,circularplayingpiece.Akingisatall,circularplayingpiecemadeby stackingtwopawns. Theboardisagridofalternatinglightanddarksquares.Piecesmayonlyoccupythe darksquares.Thesizeofthegriddependsonthevariantofthegame. Atthestartofthegame,eachplayerhasanarmyofpawns,occupyingmultiple adjacentrowsononesideoftheboard.Thetwoarmiesstartonoppositesidesofthe boardwithtwovacantrowsbetweenthem. Apiecemaycaptureanopposingpiecebyjumpingoveritintoanunoccupiedsquare. Apiecemaymakemultiplejumpsinoneturn. Onreachingthefarthestrow,apawnispromotedtoaking. Thedifferencebetweenapawnandakingdependsonthevariant.Insomevariants, onlyakingcanmovebacktowarditsstartingside.Moreover,insomevariants,a pawncancrossonlyoneunoccupiedsquareatatimewhileakingcancrossmultiple unoccupiedsquares.Thelatterarecalledflyingkings. Note Fordescriptionsofmanyinternationalvariantsofcheckers,ordraughts,referto https://en.wikipedia.org/wiki/Draughts. Wewillbuildanapplicationthatmonitorsareal-worldgameofcheckersviaawebcam. Theapplicationwilldetectacheckerboardandclassifyeachsquareasanemptysquare,a lightpawn,alightking,adarkpawn,oradarkking.Furthermore,theapplicationwill alterthewebcam’simagestocreateabird’s-eyeviewoftheboard,withlabelstoshowthe resultsoftheclassification.Thisisasimplecaseofaugmentedreality,meaningthatthe applicationappliesspecialeffectsandannotationstoareal-timeviewofareal-world object.Wewillcallthisapplication,quitesimply,Checkers. Note Thecompletedprojectforthischaptercanbedownloadedfrom http://nummist.com/opencv/4507_07.zip,andanyFAQanderratacanbefoundat http://nummist.com/opencv. PlanningtheCheckersapplication Let’sgivefurtherthoughttothereal-worldscenethatourCheckersprogramwillexpect andthevirtualsceneitwillcreate.Considerthefollowingclose-upphotograph,showing partofan8x8checkerboard.Fromthetop-leftcornertoright-handside,weseealight king,alightpawn,adarkpawn,andadarkking. Thischeckerboardisjustasheetofmattepaperonwhichblackandwhitesquaresare printed(thepaperisgluedtoafoamboardtomakeitrigid).Theplayingpieceshappento bepokerchips—redchipsforthedarksideandgraychipsforthelightside.Astackof twopokerchipsisapawn,whileastackoffourisaking.Asthisexamplesuggests, peoplemayplaycheckerswithhomemadeorimprovisedsets.Thereisnoguaranteethat twocheckerssetswilllookalike,sowewilltrytoavoidrigidassumptionsaboutthecolor scheme.However,highcontrastisgenerallyhelpfulincomputervision,andthisexample isidealbecauseitusesfourcontrastingcolorsfordarksquares,lightsquares,darkpieces, andlightpieces. Wewillassumethatalightbordersurroundsthecheckerboard(seetheprecedingimage). Thisassumptionmakesiteasiertodetecttheboard’sedge. Lookcarefullyatthewhitesquarestotheleftofthekings.Sincethekingsaretallerthan thepawns,thekingscastlongershadowsintoadjacentlightsquares.Wewillrelyonthis observationtodifferentiatebetweenpawnsandkings.Importantly,inabird’s-eyeviewof theboard,theheightsoftheplayingpieceswillnotbevisiblebuttheshadowswillbe.We willassumethattheshadowsareapproximatelyorthogonal(notdiagonal),andare sufficientlylongtoreachanadjacentsquare. Tofurthersimplifyourcomputervisionwork,wewillrequirethatthecameraand checkerboardremainstationaryrelativetoeachother.Thecamerashouldhaveaviewof theentireboardplusasmallmargin.Toachievethis,thewebcamwillneedtobe approximately2feet,or0.6meters,awayfromtheboard(however,theexactrequirement willvarydependingonthewebcam’sfieldofviewandtheboard’ssize).Asseeninthe followingphotograph,atripodisagoodwaytokeepthewebcamstationaryinahigh positionfromwhereitcanviewthewholeboard: Asthefollowingscreenshotshows,ourCheckersapplicationwilldisplayboththe unmodifiedcameraview(left)andanidealizedbird’s-eyeview(right): Thebird’s-eyeviewcontainsannotations,orinotherwords,textforshowingthe classificationresults.Theannotation1meansadarkpawn,11adarkking,2alightpawn, and22alightking.Alackofannotationmeansanemptysquare. NotetheGUIwidgetsatthebottomofthescreenshot.Thesecontrolswillenabletheuser toadjustcertainparametersofourcheckerboarddetectorandsquareclassifier. Specifically,thefollowingadjustmentswillbesupported: RotateboardCCW:Clickonthisbuttontorotatethebird’s-eyeview90degrees counterclockwise(CCW) FlipboardX:Clickonthisbuttontoflipthebird’s-eyeviewhorizontally FlipboardY:Clickonthisbuttontoflipthebird’s-eyeviewvertically Shadowdirection:Selectoneoftheradiobuttons—up,left,down,orright—to specifythedirectionofthekings’shadowsinthebird’s-eyeview Emptythreshold:Movethisslidertothelefttoclassifymoresquaresasnonempty, ortotherighttoclassifymoresquaresasempty Playerthreshold:Movethisslidertothelefttoclassifymorepiecesaslight,orto therighttoclassifymorepiecesasdark Shadowthreshold:Movethisslidertothelefttoclassifymorepiecesaskings,orto therighttoclassifymorepiecesaspawns WewilldivideourimplementationoftheCheckersapplicationintosixPythonmodules: Checkers.py:ThismoduleimplementstheGUIusingWxPython. CheckersModel.py:ThismoduleusesOpenCV,NumPy,andscikit-learntocapture, analyze,andaugmentimagesofacheckersgameinrealtime. WxUtils.py:ThismoduleprovidesautilityfunctiontoconvertimagesfromOpenCV formatstoadisplayableWxPythonformat.Theimplementationincludesa workaroundforaWxPythonbugthataffectsfirst-generationRaspberryPi computers. ResizeUtils.py:UsingOpenCV,thismoduleprovidesautilityfunctiontotrytoset acamera’sresolutionandreturntheactualresolution. ColorUtils.py:UsingNumPy,thismoduleprovidesutilityfunctionstoextracta colorchannel(suchastheredcomponentofanimage)andquantifydifferencesthe betweencolors. CVBackwardCompat.py:ThismoduleprovidesaliasessothatthingsinOpenCV2.x appeartohavethesamenamesastheirequivalentsinOpenCV3.x. Beforewewriteanyofthesemodules,let’ssetupthedependencies. SettingupOpenCVandother dependencies Thischapter’sprojectwillworkwithOpenCV2.xor3.xandPython2.7or3.4.Notethat OpenCV2.xdoesnotsupportPython3.x.However,onmostLinuxsystems,itismore convenienttoinstallOpenCV2.x(andusePython2.7)becauseOpenCV3.xdoesnot havebinarypackagesformostLinuxsystemsyet. Thefollowingsubsectionscoveronlythesimplestwaystosetupourdependencieson Windows,Mac,andseveralLinuxdistributions.Otherapproaches(andotherUnix-like platforms)canalsowork.Forexample,advancedusersmaywishtoconfigureandbuild OpenCV3.0’ssourcecodeonLinuxsystemsthatdonotyethaveprepackagedbuildsof OpenCV3.x.Forguidanceonalternativesetups,refertooneofPacktPublishing’s dedicatedbooksonOpenCV,suchasLearningOpenCV3ComputerVisionwithPython byJoeMinichinoandJosephHowse. Windows ChristophGohlke,fromtheUniversityofCalifornia,Irvine,providesmanyreliable prebuiltversionsofscientificPythonlibrariesforWindows.Gotohisdownloadsiteat http://www.lfd.uci.edu/~gohlke/pythonlibs/.IfyouhadnotinstalledNumPyinChapter4, Steeringbehaviors,getthelatestNumPy+MKLwhlfilefromGohlke’ssite(MKLisan IntelMathKernelLibrarythatprovidesoptimizations).Atthetimeofwritingthisbook,it isoneofthefollowing: Python2.7,32-bit:numpy‑1.9.2+mkl‑cp27‑none‑win32.whl Python2.7,64-bit:numpy‑1.9.2+mkl‑cp27‑none‑win_amd64.whl Python3.4,32-bit:numpy‑1.9.2+mkl‑cp34‑none‑win32.whl Python3.4,64-bit:numpy‑1.9.2+mkl‑cp34‑none‑win_amd64.whl OpenCommandPromptandusepiptoinstallNumPyfromthewheelfile.Therelevant commandwillbesomethinglikethis: $pipinstallnumpy‑1.9.2+mkl‑cp27‑none‑win32.whl TheoutputfromthiscommandshouldincludeSuccessfullyinstallednumpy-1.9.2+mkl orasimilarline. Note IfyourunintoproblemslaterwhileimportingmodulesfromOpenCVorscikit-learn,try uninstallinganypreviousversionofNumPyfromChapter4,Steeringbehaviorsandusing thelatestNumPy+MKLwhlfilefromGohlke’ssite. Similarly,getGohlke’slatestwhlfileforscikit-learnandinstallitwithpip.Atthetimeof writingthisbook,itisoneofthefollowing: Python2.7,32-bit:scikit_learn‑0.16.1‑cp27‑none‑win32.whl Python2.7,64-bit:scikit_learn‑0.16.1‑cp27‑none‑win_amd64.whl Python3.4,32-bit:scikit_learn‑0.16.1‑cp34‑none‑win32.whl Python3.4,64-bit:scikit_learn‑0.16.1‑cp34‑none‑win_amd64.whl Also,atthetimeofwritingthisbook,GohlkeoffersdownloadsofOpenCV2.4forPython 2.7andOpenCV3.0forPython3.4.Thefollowingarethelatestversions: Python2.7,32-bit:opencv_python‑2.4.11‑cp27‑none‑win32.whl Python2.7,64-bit:opencv_python‑2.4.11‑cp27‑none‑win_amd64.whl Python3.4,32-bit:opencv_python‑3.0.0‑cp34‑none‑win32.whl Python3.4,64-bit:opencv_python‑3.0.0‑cp34‑none‑win_amd64.whl IfyouaresatisfiedwithusingGohlke’slatestbuildofOpenCV,downloadthewhlfileand installitwithpip. Alternatively,togetOpenCV3.xwithPython2.7bindings,wecandownloadanofficial buildfromtheOpenCVsiteandperformsomeinstallationstepsmanually.Theavailable downloadsofOpenCVarelistedathttp://opencv.org/downloads.html.Getthelatest versionforWindows.Atthetimeofwritingthisbook,thelatestfileiscalledopencv- 3.0.0.exe,anditincludesprebuiltbindingsforPython2.7butnot3.4.Runit,andwhen prompted,enterthefolderintowhichyouwanttoextractOpenCV.Wewillrefertothis locationas<opencv_unzip_path>.Amongotherthings,theextractedsubfolderscontain thepydanddllfiles,whicharecompiledlibraryfilesthatthePythoninterpretercanload atruntime.Next,wemustensurethatPythoncanfindthesefiles. CopyoneofthefollowingpydfilestothePythonsite-packagesfolder: Python2.7,32-bit:<opencv_unzip_path>/opencv/build/python/x86/cv2.pyd Python2.7,64-bit:<opencv_unzip_path>/opencv/build/python/x64/cv2.pyd Note Typically,thePython2.7site-packagesfolderwouldbelocatedat C:\Python27\Lib\site-packages. Now,editthesystem’sPathvariabletoincludeoneofthefollowingfolders(which containdllfilesofOpenCVandsomeofitsdependencies): Python2.7,32-bit:<opencv_unzip_path>/build/x86/vc12/bin Python2.7,64-bit:<opencv_unzip_path>/build/x64/vc12/bin Note ThePathvariablecanbeeditedbygoingtoControlPanel|SystemandSecurity| System|Advancedsystemsettings|EnvironmentVariables….Edittheexisting valueofPathbyappendingsomethinglike; <opencv_unzip_path>/build/x86/vc12/bin.Notetheuseofthesemicolonasa separatorbetweenpaths. Finally,werequirewxPython.ForPython2.7,wxPythoninstallersareavailableat http://wxpython.org/download.php.Downloadandrunthelatestinstaller.Atthetimeof writing,itisoneofthefollowingexefiles: Python2.7,32-bit:wxPython3.0-win32-3.0.2.0-py27.exe Python2.7,64-bit:wxPython3.0-win64-3.0.2.0-py27.exe ForPython3.4,wemustusewxPythonPhoenix,whichisamoreactivelydevelopedfork ofwxPython.SnapshotbuildsofwxPythonPhoenixarelistedat http://wxpython.org/Phoenix/snapshot-builds/.DownloadthelatestwhlfileforPython3.4 onWindows,andinstallitwithpip.Atthetimeofwritingthisbook,itisoneofthe following: Python3.4,32-bit:wxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-nonewin32.whl Python3.4,64-bit:wxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-nonewin_amd64.whl Now,wehaveallthenecessarydependenciesfordevelopingourprojectonWindows. Mac TheMacPortspackagemanagerprovidesaneasycommand-linetooltoconfigure,build, andinstallopensourcesoftware,suchasOpenCV.MacPortsitselfisanopensource communityproject,butitdependsonApple’sXcodedevelopmentenvironment,including theXcodeCommandLineTools.TosetupXcode,theXcodeCommandLineTools,and thenMacPorts,followtheinstructionsgivenathttps://www.macports.org/install.php. OnceMacPortsissetup,wecanopentheterminalandstartinstallingpackages,also knownasports.Atypicalinstallationcommandhasthefollowingformat: $sudoportinstall<package>+<variant_0>+<variant_1>... Thevariantsarealternativeconfigurationsoftheport.Forexample,OpenCVcanbe configuredtobuilditsPythonbindingsandtouseoptimizationsforcertainpiecesof hardware.TheseoptimizationsaddmoredependenciestoOpenCVbutmaymakeits functionsrunfaster.WewilltellOpenCVtooptimizeitselfviathreeframeworks:Eigen (forCPUvectorprocessing),IntelThreadBuildingBlocks(TBB,forCPU multiprocessing),andOpenCL(forGPUmultiprocessing).TheOpenCLoptimizationsare notusedbythePythoninterface,butitisgoodtohavethemjustincaseyouworkin futurewithOpenCVinotherlanguages. Tip Tosearchforportsbyname,runacommandsuchasthefollowing: $portlist*cv* Theprecedingcommandwilllistallportswhosenamescontaincv,notablytheopencv port.Tolistthevariantsofaport,runacommandsuchasthis: $portvariantsopencv ToinstallOpenCVforPython2.7,runthefollowingcommandintheterminal: $sudoportinstallopencv+python27+eigen+tbb+opencl Alternatively,toinstallitforPython3.4,runthiscommand: $sudoportinstallopencv+python34+eigen+tbb+opencl Notethatopencv+python27dependsonpython27andpy27-numpy,whileopencv +python34dependsonpython34andpy34-numpy.Thus,therelevantPythonversionand NumPyversionarealsoinstalled.AllMacPortsPythoninstallationsareseparatefrom Mac’sbuilt-inPythoninstallation(sometimescalledApplePython).TousetheMacPorts Python2.7installationasyourdefaultPythoninterpreter,runthefollowingcommand: $sudoportselectpythonpython27 Alternatively,tomaketheMacPortsPython3.4installationyourdefaultPython interpreter,runthiscommand: $sudoportselectpythonpython34 IfiteverbecomesnecessarytomakeApplePythonthedefaultagain,runacommandsuch asthefollowing: $portselectpythonpython27-apple ForPython2.7,hereisacommandthatservestoinstallscikit-learn: $sudoportinstallpy27-scikit-learn Similarly,forPython3.4,thefollowingcommandinstallsscikit-learn: $sudoportinstallpy34-scikit-learn ToinstallwxPythonforPython2.7,runthiscommand: $sudoportinstallpy27-wxpython-3.0 ForPython3.x,weneedtousewxPythonPhoenix,whichisamoreactivelydeveloped forkofwxPython.Atthetimeofwritingthisbook,MacPortsdoesnotcontainany wxPythonPhoenixpackages,sowewillfirstsetuppipandthenuseittoinstallthelatest snapshotbuildofwxPythonPhoenix.ToinstallpipforPython3.4,runthefollowing command: $sudoportinstallpy34-pip Wecanmakethenewlyinstalledpipthedefaultpipexecutable: $sudoportselectpippip34 SnapshotbuildsofwxPythonPhoenixarelistedathttp://wxpython.org/Phoenix/snapshotbuilds/.DownloadthelatestwhlfileforPython3.4onMac.Atthetimeofwritingthis book,itiswxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-cp34mmacosx_10_6_intel.whl.Toinstallthispackage,runacommandsuchasthefollowing: $sudopipinstallwxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-cp34mmacosx_10_6_intel.whl TheoutputfromthiscommandshouldincludeSuccessfullyinstalledwxPythonPhoenix-3.0.3.dev1764+9289a7corasimilarline. Now,wehaveallthenecessarydependenciesfordevelopingourprojectonMac. Debiananditsderivatives,includingRaspbian, Ubuntu,andLinuxMint ForPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,open theterminalandrunthefollowingcommand: $sudoapt-getinstallpython-opencvpython-scikits-learnpython-wxgtk2.8 Notethatthepython-opencvpackagedependsonthepython-numpypackage,sopythonnumpywillalsobeinstalled. Asanalternative,youcaninstallwxPythonPhoenixinsteadofthepython-wxgtk2.8 package.TosetupwxPythonPhoenixanditsdependencies,trythefollowingcommands: $sudoapt-getinstalllibwxbase3.0-devlibwxgtk3.0-devwx-common libwebkit-devlibwxgtk-webview3.0-devwx3.0-exampleswx3.0-headerswx3.0i18nlibwxgtk-media3.0-dev $pipinstall--upgrade--pre-fhttp://wxpython.org/Phoenix/snapshotbuilds/--trusted-hostwxpython.orgwxPython_Phoenix However,ifindoubt,justusethepython-wxgtk2.8package. Fedoraanditsderivatives,includingRHELand CentOS ForPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,open theterminalandrunthefollowingcommand: $sudoyuminstallopencv-pythonpython-scikit-learnwxPython Notethattheopencv-pythonpackagedependsonthenumpypackage,sonumpywillalso beinstalled. OpenSUSEanditsderivatives Again,forPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem, opentheterminalandrunthefollowingcommand: $sudoyuminstallpython-opencvpython-scikit-learnpython-wxWidgets Notethatthepython-opencvpackagedependsonthepython-numpypackage,sopythonnumpywillalsobeinstalled. SupportingmultipleversionsofOpenCV OpenCV2.xandOpenCV3.xbothusethenamecv2fortheirtop-levelPythonmodule. However,insidethismodule,someclasses,functions,andconstantshavebeenrenamedin OpenCV3.x.Moreover,somefunctionalityisentirelynewinOpenCV3.x,butourproject reliesonlyonfunctionalitythatispresentinbothmajorversions. Tobridgeafewofthenamingdifferencesbetweenversions2.xand3.x,wecreatea module,CVBackwardCompat.Itbeginsbyimportingcv2: importcv2 OpenCV’sversionstringisstoredincv2.__version__.Forexample,itsvaluemaybe 2.4.11or3.0.0.Wecanusethefollowinglineofcodetogetthemajorversionnumberas aninteger,suchas2or3: CV_MAJOR_VERSION=int(cv2.__version__.split('.')[0]) ForOpenCV2.x(orearlier),wewillinjectnewnamesintotheimportedcv2moduleso thatallthenecessaryOpenCV3.xnameswillbepresent.Specifically,weneedtocreate aliasesforseveralconstantsthathavenewnamesinOpenCV3.x,asseeninthefollowing code: ifCV_MAJOR_VERSION<3: #CreatealiasestomakepartsoftheOpenCV2.xlibrary #forward-compatible. cv2.LINE_AA=cv2.CV_AA cv2.CAP_PROP_FRAME_WIDTH=cv2.cv.CV_CAP_PROP_FRAME_WIDTH cv2.CAP_PROP_FRAME_HEIGHT=cv2.cv.CV_CAP_PROP_FRAME_HEIGHT cv2.FILLED=cv2.cv.CV_FILLED ThisistheentireimplementationofCVBackwardCompat.Ourothermoduleswillbeableto importtheCVBackwardCompatinstanceofcv2anduseanyofthealiasesthatwemayhave injected.Wewillseeanexampleinthenextmodule—ResizeUtils. Configuringcameras OpenCVprovidesaclass,calledcv2.VideoCapture,thatrepresentsastreamofimages fromeitheravideofileoracamera.Thisclasshasmethodssuchasread(image)for exposingthestream’snextimageasaNumPyarray.Italsohastheget(propId)and set(propId,value)methodsforaccessingpropertiessuchasthewidthandheight(in pixels),colorformat,andframerate.Thevalidpropertiesandvaluesmaydependonthe system’svideocodecsorcameradrivers. Acrosscameras,thedefaultpropertyvaluesmaydifferdramatically.Forexample,one cameramightdefaulttoanimagesizeof640x480,whileanothermaydefaultto1920x 1080.Forgreaterpredictability,weshouldtrytosetcrucialparametersratherthanrelyon thedefaults.Let’screateamodulecalledResizeUtilscontainingautilityfunctionto configuretheimagesize. TheResizeUtilsmodulebeginsbyimportingtheCVBackwardCompatinstanceofcv2, whichmaycontainaliases(dependingontheOpenCVversion).Hereistheimport statement: fromCVBackwardCompatimportcv2 ThepropertyIDsforwidthandheightarestoredinconstantscalled cv2.CAP_PROP_FRAME_WIDTHandcv2.CAP_PROP_FRAME_HEIGHT,respectively(referringto theprevioussection,notethattheconstants’originalnamesinOpenCV2.xwere cv2.cv.CV_CAP_PROP_FRAME_WIDTHandcv2.cv.CV_CAP_PROP_FRAME_HEIGHT,butwe createdaliasestomatchtheOpenCV3.xnames).Notetheuseoftheseconstantsinthe followingimplementationoftheutilityfunction: defcvResizeCapture(capture,preferredSize): #Trytosettherequesteddimensions. w,h=preferredSize successW=capture.set(cv2.CAP_PROP_FRAME_WIDTH,w) successH=capture.set(cv2.CAP_PROP_FRAME_HEIGHT,h) ifsuccessWandsuccessH: #Therequesteddimensionsweresuccessfullyset. #Returntherequesteddimensions. returnpreferredSize #Therequesteddimensionsmightnothavebeenset. #Returntheactualdimensions. w=capture.get(cv2.CAP_PROP_FRAME_WIDTH) h=capture.get(cv2.CAP_PROP_FRAME_HEIGHT) return(w,h) Asarguments,thefunctiontakesacv2.VideoCaptureobjectcalledcaptureandatwodimensionaltuplecalledpreferredSize.Wetrytoconfigurecapturetousethepreferred widthandheightinpreferredSize.Thepreferreddimensionsmayormaynotbe supported,soaswiththefeedback,wereturnatupleoftheactualwidthandheight. TheResizeUtilsmodulewillbeusefulforourCheckersModelmodule,as CheckersModelisresponsibleforinstantiatingandreadingfromcv2.VideoCapture. Workingwithcolors Normally,whenOpenCVobtainsanimagefromafileorcamera,itputstheimageinthe blue-green-red(BGR)colorformat.Morespecifically,theimageisa3DNumPyarrayin whichimage[y][x][0]isapixel’sbluevalue(intherangeof0to255),image[y][x][1] isitsgreenvalue,andimage[y][x][2]isitsredvalue.Theyandxindicesstartfromthe top-leftcorneroftheimage.Ifweconvertanimageintograyscale,itbecomesa2D NumPyarray,inwhichimage[y][x]isapixel’sgrayscalevalue. Let’swritesomeutilityfunctionsintheColorUtilsmoduletoworkwithcolordata.Our functionswillusePython’sstandardmathmoduleandNumPy,asseeninthefollowing importstatements: importmath importnumpy Let’swriteafunctionthatallowsustocopyasinglecolorchannelfromasourceimage (whichisinBGRformat)toadestinationimage(whichisingrayscaleformat).Ifthe specifieddestinationimageisNoneoritsformatiswrong,ourfunctionwillcreateit.To copythechannel,wejusttakeaflatviewofthesourceimage,sliceitwithastrideof3, andassigntheresulttoafullsliceofthedestinationimage,asseeninthefollowing implementation: defextractChannel(src,channel,dst): dstShape=src.shape[:2] ifdstisNoneordst.shape!=dstShapeor\ dst.dtype!=numpy.uint8: dst=numpy.empty(dstShape,numpy.uint8) dst[:]=src.flatten()[channel::3].reshape(dstShape) returndst Fortypicalcheckerboardsundertypicallightingconditions,theredchannelshowsahigh contrastbetweendarkandlightsquares.Later,wewillusethisobservationtoour advantage. Ontheotherhand,partsofouranalysiswillrelyonfullcolorsinsteadofonlyone channel.WewillneedtoquantifythecontrastordistancebetweentwoBGRcolorsin ordertodecidewhetherasquarecontainsalightpiece,adarkpiece,ashadow,ornothing. AnaïveapproachistousetheEuclideandistance,whichwouldbeimplementedlikethis: defcolorDist(color0,color1): returnmath.sqrt( (color0[0]-color1[0])**2+ (color0[1]-color1[1])**2+ (color0[2]-color1[2])**2) However,thisapproachassumesthatallcolorchannelshavethesamescaleandthescale islinear.Thisassumptiondefiescommonsense.Forexample,mostpeoplewouldagree thatthecoloramber(b=0,g=191,r=255)isnotashadeofred(b=0,g=0,r=255)butthat thecoloremerald(b=191,g=255,r=0)isashadeofgreen(b=0,g=255,r=0),even thoughthesetwopairingsrepresentthesameEuclideandistance(191).Manyalternative formulationsassumethatthechannelsarestilllinearbuthavedifferentscales—typically, theyassumethatgreenhasthebiggestunit,followedbyred,andthenblue.These formulationsyieldhighcontrastbetweenyellowandblue(forexample,betweensunlight andshade),andyetyieldlowcontrastbetweenshadesofblue.Thisisstillrather unsatisfactorybecausemostpeoplearegoodatdistinguishingshadesofblue.Thiadmer Riemersmacomparesseveraldistanceformulationsat http://www.compuphase.com/cmetric.htm,andproposesanalternativeinwhichthegreen scaleislinearwhiletheredandbluescalesarenonlinear,withreddifferenceshaving moreweightincomparisonsofreddishcolorsandbluedifferenceshavingmoreweightin comparisonsofnon-reddishcolors.Let’sreplaceourpreviouscolorDistfunctionwith thefollowingimplementationofRiemersma’smethod: defcolorDist(color0,color1): #Calculateared-weightedcolordistance,asdescribed #here:http://www.compuphase.com/cmetric.htm rMean=int((color0[2]+color1[2])/2) rDiff=int(color0[2]-color1[2]) gDiff=int(color0[1]-color1[1]) bDiff=int(color0[0]-color1[0]) returnmath.sqrt( (((512+rMean)*rDiff*rDiff)>>8)+ 4*gDiff*gDiff+ (((767-rMean)*bDiff*bDiff)>>8)) Basedontheprecedingformula,thedistancebetweenblackandwhiteisapproximately 764.8.Thescaleofthisdistanceisnotintuitive.Wemightprefertoworkwithnormalized values,wherebythenormalizeddistancebetweenblackandwhiteisdefinedas1.0.The followingfunctionreturnsanormalizedcolordistance: defnormColorDist(color0,color1): #Normalizebasedonthedistancebetween(0,0,0)and #(255,255,255). returncolorDist(color0,color1)/764.8333151739665 Nowwehavesufficientutilityfunctionstosupportourupcomingimplementationofthe CheckersModelmodule,whichwillcaptureandanalyzeimages. Buildingtheanalyzer TheCheckersModelmoduleistheeyesandthebrainofourproject.Itbringstogether everythingexcepttheGUI.Specifically,itdependsonNumPy,OpenCV,scikit-learn,and ourColorUtilsandResizeUtilsmodules,asreflectedinthefollowingimport statements: importnumpy importsklearn.cluster fromCVBackwardCompatimportcv2 importColorUtils importResizeUtils Note Althoughwearecombiningimagecapturingandanalysisintoonemodule,theyare arguablydistinctresponsibilities.Forthisproject,theyshareadependencyonOpenCV. However,inafutureproject,youmightcaptureimagesfromacamerathatrequires anotherlibrary,orfromanentirelydifferenttypeofsource,suchasanetwork.Youmight evensupportawidevarietyofcapturingtechniquesinoneproject.Wheneveryoufeelthat imagecaptureisacomplexprobleminitsownright,considerdedicatingatleastone separatemoduletoit. Tomakeourcodemorereadable,wewilldefineseveralconstantsinthismodule.These constantsrepresentthepossiblestatesoftheboard’srotation,theshadows’direction,and eachsquare’sclassification.Herearetheirdefinitions: ROTATION_0=0 ROTATION_CCW_90=1 ROTATION_180=2 ROTATION_CCW_270=3 DIRECTION_UP=0 DIRECTION_LEFT=1 DIRECTION_DOWN=2 DIRECTION_RIGHT=3 SQUARE_STATUS_UNKNOWN=-1 SQUARE_STATUS_EMPTY=0 SQUARE_STATUS_PAWN_PLAYER_1=1 SQUARE_STATUS_KING_PLAYER_1=11 SQUARE_STATUS_PAWN_PLAYER_2=2 SQUARE_STATUS_KING_PLAYER_2=22 ThismodulewillalsocontaintheCheckersModelclass,declaredlikethis: classCheckersModel(object): NotethatwehavemadeCheckersModelasubclassofobject.Thisisimportantfor Python2.xcompatibilitybecausewearegoingtousepropertygetterandsettermethods, asdiscussedinthenextsubsection.UnlikePython3.x,Python2.xdoesnotsupport accessormethodsinallclasses,butratheronlyinsubclassesofobject. Providingaccesstotheimagesandclassification results ThemembervariablesoftheCheckersModelclasswillincludecurrentimagesofthe scene(thatis,everythingthatthewebcamcansee)inBGRandgrayscale,aswellasa currentimageoftheboardinBGR.Unlikethescene,theboardwillbeabird’s-eyeview andmaycontaintextinordertoshowtheclassificationresults.Wewillprovideproperty getterssothatothermodulesmayreadtheimagesandtheirsizes,asseeninthefollowing code: @property defsceneSize(self): returnself._sceneSize @property defscene(self): returnself._scene @property defsceneGray(self): returnself._sceneGray @property defboardSize(self): returnself._boardSize @property defboard(self): returnself._board Note Propertygettersandsetterssimplyprovideashorthandnotationsothatamethodlooks likeanon-callablevariable.Hereisanexamplethatdemonstrateshowtouseaproperty getter: bm=BoardModel() scene=bm.scene#Callsgetter,bm.scene() Here,wehaveimplementedonlyagetterandnotasetterbecauseothermodulesshould notsettheimages.Thus,apieceofcodesuchasthefollowingwillproduceanerror: bm.scene=None#Callssetter,bm.scene(None) #Error!Thereisnosuchsetter. Thenextsubsection,Providingaccesstoparametersfortheusertoconfigure,willgive examplesonhowtoimplementsettermethods. Wealsoprovideagetterfortheclassificationresultsasa2DNumPyarray: @property defsquareStatuses(self): returnself._squareStatuses Forexample,ifPlayer1hasakinginthetop-leftcorneroftheboard, boardModel.squareStatuses[0,0]willbe11(thevalueofthe SQUARE_STATUS_KING_PLAYER_1constant,whichwedefinedearlier). AlltheaforementionedpropertiesrepresenttheresultsoftheCheckersModelmodule’s work,soothermodulesshouldtreatthemasread-only(andthustheyhaveonlygetters, notsetters).Next,let’sconsiderotherpropertiesthatrepresenttheparametersofthe CheckersModelmodule’swork,andhavebothgettersandsetterssothatothermodules mayreconfigurethem. Providingaccesstoparametersfortheuserto configure TounderstandwhyCheckersModeloffersparametersthatarereconfigurableatruntime, let’slookatapreviewofthethingsthatwewillnotautomateinthisproject: Thebird’s-eyeviewofthecheckerboardmayberotatedandflippedinaway differentfromwhattheuserexpects.ThisproblemisdiscussedintheCreatingand analyzingthebird’s-eyeviewoftheboardsubsection.Weallowtheusertospecifya differentrotation(0,90,180,or270degrees)andflip(X,Y,neither,orboth). Theshadows’directionisnotdetectedautomatically.Bydefault,weassumethatthe shadows’directionisup(negativey).Weallowtheusertospecifyadirection(up, right,down,orleft). Toclassifythecontentsofthesquares,wecomparethetwodominantcolorsinthe square,andthiscomparisonreliesoncertainthresholdvalues.Forsomelighting conditionsandsomecolorsofcheckerssets,thedefaultthresholdsmightnotbe appropriate.Weallowtheusertospecifydifferentthresholds.Theprecisemeaningof eachthresholdisdiscussedintheAnalyzingthedominantcolorsinasquare subsection. Theboard’srotationisexpressedasaninteger,correspondingtooneoftheconstantsthat wedeclaredatthestartofthismodule.Thefollowingcodeimplementsthegettersand settersfortherotation,andthesetterusesthemodulusoperatortoensurethattherotations wraparound: @property defboardRotation(self): returnself._boardRotation @boardRotation.setter defboardRotation(self,value): self._boardRotation=value%4 Thedirectionoftheshadowsisasimilarproperty: @property defshadowDirection(self): returnself._shadowDirection @shadowDirection.setter defshadowDirection(self,value): self._shadowDirection=value%4 Thethresholdsandflipdirectionsdonotrequireanyspeciallogicinagetterorsetter,so wewillsimplyimplementthemasplainoldmembervariables.Wecanseetheir declarationsinthefollowingcode,whichisthestartoftheclass’sinitializer: def__init__(self,patternSize=(7,7),cameraDeviceID=0, sceneSize=(800,600)): self.emptyFreqThreshold=0.3 self.playerDistThreshold=0.4 self.shadowDistThreshold=0.45 self._boardRotation=ROTATION_0 self.flipBoardX=False self.flipBoardY=False self._shadowDirection=DIRECTION_UP Let’sproceedtolookattherestofthemembervariablesandinitializationcode. Initializingtheentiremodelofthegame Aswehavepreviouslydiscussed,theroleoftheCheckersModelclassistoproduce imagesofthesceneandboardandclassifythecontentsofeachsquare.Theremainderof the__init__methoddeclaresvariablesthatpertaintoimagingandclassification. TheimagesofthesceneandboardwillinitiallybeNone,asseenhere: self._scene=None self._sceneGray=None self._board=None Lookingbackattheprevioussubsection,notethatpatternSizeisoneoftheinitializer’s arguments.Thisvariablereferstothenumberofinternalcornersintheboard,suchas(7, 7)inastandardAmericancheckerboardwitheightrowsandeightcolumns.Later,wewill seethatthenumberofinternalcornersisimportantforcertainOpenCVfunctions.Let’s putthecornerdimensionsandcountintothemembervariables,likethis: self._patternSize=patternSize self._numCorners=patternSize[0]*patternSize[1] Later,asthestepstowardclassification,wewillmeasurethefrequencyanddistanceofthe twodominantcolorsineachsquare.ThistaskisdiscussedintheAnalyzingthedominant colorsinasquaresubsection.Fornow,wewillonlycreateemptyNumPyarraysforthis data: self._squareFreqs=numpy.empty( (patternSize[1]+1,patternSize[0]+1), numpy.float32) self._squareDists=numpy.empty( (patternSize[1]+1,patternSize[0]+1), numpy.float32) Similarly,wewillcreateaNumPyarrayfortheresultsofclassification,andwewillfillin thisarraywiththeSQUARE_STATUS_UNKNOWNvalue(whichwepreviouslydefinedas-1): self._squareStatuses=numpy.empty( (patternSize[1]+1,patternSize[0]+1), numpy.int8) self._squareStatuses.fill(SQUARE_STATUS_UNKNOWN) Tofindeachsquare’sdominantcolors,wewillrelyonaclassinscikit-learncalled sklearn.cluster.MiniBatchKMeans.Thisclassrepresentsastatisticalprocesscalledkmeansclustering,whichseparatesdataintoagivennumberofgroupsandfindsthe centroidofeachgroup.Fornow,weonlyneedtoconstructaninstanceoftheclasswitha singleargument,n_clusters,whichindicatesthenumberofgroupsthattheclustererwill distinguish(2inthiscase,becausewewanttoknowthetwodominantcolors): self._clusterer=sklearn.cluster.MiniBatchKMeans(2) Tosupportwebcaminput,wewillcreateaVideoCaptureobjectandsetitscapture dimensionsusingourutilityfunctionfromtheResizeUtilsmodule: self._capture=cv2.VideoCapture(cameraDeviceID) self._sceneSize=ResizeUtils.cvResizeCapture( self._capture,sceneSize) w,h=self._sceneSize Later,aswecaptureimages,wewilltrytofindtheinternalcornersofthecheckerboard, andwewillstorethesuccessfullydetectedcornerssothatwedonothavetoperformthis searchfromscratchforeveryframe.Thefollowingtwomembervariableswillholdthe previouslydetectedcorners(initiallyNone)andagrayscaleimageofthesceneatthetime ofthepreviousdetection(initiallyanemptyimage): self._lastCorners=None self._lastCornersSceneGray=numpy.empty( (h,w),numpy.uint8) Thescene’sdetectedcornercoordinateswillimplyahomography(amatrixthatdescribes adifferenceinperspective)whenwecomparethemwiththeknowncornercoordinatesin abird’s-eyeviewoftheboard.Likethecorners,thehomographydoesnotneedtobe recomputedforeveryframe,sowewillstoreitinthefollowingmembervariable(initially None): self._boardHomography=None Wewillarbitrarilystipulatethatthebird’s-eyeviewoftheboardwillbenolargerthanthe capturedimageofthescene.Startingfromthisconstraint,wewillcalculatethesizeofa squareandtheboard: self._squareWidth=min(w,h)//(max(patternSize)+1) self._squareArea=self._squareWidth**2 self._boardSize=( (patternSize[0]+1)*self._squareWidth, (patternSize[1]+1)*self._squareWidth ) Havingdeterminedthesizeofasquare,wewillcalculateasetofcornercoordinatesfor thebird’s-eyeview.Weneedthecoordinatesofonlythoseinternalcornerswherefour squaresmeet.Later,intheDetectingtheboard’scornersandtrackingtheirmotion subsection,wewillcomparetheseidealcoordinatestotheinternalcornersofthe checkerboardthataredetectedinthesceneinordertofindthehomography.Hereisour codeforcreatingalistandthenaNumPyarrayofevenlyspacedcorners: self._referenceCorners=[] forxinrange(patternSize[0]): foryinrange(patternSize[1]): self._referenceCorners+=[[x,y]] self._referenceCorners=numpy.array( self._referenceCorners,numpy.float32) self._referenceCorners*=self._squareWidth self._referenceCorners+=self._squareWidth ThisconcludestheinitializationoftheCheckersModelclass.Next,let’sconsiderhowthe imagesofthesceneandboardwillchange,alongwiththeresultsofclassification. Updatingtheentiremodelofthegame OurCheckersModelclasswillprovidethefollowingupdatemethod,whichothermodules maycall: defupdate(self,drawCorners=False, drawSquareStatuses=True): Specifically,theGUIapplication(intheCheckersmodule)willcallthisupdatemethod. Logically,theapplicationisresponsibleformanagingresourcesandensuringthat everythingremainsresponsive,soitisinabetterpositiontodecidewhenanupdate shouldoccur.Theupdatemethod’sdrawCornersargumentspecifieswhethertheresultsof cornerdetectionshouldbedisplayedinthescene.ThedrawSquareStatusesargument specifieswhethertheresultsofclassificationshouldbedisplayedinthebird’s-eyeviewof theboard. Theupdatemethodreliesonseveralhelpermethods.First,wewillcallahelperthattries tocaptureanewimageofthescene.Ifthisfails,wereturnFalsetoinformthecallerthat noupdatehasoccurred: ifnotself._updateScene(): returnFalse#Failure Wewillproceedtosearchforthecheckerboardinthescene.Asuccessfulsearchwill produceasetofcornercoordinatesfortheboard’ssquares.Moreover,thesecoordinates willimplyahomography.Thefollowinglineofcodecallsahelpermethodthatis responsibleforfindingthecornersandahomography: self._updateBoardHomography() Next,wewillcallahelpermethodthatisresponsibleforcreatingthebird’s-eyeviewof theboardaswellasanalyzingandclassifyingeachsquare: self._updateBoard() Atthispoint,theboarddetectionandsquareclassificationarecomplete,butwemaystill needtodisplayresultsdependingonthedrawCornersanddrawSquareStatuses arguments.Conveniently,OpenCVprovidesafunction, cv2.drawChessboardCorners(image,patternSize,corners,patternWasFound),to displayasetofdetectedcornersinascenecontainingachessboardorcheckerboard.Here isthewayweuseit: ifdrawCornersandself._lastCornersisnotNone: #Drawtheboard'sgrid. cv2.drawChessboardCorners( self._scene,self._patternSize, self._lastCorners,True) Wehaveanotherhelpermethodfordisplayingtheclassificationresultinagivensquare. Thefollowingcodeshowshowweiterateoversquaresandcallthehelper: ifdrawSquareStatuses: foriinrange(self._patternSize[0]+1): forjinrange(self._patternSize[1]+1): self._drawSquareStatus(i,j) Atthispoint,theupdatehassucceeded,andwereturnTruetoletthecallerknowthat therearenewresults: returnTrue#Success Let’sdelvedeeperintothehelpermethods’roles,startingwithimagecapture. Capturingandconvertinganimage OpenCV’sVideoCaptureclasshasaread(image)method.Thismethodcapturesan imageandwritesittothegivendestinationarray(ifimageisNoneoritsformatiswrong,a newarrayiscreated).Themethodreturnsatuple,(retval,image),containingaboolean (Trueifthecapturehassucceeded)andthentheimage(eithertheoldarrayoranewly createdarray).Weusethismethodtotrytocaptureanewscenefromthecamera.Ifthis succeeds,weuseourextractChannelutilityfunctiontogettheredchannelasagrayscale versionofthescene.Thefollowingmethodperformsthesestepsandreturnsabooleanto indicatesuccessorfailure: def_updateScene(self): success,self._scene=self._capture.read(self._scene) ifnotsuccess: returnFalse#Failure #Usetheredchannelasgrayscale. self._sceneGray=ColorUtils.extractChannel( self._scene,2,self._sceneGray) returnTrue#Success Considerthefollowingstripofimages.Theleftmostimagerepresentstheoriginalscenein BGRcolor(thoughitwillappearasgrayscaleinthisbook’sprintedition).Thisboard’s colorsareburntamberforthedarksquares,ochreforthelightsquares,andburntsienna fortheborder(thiscolorschemeisquitecommonincheckerboards).Thesecond,third, andfourthimages(fromlefttoright)representthescene’sblue,green,andredchannels, respectively: Notethattheredchannelcapturesthecheckerboardbrightlyandwithgoodcontrast betweenlighteranddarkersquares.Moreover,theboard’sborderappearsquitelight.This isgoodbecausewearesoongoingtouseadetectorthatisoptimizedforablack-andwhiteboardwithawhiteborder. Detectingtheboard’scornersandtrackingtheir motion Wedonotwanttoupdatetheboard’scornersandhomographyineveryframe,butonlyin frameswheretheboardorcamerahasmoved.Thisrulereducesthecomputationalburden inmostframesandallowsustokeepagooddetectionresultfromapreviousframesothat wedonotrequireanunobstructedviewoftheboardineveryframe.Particularly,wecan berelativelysureofgettinggooddetectionresultswhentheboardisempty,andwewould wanttokeeptheseresultsforlaterframesinwhichtheboardisclutteredwithpiecesor withtheplayers’movinghands. Beforefindingnewcornersandthehomography,let’slookattrackingmotion.Suppose wehavedetectedasetofcornersinapreviousframe.Ratherthansearchingtheentire imagefornewcorners,wecantrytofindthesamecornersatorneartheirprevious locations.Thisidea—mappingframe-to-framemotionofindividualpointsinanimage— iscalledopticalflow.OpenCVprovidesimplementationsofseveralopticalflow techniques.WewilluseatechniquecalledpyramidalLukas-Kanade,whichis implementedinthecv2.calcOpticalFlowPyrLK(prevImg,nextImg,prevPts)function. Thisfunctionreturnsatuple,(nextPts,status,error).Eachofthetuple’selementsis anarray.ThenextPtscontainsthepoints’estimatednewcoordinates.Thestatus containscodesforindicatingwhethereachestimateisvalid(1,meaningthepointwas tracked)orinvalid(0,meaningitwaslost).Finally,errorcontainsameasurementof dissimilaritybetweenthepixelsintheoldandnewneighborhoodsforeachpoint.Ahigh averageerroracrossallpointsimpliesthateverythinglooksdifferentand,mostlikely,that theboardorcamerahasmoved.Weapplythisreasoninginthefollowingcode,whichis thebeginningofthe_updateBoardHomographymethod: def_updateBoardHomography(self): ifself._lastCornersisnotNone: corners,status,error=cv2.calcOpticalFlowPyrLK( self._lastCornersSceneGray, self._sceneGray,self._lastCorners,None) #Statusis1iftracked,0ifnottracked. numCornersTracked=sum(status) #Ifnottracked,errorisinvalidsosetitto0. error[:]=error*status meanError=(sum(error)/numCornersTracked)[0] ifmeanError<4.0: #Theboard'soldcornersandhomographyare #stillgoodenough. return Note Thethreshold,meanError<4.0,hasbeenchosenexperimentally.Ifyouseethatthe estimateoftheboard’scornerschangesevenwhentheboardisstill,tryraisingthe threshold.Conversely,ifyoufindthattheestimateoftheboard’scornersremains unchangedevenwhentheboardismoved,youmayneedtolowerthethreshold. Ifwehavenotyetreturned,itmeansthateithertherearenopreviouslyfoundcorners,or thepreviouslyfoundcornersarenolongervalid(probablybecausetheboardorthe camerahasmoved).Eitherway,wemustsearchfornewcorners.OpenCVprovidesmany general-purposefunctionsforfindinganykindofcorner,butconveniently,italsoprovides aspecializedfunctionforfindingthecornersofsquaresinachessboardoracheckerboard. Thisfunction,cv2.findChessboardCorners(image,patternSize),canfindaboardof anyspecifieddimensions,evenanon-squareboard.ThepatternSizeargumentspecifies thenumberofinternalcorners,suchas(7,7)forastandardAmericancheckerboardwith eightrowsandeightcolumns.Thisfunctionreturnsatuple,(retval,corners),where retvalisaBoolean(whichisTrueifallthecornerswerefound)andcornersisalistof cornercoordinates.Itexpectsacheckerboardwithpureblackandpurewhitesquares,but othercolorschemesmayworkdependingonthestrengthoftheircontrastinthegiven grayscaleimage.Moreover,thefunctionexpectstheboardtohavealightborder,which makesiteasiertofindthecornersoftheoutermostdarksquares.Thefollowingcode showsourusageoffindChessboardCorners: #Findthecornersintheboard'sgrid. cornersWereFound,corners=cv2.findChessboardCorners( self._sceneGray,self._patternSize, flags=cv2.CALIB_CB_ADAPTIVE_THRESH) NotethatfindChessboardCornershasanoptionalflagsargument.Weusedaflagcalled cv2.CALIB_CB_ADAPTIVE_THRESH,whichstandsforadaptivethresholding.Whenthis flagisset,thefunctionattemptstocompensatefortheoverallbrightnessoftheimageso thatitdoesnotnecessarilyrequirethesquarestolookreallyblackorreallywhite. Thesmallcirclesandlinesshowninthefollowingimageareavisualizationofthecorners foundusingfindChessboardCorners.TheyaredrawnusingthedrawChessboardCorners function,whichwecoveredintheprevioussubsection. Ifthecornersarefound,wecanconverttheircoordinatesfromalisttoaNumPyarray. Then,wecancomparethefoundcoordinateswiththereferencecoordinatesforabird’seyeview(rememberthatwealreadyinitializedthereferencecoordinatesintheInitializing theentiremodelofthegamesubsection).Tocomparethetwosetsofcoordinates,wewill useanOpenCVfunctioncalledcv2.findHomography(srcPoints,dstPoints,method). Theoptionalmethodargumentrepresentsastrategytorejectoutliers(incongruouspoints thatshouldnotbecountedtowardtheresult).Thefunctionreturnsahomographymatrix, whichisatransformationthatmapssrcPointstodstPointswithminimalerror.The followingcodedemonstratesouruseoffindHomography: ifcornersWereFound: #Findthehomography. corners=numpy.array(corners,numpy.float32) corners=corners.reshape(self._numCorners,2) self._boardHomography,matches=cv2.findHomography( corners,self._referenceCorners,cv2.RANSAC) #Recordthecornersandtheirimage. self._lastCorners=corners self._lastCornersSceneGray[:]=self._sceneGray Notethatwekeepacopyofthegrayscaleimageofthescene.Thenexttimethismethodis called,wewillcomparetheoldandnewgrayscalescenestotrackthecorners’motion. Atthispoint,wemayhavefoundtheboard’shomography,butwehavenotyetcreatedan imagetoshowthebird’s-eyeview.Let’stacklethistasknext. Creatingandanalyzingthebird’s-eyeviewofthe board Iftheboard’scornersandhomographyarefound,wecantransformthescene’s perspectivetoproduceabird’s-eyeviewoftheboard.TherelevantfunctioninOpenCVis cv2.warpPerspective(src,M,dsize,dst),whereMisthehomographymatrixand dsizeisanarbitrarysizefortheoutputimage.Thisfunctionappliesthehomography matrixtochangetheperspective,andthenitresizesandcropstheresult.Our _updateBoardmethodbeginswiththefollowingcode: def_updateBoard(self): ifself._boardHomographyisnotNone: #Warptheboardtoobtainabird's-eyeview. self._board=cv2.warpPerspective( self._scene,self._boardHomography, self._boardSize,self._board) Thefollowingpairofimagesshowsthecamera’sviewofthescene(left),andtheresulting bird’s-eyeviewoftheboard(right)afterperspectivetransformation: Atthisstage,thebird’s-eyeviewmightstillneedtoberotatedand/orflippedtomatchthe user’ssubjectiveperceptionofdirections.Theuserisprobablysittingononesideofthe boardandperceivesthissideasthe“near”or“down”(positivey)side.Thecameramight beonthesameside,anyotherside,oradiagonal!Moreover,dependingonitsdriversand configuration,thecameramayevencapturemirroredimages.The findChessboardCornersfunctiondoesnotlimititssearch.Asfarasitisconcerned,the scenecouldshowanupside-downandmirroredchessboard,andthissolutionisasgoodas anyothersolutionthatputsasetofcornersintherightplaces.Wecouldinspectandedit thevaluesinthe_boardHomographymatrixtoenforceourownassumptions,butinsteadof this,wewilllettheuserinspecttheimageanddecideonanynecessarychanges. Lookingcloselyattheprecedingpairofimages,notethattheboardhasadarkorblurry seamdownthemiddle(itcanfoldhere).Theseamisapproximatelyhorizontalinthe camera’sviewbutverticalinthebird’s-eyeview.Moreover,thetwoviewsdifferbya horizontalflip.Ausermightfindthesedifferencesunintuitive. OpenCVprovidesafunctioncalledcv2.flip(src,flipCode,dst)toflipanimagein themannerspecifiedbyflipCode(-1orlesstoflipbothxandycoordinates,0toflipy coordinates,and1orgreatertoflipxcoordinates).Anotherfunction, cv2.transpose(src,dst),servestoswapxandycoordinateswitheachother.Arotation of90or270degreescanbeimplementedasacombinationofatransposeandaflip,while arotationof180degreescanbeimplementedasaflipinbothdimensions.Let’susethese approachesinthefollowingcodetoapplyaspecifiedflipandrotation: #Rotateandfliptheboard. flipX=self.flipBoardX flipY=self.flipBoardY ifself._boardRotation==ROTATION_CCW_90: cv2.transpose(self._board,self._board) flipX=notflipX elifself._boardRotation==ROTATION_180: flipX=notflipX flipY=notflipY elifself._boardRotation==ROTATION_CCW_270: cv2.transpose(self._board,self._board) flipY=notflipY ifflipX: ifflipY: cv2.flip(self._board,-1,self._board) else: cv2.flip(self._board,1,self._board) elifflipY: cv2.flip(self._board,0,self._board) Later,wewillensurethatusercansetboardRotation,flipBoardX,andflipBoardYvia theGUI.Forexample,theusercanseetheprecedingpairofimagesandthenmake adjustmentstoproducethefollowingpairofimagesinstead: Oncethetransformationsarecomplete,wewilliterateoverallthesquaresandcalla helpermethodtogeneratedataabouteachsquare’scolor: foriinrange(self._patternSize[0]+1): forjinrange(self._patternSize[1]+1): self._updateSquareData(i,j) Then,wewilliterateoverallthesquaresagainandcallahelpermethodtoupdatethe classificationofeachsquare: foriinrange(self._patternSize[0]+1): forjinrange(self._patternSize[1]+1): self._updateSquareStatus(i,j) Notethatasquare’sclassificationmayrelyondataaboutaneighboringsquare(sincewe maysearchforashadowtodistinguishakingfromapawn).Thisiswhyweanalyzethe colorsofallthesquaresinonestepandclassifythesquaresinanotherstep.Let’sconsider theanalysisofcolorsnow. Analyzingthedominantcolorsinasquare Our_updateSquareDatamethodtakesasquare’sindicesasarguments,anditbeginsby calculatingthesquare’stop-leftandbottom-rightpixelcoordinatesinthe_boardimage,as seeninthefollowingcode: def_updateSquareData(self,i,j): x0=i*self._squareWidth x1=x0+self._squareWidth y0=j*self._squareWidth y1=y0+self._squareWidth Rememberthatwehaveamembervariablecalled_clusterer.Itisaninstanceofthe sklearn.cluster.MiniBatchKMeansclass.Thisclasshasamethod,calledfit(X),that classifiesthedatainXandstorestheresultsinthemembervariablesofMiniBatchKMeans. Xmustbea2DNumPyarray,sowemustreshapethesquare’simagedata.Forexample,if asquarehas75x75pixelswiththreecolorchannels,wewillpassaviewofthesquareas anarrayofshape(75*75,3),not(75,75,3).Thefollowingcodeshowshowweslice andreshapethesquareandpassittothefitmethod: #Findthetwodominantcolorsinthesquare. self._clusterer.fit( self._board[y0:y1,x0:x1].reshape( self._squareArea,3)) Theresultsofthecolorclusteringarestoredin_clusterer.centersand _clusterer.labels,whichareNumPyarrays.Theshapeofcentersis(2,3),andit representsthetwodominantBGRcolorsinthesquare.Meanwhile,labelsisaonedimensionalarraywhoselengthisequaltothenumberofpixelsinthesquare.Eachvalue inlabelsis0ifthepixelisclusteredwiththefirstdominantcolor,or1ifthepixelis clusteredwiththeseconddominantcolor.Thus,themeanoflabelsrepresentsthesecond color’sfrequency(theproportionofpixelsthatareclusteredwiththiscolor).The followingcodeshowshowwecanfindthefrequencyofthelessdominantcolor,aswell asthenormalizeddistancebetweenthetwodominantcolors,basedontheformulainour ColorUtilsmodule: #Findtheproportionofthesquare'sareathatis #occupiedbythelessdominantcolor. freq=numpy.mean(self._clusterer.labels_) iffreq>0.5: freq=1.0-freq #Findthedistancebetweenthedominantcolors. dist=ColorUtils.normColorDist( self._clusterer.cluster_centers_[0], self._clusterer.cluster_centers_[1]) self._squareFreqs[j,i]=freq self._squareDists[j,i]=dist Thefrequencyanddistanceenableustotalkaboutthesquare’scolorsatamuchhigher levelofabstractionthanrawchannelvalues.Forexample,wemayobserve,“Thissquare containsabigobjectthatcontrastsstronglywiththebackground,”(highfrequencyand highdistance)withoutneedingtosearchforanyspecificsquarecolororplayingpiece color.Next,wewillusesuchobservationstoclassifythecontentsofasquare. Classifyingthecontentsofasquare Our_updateSquareStatusesmethodtakesasquare’sindicesasarguments(again),andit beginsbylookingupthesquare’sfrequencyanddistancedata,asseeninthiscode: def_updateSquareStatus(self,i,j): freq=self._squareFreqs[j,i] dist=self._squareDists[j,i] Wearealsointerestedinthefrequencyanddistancedataofaneighboringsquarethatmay potentiallycontainashadow.Asdiscussedearlier,theusermayconfigureashadow’s direction.Thefollowingcodeshowshowwecanselectaneighborbasedontheshadow’s direction: ifself._shadowDirection==DIRECTION_UP: ifj>0: neighborFreq=self._squareFreqs[j-1,i] neighborDist=self._squareDists[j-1,i] else: neighborFreq=None neighborDist=None elifself._shadowDirection==DIRECTION_LEFT: ifi>0: neighborFreq=self._squareFreqs[j,i-1] neighborDist=self._squareDists[j,i-1] else: neighborFreq=None neighborDist=None elifself._shadowDirection==DIRECTION_DOWN: ifj<self._patternSize[1]: neighborFreq=self._squareFreqs[j+1,i] neighborDist=self._squareDists[j+1,i] else: neighborFreq=None neighborDist=None elifself._shadowDirection==DIRECTION_RIGHT: ifi<self._patternSize[0]: neighborFreq=self._squareFreqs[j,i+1] neighborDist=self._squareDists[j,i+1] else: neighborFreq=None neighborDist=None else: neighborFreq=None neighborDist=None Weexpectaking’sshadowtobeasmallregion(lowfrequency)thatcontrastsstrongly (highfrequency)withthebackgroundofalightsquare.Thus,wewilltestwhetherthe frequencyisbelowacertainthresholdandthatthedistanceisaboveanotherthreshold,as seeninthefollowingcode: castsShadow=\ neighborFreqisnotNoneand\ neighborFreq<self.emptyFreqThresholdand\ neighborDistisnotNoneand\ neighborDist>self.shadowDistThreshold Rememberthattheusermayconfigurethethresholdsinordertomanuallyadaptour approachtodifferentlightingconditionsandcolorschemes. Note Notethatthesquaresononeedgeoftheboardwillnothaveanyneighborsinthe shadow’sdirection,sowejustassumethatthereisnoshadowthere.Wecouldimproveon ourapproachbyanalyzingtheborderareasjustpasttheboard’sedge. Atthispoint,wehaveanideaofwhethertheneighbormightbeashadowornot,butwe stillneedtoconsiderthecurrentsquare.Weexpectaplayingpiecetobealargeobject (highfrequency),andintheabsenceofsuchanobject,thesquaremustbeempty.This logicisreflectedinthefollowingcode,whichreliesonafrequencythreshold: iffreq<self.emptyFreqThreshold: squareStatus=SQUARE_STATUS_EMPTY else: Aplayingpiecemaybeeitherdarkorlight,andeitherapawnoraking.Weexpectalight playingpiecetocontraststrongly(highdistance)withthebackgroundofadarksquare, whileadarkplayingpiecewillhaveweakercontrast.Thus,anotherdistancethresholdis tested.Moreover,weexpectakingtohavealongshadowthatextendsintoaneighboring square,whileapawnshouldhaveashortershadow.Thefollowingcodereflectsthese criteria: ifdist<self.playerDistThreshold: ifcastsShadow: squareStatus=SQUARE_STATUS_KING_PLAYER_1 else: squareStatus=SQUARE_STATUS_PAWN_PLAYER_1 else: ifcastsShadow: squareStatus=SQUARE_STATUS_KING_PLAYER_2 else: squareStatus=SQUARE_STATUS_PAWN_PLAYER_2 Atthispoint,wehaveaclassificationresult.Wewillstoreitsothatothermethods(and othermodules,viaapropertygetter)mayaccessit: self._squareStatuses[j,i]=squareStatus Next,wewillprovideaconvenientwaytovisualizetheclassificationsofallthesquares. Drawingtext Asthelaststepofupdatingtheimageoftheboard,wewilldrawtextatopeachnonempty squaretoshowthenumericcodeoftheclassificationresult.OpenCVprovidesafunction calledcv2.putText(img,text,org,fontFace,fontScale,color,thickness, lineType)fordrawingtextatthepositionspecifiedbytheorg(origin)argument.The originreferstothetop-leftcornerofthetext.However,wewantthetexttobecenteredin thesquare.Tofindthetext’soriginrelativetothesquare’scenter,weneedtoknowthe sizeofthetextinpixels.Fortunately,OpenCVprovidesanotherfunction, cv2.getTextSize(text,fontFace,fontScale,thickness),forthispurpose.The followingcodeusesthesetwofunctionstoplacethetextinthecenterofagivensquare: def_drawSquareStatus(self,i,j): x0=i*self._squareWidth y0=j*self._squareWidth squareStatus=self._squareStatuses[j,i] ifsquareStatus>0: text=str(squareStatus) textSize,textBaseline=cv2.getTextSize( text,cv2.FONT_HERSHEY_PLAIN,1.0,1) xCenter=x0+self._squareWidth//2 yCenter=y0+self._squareWidth//2 textCenter=(xCenter-textSize[0]//2, yCenter+textBaseline) cv2.putText(self._board,text,textCenter, cv2.FONT_HERSHEY_PLAIN,1.0, (0,255,0),1,cv2.LINE_AA) Notethatweusethecv2.FONT_HERSHEY_PLAINandcv2.LINE_AAconstantstoselectthe HersheyPlainfontandanti-aliasing. ThiscompletesthefunctionalityoftheCheckersModelclass.Next,wewriteautility functiontoconvertanOpenCVimageforusewithwxPython.Afterthis,wewill implementtheGUIapplication.ItconfiguresaninstanceofCheckersModelanddisplays theresultingimagesofthesceneandboard. ConvertingOpenCVimagesforwxPython Aswehaveseenearlier,OpenCVtreatsimagesasNumPyarrays—typically,3Darraysin BGRformator2Darraysingrayscaleformat.Conversely,wxPythonhasitsownclasses forrepresentingimages,typicallyinRGBformat(thereverseofBGR).Theseclasses includewx.Image(aneditableimage),wx.Bitmap(adisplayableimage),and wx.StaticBitmap(aGUIelementthatdisplaysaBitmap). OurwxUtilsmodulewillprovideafunctionthatconvertsaNumPyarrayfromeither BGRorgrayscaletoanRGBBitmap,readyfordisplayinawxPythonGUI.This functionalitydependsonOpenCVandwxPython,asreflectedinthefollowingimport statements: fromCVBackwardCompatimportcv2 importwx Conveniently,wxPythonprovidesafactoryfunctioncalledwx.BitmapFromBuffer(width, height,dataBuffer),whichreturnsanewBitmap.ThisfunctioncanacceptaNumPy arrayinRGBformatasthedataBufferargument.However,abugcauses BitmapFromBuffertofailonthefirst-generationRaspberryPi,apopularsingle-board computer(SBC).Asaworkaround,wecanuseapairoffunctions: wx.ImageFromBuffer(width,height,dataBuffer)andwx.BitmapFromImage(image). Thelatterislessefficient,soweshouldpreferablyuseitonlyonhardwarethatisaffected bythebug. Tocheckwhetherwearerunningonthefirst-generationPi,wecaninspectthenameof thesystem’sCPU,asseeninthefollowingcode: #TrytodeterminewhetherweareonRaspberryPi. IS_RASPBERRY_PI=False try: withopen('/proc/cpuinfo')asf: forlineinf: line=line.strip() ifline.startswith('Hardware')and\ line.endswith('BCM2708'): IS_RASPBERRY_PI=True break except: pass Next,let’slookatourconversionfunction’simplementationforthefirst-generationPi. First,wecheckthedimensionalityoftheNumPyarray,thenmakeaninformedguess aboutitsformat(BGRorgrayscale),andfinallyconvertittoanRGBarrayusingan OpenCVfunctioncalledcv2.cvtColor(src,code).Thecodeargumentspecifiesthe sourceanddestinationformats,suchascv2.COLOR_BGR2RGB.Afterallofthis,weuse ImageFromBufferandBitmapFromImagetoconverttheRGBarraytoanImageandthe ImagetoaBitmap: ifIS_RASPBERRY_PI: defwxBitmapFromCvImage(image): iflen(image.shape)<3: image=cv2.cvtColor(image,cv2.COLOR_GRAY2RGB) else: image=cv2.cvtColor(image,cv2.COLOR_BGR2RGB) h,w=image.shape[:2] wxImage=wx.ImageFromBuffer(w,h,image) bitmap=wx.BitmapFromImage(wxImage) returnbitmap Theimplementationforotherkindsofhardwareissimilar,exceptthatweconvertthe RGBarraydirectlytoaBitmapusingBitmapFromBuffer: else: defwxBitmapFromCvImage(image): iflen(image.shape)<3: image=cv2.cvtColor(image,cv2.COLOR_GRAY2RGB) else: image=cv2.cvtColor(image,cv2.COLOR_BGR2RGB) h,w=image.shape[:2] #ThefollowingconversionfailsonRaspberryPi. bitmap=wx.BitmapFromBuffer(w,h,image) returnbitmap WewillusethisfunctioninourGUIapplication,comingupnext. BuildingtheGUIapplication TheCheckersmodulewillcontainallofthecoderequiredfortheGUIapplication.This moduledependsonwxPythonaswellasPython’sstandardthreadingmoduletoallowus toputalloftheintensivecomputervisionworkontoabackgroundthread.Moreover,we willrelyonourCheckersModelmoduleforthecapturingandanalysisofimages,andour WxUtilsmoduleforitsimageconversionutilityfunction.Herearetherelevantimport statements: importthreading importwx importCheckersModel importWxUtils Ourapplicationclass,Checkers,isasubclassofwx.Frame,whichrepresentsanormal window(notadialog).WeinitializeitwithaninstanceofCheckersModel,andawindow title(Checkersbydefault).Herearethedeclarationsoftheclassandthe__init__ method: classCheckers(wx.Frame): def__init__(self,checkersModel,title='Checkers'): WewillalsostoreCheckersModelinamembervariable,likethis: self._checkersModel=checkersModel Theimplementationoftheinitializercontinuesinthefollowingsubsections.Wewillsee howtheapplicationlaysouttheGUI,handlesevents,andinteractswithCheckersModel. Creatingawindowandbindingevents Wewillcreateawindowbyinitializingthewx.Framesuperclass.Ratherthanusethe defaultwindowstyle,wewillspecifyacustomstylethatdoesnotallowthewindowtobe resized.Wewillalsospecifythewindow’stitleandagraybackgroundcolorinthiscode: style=wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.CAPTION|\ wx.SYSTEM_MENU|wx.CLIP_CHILDREN wx.Frame.__init__(self,None,title=title,style=style) self.SetBackgroundColour(wx.Colour(232,232,232)) MostwxPythonclassesinheritaBind(event,handler)methodfromahigh-levelclass calledEvtHandler.Themethodregistersagivencallbackfunction(handler)foragiven typeofGUIevent(event).Whentheobjectreceivesaneventofthegiventype,the callbackisinvoked.Forexample,let’saddthefollowinglineofcodetoensurethata givenmethodiscalledwhenthewindowisclosed: self.Bind(wx.EVT_CLOSE,self._onCloseWindow) TheprecedingcallbackisimportantbecauseourCheckersclassneedstodosomecustom cleanupasitcloses. Wecanalsogiveeventbindingsanidentifierandconnectthesebindingstokeyboard shortcutsviathewx.AcceleratorTableclass.Forexample,let’saddthiscodetobinda callbacktotheEsckey: quitCommandID=wx.NewId() self.Bind(wx.EVT_MENU,self._onQuitCommand, id=quitCommandID) acceleratorTable=wx.AcceleratorTable([ (wx.ACCEL_NORMAL,wx.WXK_ESCAPE, quitCommandID) ]) self.SetAcceleratorTable(acceleratorTable) Later,wewillimplementthecallbacksuchthattheEsckeymakesthewindowclose. NowthatwehaveawindowandabasicgraspofwxPythoneventbinding,let’sstart puttingGUIelementsinthere! CreatingandlayingoutimagesintheGUI Thewx.StaticBitmapclassisaGUIelementthatdisplaysawx.Bitmap.Usingthe followingcode,let’screateapairofStaticBitmapforourimagesofthesceneandthe board: self._sceneStaticBitmap=wx.StaticBitmap(self) self._boardStaticBitmap=wx.StaticBitmap(self) Later,wewillimplementahelpermethodtoshowtheCheckersModel'slatestimages.We willalsocallthishelpernowtoinitializetheBitmapsofStaticBitmap: self._showImages() TodefinethelayoutsoftheStaticBitmapsandotherwxPythonwidgets,wemustadd themtoakindofcollectioncalledwx.Sizer.Ithasseveraldirectandindirectsubclasses, suchaswx.BoxSizer(asimplehorizontalorverticallayout)andwx.GridSizer.Forthis project,wewillusewx.BoxSizeronly.ThefollowingcodeputsourtwoStaticBitmaps inahorizontallayout,withthesceneimagefirst(leftmost)andtheboardimagesecond: videosSizer=wx.BoxSizer(wx.HORIZONTAL) videosSizer.Add(self._sceneStaticBitmap) videosSizer.Add(self._boardStaticBitmap) TherestoftheGUIwillconsistofbuttons,aradiobox,andsliders,allforthepurposeof enablingtheusertoconfiguretheCheckersModel. Creatingandlayingoutcontrols Bydesign,severalpropertiesofourCheckersModelclassmustbeconfiguredinteractively. Apersonmustadjusttheseparameterswhileviewingtheresultsofboarddetectionand squareclassification.Adjustmentsshouldbenecessaryonlyaftertheboardisinitially detectedandafteranymajorchangesinthelighting. Rememberthattheboarddetectordoesnotlimititssearchtoanyrangeofrotations,andit maychoosearotationinanyquadrant.Theusermaywishtochangetherotationtomatch hisorhersubjectiveideaofwhichwaytheboardisfacing.Usingthefollowingcode,we willcreateabuttonlabeledRotateboardCCW,andwewillbindittoacallbackthatwill beresponsibleforadjustingtherotationbyincrementsof90degreescounterclockwise: rotateBoardButton=wx.Button( self,label='RotateboardCCW') rotateBoardButton.Bind( wx.EVT_BUTTON, self._onRotateBoardClicked) Alsorememberthatthedetectormayarbitrarilyfliptheboard,sinceitmakesno assumptionaboutwhetherornotthecameraismirrored.Let’screateandbindtheFlip boardXandFlipboardYbuttonstogivetheuseradditionalcontroloverthe perspective: flipBoardXButton=wx.Button( self,label='FlipboardX') flipBoardXButton.Bind( wx.EVT_BUTTON, self._onFlipBoardXClicked) flipBoardYButton=wx.Button( self,label='FlipboardY') flipBoardYButton.Bind( wx.EVT_BUTTON, self._onFlipBoardYClicked) Asthesquareclassifierreliesontheshadows’directiontodeterminewherethetallking pieceslie,weenabletheusertospecifytheshadows’directionusingaradiobox(asetof radiobuttons).TheboxislabeledShadowdirectionandtheoptionsareup,left,down, andright,asspecifiedinthefollowingcode: shadowDirectionRadioBox=wx.RadioBox( self,label='Shadowdirection', choices=['up','left','down','right']) shadowDirectionRadioBox.Bind( wx.EVT_RADIOBOX, self._onShadowDirectionSelected) Notethattheradiobuttonsintheboxarecollectivelyboundtoonecallback,whichis invokedwheneveranybuttonispressed. Lastamongthecontrols,wewillprovidesliderstoconfiguretheclassifier’sthreshold values.Thesedefineitsexpectationsaboutthecolorcontrastindifferentkindsofsquares. Thecodeshownherecallsahelpermethodtomakeandbindthreesliders,labeledEmpty threshold,Playerthreshold,andShadowthreshold: emptyFreqThresholdSlider=self._createLabeledSlider( 'Emptythreshold', self._checkersModel.emptyFreqThreshold*100, self._onEmptyFreqThresholdSelected) playerDistThresholdSlider=self._createLabeledSlider( 'Playerthreshold', self._checkersModel.playerDistThreshold*100, self._onPlayerDistThresholdSelected) shadowDistThresholdSlider=self._createLabeledSlider( 'Shadowthreshold', self._checkersModel.shadowDistThreshold*100, self._onShadowDistThresholdSelected) Note ElsewhereintheCheckersclass,weimplementthefollowingmethodtohelpcreate sliders: def_createLabeledSlider(self,label,initialValue,callback): slider=wx.Slider(self,size=(180,20)) slider.SetValue(initialValue) slider.Bind(wx.EVT_SLIDER,callback) staticText=wx.StaticText(self,label=label) sizer=wx.BoxSizer(wx.VERTICAL) sizer.Add(slider,0,wx.ALIGN_CENTER_HORIZONTAL) sizer.Add(staticText,0,wx.ALIGN_CENTER_HORIZONTAL) returnsizer Allourcontrolswillsharecertainlayoutproperties.Theywillbecenteredverticallyin theirsizerandwillhave8pixelsofpaddingontheirright-handside(toseparateeach controlfromitsneighbor).Let’sdeclarethesepropertiesonce,likethis: controlsStyle=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT controlsBorder=8 Usingtheseproperties,let’saddallthecontrolstoahorizontalsizer,asseeninthe followingcode: controlsSizer=wx.BoxSizer(wx.HORIZONTAL) controlsSizer.Add(rotateBoardButton,0, controlsStyle,controlsBorder) controlsSizer.Add(flipBoardXButton,0, controlsStyle,controlsBorder) controlsSizer.Add(flipBoardYButton,0, controlsStyle,controlsBorder) controlsSizer.Add(shadowDirectionRadioBox,0, controlsStyle,controlsBorder) controlsSizer.Add(emptyFreqThresholdSlider,0, controlsStyle,controlsBorder) controlsSizer.Add(playerDistThresholdSlider,0, controlsStyle,controlsBorder) controlsSizer.Add(shadowDistThresholdSlider,0, controlsStyle,controlsBorder) Nowwehaveallourcontrolsinarow! Nestinglayoutsandsettingtherootlayout Sizerscanbenested.Let’screateaverticalsizerandaddourtwoprevioussizerstoitso thatourimageswillappearfirst(topmost)andourcontrolssecond: rootSizer=wx.BoxSizer(wx.VERTICAL) rootSizer.Add(videosSizer) rootSizer.Add(controlsSizer,0,wx.EXPAND|wx.ALL, border=controlsBorder) Thewindowshouldadoptasizerastherootofthelayoutandshouldresizeitselftofit thislayout.Thenextlineofcodedoesthis: self.SetSizerAndFit(rootSizer) Now,wehaveaGUIwithalayoutandsomeeventbindings,buthowdowestartrunning updatestothecheckersanalyzer? Startingabackgroundthread TheCheckersModelclassencapsulatesalloftheheavyprocessinginthisproject, particularlythecaptureandanalysisofimages.Ifweranitsupdate()methodonthemain thread(whichwxPythonusesforGUIevents),theGUIwouldbecomeunresponsive, becauseupdate()wouldhogtheprocessorallthetime.Thus,wemustcreatea backgroundthreadthatupdate()maysafelymonopolize.Thefollowingcodeuses Python’sstandardthreading.Threadclasstorunagivenfunctiononanewthread: self._captureThread=threading.Thread( target=self._runCaptureLoop) self._running=True self._captureThread.start() Notethatweinitializedamembervariable,_running,beforewestartedthethread.Later, intheimplementationofthethread’sfunction,wewillusethe_runningvariableto controltheterminationofaloop. Alittlelater,wewillseetheimplementationofthe_runCaptureLoop()method,whichwe haveassignedtorunonthebackgroundthread.First,let’slookattheimplementationsof thecallbackmethodsthatwehaveboundtovariousGUIevents. Closingawindowandstoppingabackground thread Whenthewindowcloses,weneedtoensurethatthebackgroundthreadterminates normally,meaningthatthethread’sfunctionmustreturn.Thethreading.Threadclasshas amethodcalledjoin()thatblocksthecaller’sthreaduntilthecalleethreadreturns.Thus, itallowsustowaitforanotherthread’scompletion.Aswewillseelater,ourbackground threadcontinuesuntil_runningisFalse,sowemustsetthisbeforecallingjoin(). Finally,wemustcleanupthewx.FrameobjectbycallingitsDestroy()method.Hereis thecodefortherelevantcallback: def_onCloseWindow(self,event): self._running=False self._captureThread.join() self.Destroy() Note AlleventcallbacksinwxPythonrequireaneventargument.Dependingonthetypeof event,theeventargumentmayhavepropertiesthatgivedetailsabouttheuser’sinputor othercircumstances. Thewindowwillclose(andtheprecedingcallbackwillbecalled)whentheuserclickson thestandardclosebutton(X),orwhenwecallthewx.FrameobjectsClose()method. RememberthatwewantthewindowtoclosewhentheuserpressestheEsckey.Thus,the Esckeyismappedtothefollowingcallback: def_onQuitCommand(self,event): self.Close() Therestofourcallbacksrelatetocontrolsthatrepresenttheanalyzer’sconfiguration. Configuringtheanalyzerbasedonuserinput RememberthattheCheckersModelclasshasaboardRotationproperty.Thisisaninteger intherangeof0to3,representingcounterclockwiserotationin90-degreeincrements.The property’ssetterusesamodulusoperatortoensurethatavalueof4circlesbackto0,and soon.Thus,whentheuserclicksontheGUI’sRotateboardCCWbutton,wecansimply incrementboardRotation,likethis: def_onRotateBoardClicked(self,event): self._checkersModel.boardRotation+=1 Similarly,whentheuserclicksontheFlipboardXorFlipboardYbutton,wecan negatethevalueoftheflipBoardXorflipBoardYproperty(rememberthatthese propertiesarebooleans).Herearetherelevantcallbacks: def_onFlipBoardXClicked(self,event): self._checkersModel.flipBoardX=\ notself._checkersModel.flipBoardX def_onFlipBoardYClicked(self,event): self._checkersModel.flipBoardY=\ notself._checkersModel.flipBoardY LiketheboardRotationproperty,theshadowDirectionpropertyisanintegerthat representsacounterclockwiserotationin90-degreeincrements.Theoptionsinthe Shadowdirectioncontrolboxarearrangedinthesameorder(up,right,down,andleft). Conveniently,wxPythongivestheselectedoption’sindextothecallbackin event.Selection.WecanassignthisindextotheshadowDirectionproperty,asseenin thefollowingcode: def_onShadowDirectionSelected(self,event): self._checkersModel.shadowDirection=event.Selection Thethresholdproperties(emptyFreqThreshold,playerDistThreshold,and shadowDistThreshold)arefloating-pointnumbersintherangeof0.0to1.0.Bydefault, wxPython’sslidersinterprettheuser’sinputasanintegerintherangeof0to100.Thus, whentheusermovesaslider,wechangethecorrespondingthresholdtoone-hundredthof theslider’svalue,asseeninthesecallbacks: def_onEmptyFreqThresholdSelected(self,event): self._checkersModel.emptyFreqThreshold=\ event.Selection*0.01 def_onPlayerDistThresholdSelected(self,event): self._checkersModel.playerDistThreshold=\ event.Selection*0.01 def_onShadowDistThresholdSelected(self,event): self._checkersModel.shadowDistThreshold=\ event.Selection*0.01 Astheuserchangestheproperties,theeffectontheclassificationshouldbevisibleinreal time,orwithonlyamomentarylag.Let’sconsiderhowtoupdateanddisplaytheresults. Updatingandshowingimages Onthebackgroundthread,theapplicationwillcontinuallyaskitsCheckersModelinstance toupdatetheimagesandtheanalysis.Wheneveranupdatesucceeds,theapplicationmust alteritsGUItoshowthenewimages.ThisGUIeventmustbeprocessedonthemain (GUI)thread;otherwise,theapplicationwillcrash,becausetwothreadscannotaccessthe GUIobjectsatonce.Conveniently,wxPythonprovidesafunctioncalled wx.CallAfter(callableObj)tocallatargetfunction(orsomeothercallableobject)on themainthread.Thefollowingcodeimplementsourbackgroundthread’sloop,which terminateswhen_runningisFalse: def_runCaptureLoop(self): whileself._running: ifself._checkersModel.update(): wx.CallAfter(self._showImages) Onthemainthread,wegettheCheckersModelimagesoftheunprocessedsceneandthe processedboard,convertthemtowxPython’sBitmapformatusingourutilityfunction, anddisplaythemintheapplication’sStaticBitmap.Thesetwomethodscarryoutthis work: def_showImages(self): self._showImage( self._checkersModel.scene,self._sceneStaticBitmap, self._checkersModel.sceneSize) self._showImage( self._checkersModel.board,self._boardStaticBitmap, self._checkersModel.boardSize) def_showImage(self,image,staticBitmap,size): ifimageisNone: #Provideablackbitmap. bitmap=wx.EmptyBitmap(size[0],size[1]) else: #Converttheimagetobitmapformat. bitmap=WxUtils.wxBitmapFromCvImage(image) #Showthebitmap. staticBitmap.SetBitmap(bitmap) ThisistheendoftheCheckersclass,butwestillneedonemorefunctionintheCheckers module. Runningtheapplication Let’swriteamain()functiontolaunchaninstanceoftheCheckersapplicationclass.This functionmustalsocreatetheapplication’sinstanceofCheckersModel.Wewillallowthe usertospecifythecameraindexasacommand-lineargument.Forexample,iftheuser runsthefollowingcommand,CheckersModelwilluseacameraindexof1: $pythonCheckers.py1 Hereisthemain()function’simplementation: defmain(): importsys iflen(sys.argv)<2: cameraDeviceID=0 else: cameraDeviceID=int(sys.argv[1]) checkersModel=CheckersModel.CheckersModel( cameraDeviceID=cameraDeviceID) app=wx.App() checkers=Checkers(checkersModel) checkers.Show() app.MainLoop() if__name__=='__main__': main() That’sallforthecode!Let’sgrabawebcamandacheckersset! Troubleshootingtheprojectinreal-world conditions Althoughthisprojectshouldworkwithmanycheckerssets,cameraperspectives,and lightingsetups,itstillrequirestheusertosetthestagecarefully,inaccordancewiththe followingguidelines: 1. Ensurethatthewebcamandtheboardarestationary.Thewebcamshouldbeona tripodorsomeothermount.Theboardshouldalsobesecuredinplace,oritshould besufficientlyheavysothatitdoesnotmovewhentheplayerstouchit. 2. Leavetheboardemptyuntiltheapplicationdetectsitanddisplaysthebird’s-eye view.Ifthereareplayingpiecesontheboard,theywillprobablyinterferewiththe initialdetection. 3. Forbestresults,useablack-and-whitecheckerboard.Othercolorschemes(suchas darkwoodandlightwood)maywork. 4. Forbestresults,useaboardwithalightborderaroundtheplayingarea. 5. Also,forbestresults,useaboardwithamattefinishornofinish.Reflectionsona glossyboardwillprobablyinterferewiththeanalysisofthedominantcolorsinsome squares. 6. Puttheplayingpiecesonthedarksquares. 7. Useplayingpiecesthatarenotofthesamecolorasthedarksquares.Forexample,on ablackandwhiteboard,useredandwhite(orredandgray)pieces. 8. Useplayingpiecesthataresufficientlytall.Aking(apairofstackedpieces)must castashadowonanadjacentsquare. 9. Ensurethatthescenehasbrightlightcomingpredominantlyfromonedirection,such asdaylightfromawindow.Alsoensurethattheboardisalignedsuchthatthelightis approximatelyparalleltothexorygridlines.Thus,akings’shadowwillfallonan adjacentlightsquare. Experimentwithvariousreal-worldconditionsandvarioussettingsintheapplicationto seewhatworksandwhatfails.Everycomputervisionsystemhaslimits.Likeaperson,it doesnotseeeverythingclearly,anditdoesnotunderstandallthatitsees.Aspartofour testingprocess,weshouldalwaysstrivetofindthesystem’slimits.Thiswilldriveusto adoptorinventmorerobusttechniquesforourfuturework. FurtherreadingonOpenCV Ourcheckersapplicationgivesaglimpseoftheworldofcomputervision.Yet,thereis muchmoretosee!ForPythonprogrammers,thefollowingbooksfromPacktPublishing revealabroadrangeofOpenCV’sfunctionality,withimpressiveapplications: LearningOpenCV3ComputerVisionwithPython(October2015),byJoeMinichino andJosephHowse:ThisisagrandtourofOpenCV’sPythoninterfaceandthe underlyingtheoriesincomputervision,machinelearning,andartificialintelligence. Withagentlelearningcurve,itissuitableforeitherbeginnersorthosewhowantto roundouttheirknowledgeofthelibraryanditsuses. RaspberryPiComputerVisionProgramming(May2015),byAshwinPajankar:This beginner-friendlybookemphasizestechniquesthatarepracticalforlow-cost,lowpoweredplatformssuchastheRaspberryPisingle-boardcomputers. OpenCVforSecretAgents(January,2015),byJosephHowse:Thisisacollectionof creative,adventurousprojectsforintermediatetoadvanceddeveloperswhomaybe newtocomputervision.Ifyouwanttoperformabiometricrecognitionofacator seepeople’sheartbeatsthroughamotion-amplifyingwebcam,thenthisisthe(only) bookforyou! Besidestheseoptions,PacktPublishingoffersmorethanadozenotherbooksonOpenCV andcomputervisionforC++,Java,Python,iOS,andAndroiddevelopers.Wehopeto meetyouagaininanexplorationofthisexcitingandburgeoningtopic! Summary Whileanalyzingagameofcheckers,weencounteredseveralfundamentalaspectsof computervisioninthefollowingtasks: Capturingimagesfromacamera Performingcomputationsoncolorandgrayscaledata Detectingandrecognizingasetoffeatures(thecornersoftheboard’ssquares) Trackingmovementsofthefeatures Simulatingadifferentperspective(abird’s-eyeviewoftheboard) Classifyingtheregionsoftheimage(thecontentsoftheboard’ssquares) WealsosetupandusedOpenCVandotherlibraries.Havingbuiltacompleteapplication usingtheselibraries,youareinabetterpositiontounderstandthepotentialofcomputer visionandplanfurtherstudiesandprojectsinthisfield. ThischapteralsoconcludesourjourneytogetherinPythonGameProgrammingby Example.Webuiltmodernversionsofclassicvideogamesaswellasananalyzerfora classicboardgame.Alongtheway,yougainedpracticalexperienceinmanyofthemajor gameengines,graphicsAPIs,GUIframeworks,andscientificlibrariesforPython.These skillsaretransferabletootherlanguagesandlibraries,andwillserveasyourfoundation againandagainasyoubuildyourowngamesandotherpiecesofvisualsoftware! Gocreatesomemoregreatprojects,sharethefun,andstayintouchwithus(Alejandro RodasdePazat<[email protected]>andJosephHowseathttp://nummist.com/)! Index A AARectShapeclass/Processingcollisions Actor/SpaceInvadersdesign add(obj)method/Processingcollisions add_ballmethod/AddingtheBreakoutitems add_brickmethod/AddingtheBreakoutitems analyzer building/Buildingtheanalyzer access,providingtoimages/Providingaccesstotheimagesandclassification results access,providingtoclassificationresults/Providingaccesstotheimagesand classificationresults access,providingtoparametersforuserconfiguration/Providingaccessto parametersfortheusertoconfigure entiregamemodel,initializing/Initializingtheentiremodelofthegame entiregamemodel,updating/Updatingtheentiremodelofthegame image,capturing/Capturingandconvertinganimage image,converting/Capturingandconvertinganimage board’scorners,detecting/Detectingtheboard’scornersandtrackingtheir motion board’smotion,tracking/Detectingtheboard’scornersandtrackingtheir motion bird’s-eyeview,creating/Creatingandanalyzingthebird’s-eyeviewofthe board dominantcolors,analyzinginsquare/Analyzingthedominantcolorsina square contentsofasquare,classifying/Classifyingthecontentsofasquare text,drawing/Drawingtext ApplePython/Mac Arbiter/Addingphysics arrivalbehavior/Arrival B ball movement/Movementandcollisions collisions/Movementandcollisions Ballclass/TheBallclass basicgameobjects adding,togamelayer/Thegamelayer Breakoutgame overview/AnoverviewofBreakout playing/PlayingBreakout Breakoutitems adding/AddingtheBreakoutitems gameobjectinstantiation/AddingtheBreakoutitems keyinputbinding/AddingtheBreakoutitems Brickclass/TheBrickclass C cameras configuring/Configuringcameras Canvaswidget about/DivingintotheCanvaswidget canvaswidget canvas.coords()method/DivingintotheCanvaswidget canvas.move()method/DivingintotheCanvaswidget canvas.delete()method/DivingintotheCanvaswidget canvas.winfo_width()method/DivingintotheCanvaswidget canvas.itemconfig()method/DivingintotheCanvaswidget canvas.bind()method/DivingintotheCanvaswidget canvas.unbind()method/DivingintotheCanvaswidget canvas.create_text()method/DivingintotheCanvaswidget canvas.find_withtag()method/DivingintotheCanvaswidget canvas.find_overlapping()method/DivingintotheCanvaswidget references,URL/DivingintotheCanvaswidget Checkersapplication planning/PlanningtheCheckersapplication check_collisionsmethod/Startingthegame Chimpunk2D URL/IntroducingPymunk CircleShapeclass/Processingcollisions clear()method/Processingcollisions Cocos2d actions/Cocos2dactions cocos2d installing/Installingcocos2d about/Gettingstartedwithcocos2d scene/Gettingstartedwithcocos2d layer/Gettingstartedwithcocos2d sprite/Gettingstartedwithcocos2d director/Gettingstartedwithcocos2d classmembers/Gettingstartedwithcocos2d fullscreenparameter/Gettingstartedwithcocos2d resizableparameter/Gettingstartedwithcocos2d vsyncparameter/Gettingstartedwithcocos2d widthparameter/Gettingstartedwithcocos2d heightparameter/Gettingstartedwithcocos2d captionparameter/Gettingstartedwithcocos2d visibleparameter/Gettingstartedwithcocos2d userinput,handling/Handlinguserinput scene,updating/Updatingthescene collisions,processing/Processingcollisions Shootactor/Shoot’emup! HUD,adding/AddinganHUD mysteryship/Extrafeature–themysteryship Cocos2dactions about/Cocos2dactions instantactions/Cocos2dactions,Instantactions intervalactions/Intervalactions combining/Combiningactions customactions/Customactions CocosNode/Gettingstartedwithcocos2d collisiondetectiongame about/Basiccollisiondetectiongame CollisionManagerinterface about/Processingcollisions CollisionManagerBruteForce/Processingcollisions CollisionManagerGrid/Processingcollisions colors workingwith/Workingwithcolors componentbaseclass/Component-basedgameengines Cshape/Processingcollisions customactions about/Customactions D distanceformulation CentOS,settingupTopicnURL/Workingwithcolors Domain-specificLanguage(DSL)/Thescenariodefinition draw_textmethod/AddingtheBreakoutitems E evadebehavior/Pursuitandevade F faceculling about/TheCubeclass enabling/Enablingfaceculling fleebehavior/Seekandflee framespersecond(FPS)/Pygame101 freeglut/Installingpackages futureposition/Pursuitandevade G game starting/Startingthegame gameassets creating/Creatinggameassets Gameclass/TheGameclass gamedesign about/Anintroductiontogamedesign leveldesign/Leveldesign platformerskills/Platformerskills component-basedgameengines/Component-basedgameengines gameframework building/Buildingagameframework start()method/Buildingagameframework update(dt)method/Buildingagameframework on_collide(other,contacts)method/Buildingagameframework stop()method/Buildingagameframework physics,adding/Addingphysics renderablecomponents/Renderablecomponents InputManagermodule/TheInputManagermodule Gameclass/TheGameclass gamelayer basicgameobjects,addingto/Thegamelayer GameLayerclass/ThePlayerCannonandGameLayerclasses gameobjects about/Basicgameobjects Ballclass/TheBallclass Paddleclass/ThePaddleclass Brickclass/TheBrickclass GameObjects/Component-basedgameengines gamescene about/Gamescene HUDclass/TheHUDclass assembling/Assemblingthescene glClear()function/Drawingshapes glClearColor()function/Initializingthewindow glColorfunctions/Initializingthewindow glLightfv()function/Drawingshapes glLoadIdentity()function/Drawingshapes glMaterialfv()function/Drawingshapes glMatrixMode()function/Initializingthewindow glPopMatrix()function/Drawingshapes glPushMatrix()function/Drawingshapes glTranslate()function/Drawingshapes gluLookAt()function/Drawingshapes gluPerspective() function/Initializingthewindow glutDisplayFunc()function/Initializingthewindow GLUTlicensing/GettingstartedwithOpenGL glutMainLoop()function/Initializingthewindow glutSolidSphere()function/Drawingshapes glutSpecialFunc()function/Initializingthewindow,Processingtheuserinput gravitationgame about/Gravitationgame basicgameobjects/Basicgameobjects planets/Planetsandpickups pickups/Planetsandpickups player/Playerandenemies enemies/Playerandenemies explosions/Explosions GUIapplication building/BuildingtheGUIapplication window,creating/Creatingawindowandbindingevents events,binding/Creatingawindowandbindingevents images,layingout/CreatingandlayingoutimagesintheGUI images,creating/CreatingandlayingoutimagesintheGUI controls,layingout/Creatingandlayingoutcontrols controls,creating/Creatingandlayingoutcontrols layouts,nesting/Nestinglayoutsandsettingtherootlayout rootlayout,setting/Nestinglayoutsandsettingtherootlayout backgroundthread,starting/Startingabackgroundthread window,closing/Closingawindowandstoppingabackgroundthread backgroundthread,stopping/Closingawindowandstoppingabackground thread analyzerbasedonuserinput,configuring/Configuringtheanalyzerbasedon userinput images,updating/Updatingandshowingimages images,displaying/Updatingandshowingimages running/Runningtheapplication GUIlayout/ThebasicGUIlayout H homography/Initializingtheentiremodelofthegame I installation Python/InstallingPython installation,NumPy/NumPyinstallation instantactions about/Cocos2dactions,Instantactions Place/Instantactions CallFunc/Instantactions CallFuncS/Instantactions Hide/Instantactions Show/Instantactions ToggleVisibility/Instantactions IntegratedDevelopmentEnvironments(IDE) about/Aquickdemonstration intervalactions about/Intervalactions Lerp/Intervalactions MoveTo/Intervalactions MoveBy/Intervalactions JumpTo/Intervalactions JumpBy/Intervalactions Bezier/Intervalactions Blink/Intervalactions RotateTo/Intervalactions RotateBy/Intervalactions ScaleTo/Intervalactions ScaleBy/Intervalactions FadeOut/Intervalactions FadeIn/Intervalactions FadeTo/Intervalactions Delay/Intervalactions RandomDelay/Intervalactions invaders about/Invaders! Alienclass/Invaders! AlienColumnclass/Invaders! AlienGroup.class/Invaders! iter_colliding(obj)method/Processingcollisions K k-meansclustering/Initializingtheentiremodelofthegame knownentities/Processingcollisions knows(obj)method/Processingcollisions L layer/Cocos2dactions learningcurve/Leveldesign M Mac settingup/Mac MacPorts URL/Mac mainmenu adding/Addingamainmenu mathmodule about/Intervalactions URL/Intervalactions Menu/Addingamainmenu move()method/AddingtheBreakoutitems N NumPy installing/NumPyinstallation URL,forofficialbinaries/NumPyinstallation URL,forunofficialcompiledbinaries/NumPyinstallation O obstacleavoidancebehavior about/Obstacleavoidance OpenCV settingup/SettingupOpenCVandotherdependencies dependencies,settingup/SettingupOpenCVandotherdependencies Windows,settingup/Windows URL/Windows Mac,settingup/Mac Debian,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,and LinuxMint Raspbian,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu, andLinuxMint Ubuntu,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,and LinuxMint LinuxMint,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu, andLinuxMint Fedora,settingup/Fedoraanditsderivatives,includingRHELandCentOS Fedora,derivates/Fedoraanditsderivatives,includingRHELandCentOS RHEL,settingup/Fedoraanditsderivatives,includingRHELandCentOS CentOS,settingup/Fedoraanditsderivatives,includingRHELandCentOS OpenSUSE/OpenSUSEanditsderivatives OpenSUSE,derivates/OpenSUSEanditsderivatives multipleversions,supporting/SupportingmultipleversionsofOpenCV references/FurtherreadingonOpenCV OpenCVimages converting,forwxPython/ConvertingOpenCVimagesforwxPython OpenGL about/GettingstartedwithOpenGL window,initializing/Initializingthewindow shapes,drawing/Drawingshapes demo,running/Runningthedemo program,refactoring/RefactoringourOpenGLprogram userinput,processing/Processingtheuserinput used,fordrawing/DrawingwithOpenGL Cubeclass/TheCubeclass faceculling,enabling/Enablingfaceculling OpenGLUtilityLibrary/Initializingthewindow OpenGLUtilityToolkit/GettingstartedwithOpenGL opticalflow/Detectingtheboard’scornersandtrackingtheirmotion P packageinstallation PyOpenGL/Installingpackages freeglut/Installingpackages Pygame/Installingpackages packages installing/Installingpackages Paddleclass/ThePaddleclass ParticleSystemclass about/TheParticleSystemclass demonstrating/Aquickdemonstration PlayerCannonclass/ThePlayerCannonandGameLayerclasses Playerclass about/ThePlayerclassanditscomponents respawn/ThePlayerclassanditscomponents PlayerMovement/ThePlayerclassanditscomponents project troubleshooting/Troubleshootingtheprojectinreal-worldconditions pursuitbehavior/Pursuitandevade Pygame about/Installingpackages URL/Installingpackages,Pygame101 Macintosh,URL/Installingpackages adding/AddingthePygamelibrary 101/Pygame101 documentation/Pygame101 integration/Pygameintegration Pyglet about/Installingcocos2d eventframework/Handlinguserinput pyglet.window.keymodule URL/Handlinguserinput Pymunk about/IntroducingPymunk pymunkpackage,classes space/IntroducingPymunk body/IntroducingPymunk shape/IntroducingPymunk arbiter/IntroducingPymunk PyOpenGL about/Installingpackages URL/Installingpackages,Initializingthewindow PyPlatformer developing/DevelopingPyPlatformer platforms,creating/Creatingtheplatforms pickups,adding/Addingpickups shootingaction/Shooting! Playerclass/ThePlayerclassanditscomponents class/ThePyPlatformerclass PyPlatformerclass/ThePyPlatformerclass pyramidalLukas-Kanade/Detectingtheboard’scornersandtrackingtheirmotion Python installing/InstallingPython URL/InstallingPython packagingecosystem/Installingcocos2d specialmethods/Combiningactions Python2 andTkinter/InstallingPython PythonExtensionPackages URL/Installingpackages Pythonmodules,Checkersapplication Checkers.py/PlanningtheCheckersapplication CheckersModel.py/PlanningtheCheckersapplication WxUtils.py/PlanningtheCheckersapplication ResizeUtils.py/PlanningtheCheckersapplication ColorUtils.py/PlanningtheCheckersapplication CVBackwardCompat.py/PlanningtheCheckersapplication PythonPackageIndex(PyPi)/Installingcocos2d R render()method/Buildingagameframework renderablecomponents about/Renderablecomponents cameracomponent/TheCameracomponent S scenario definition/Thescenariodefinition scenarioclass/Thescenarioclass scenariomodule/Thescenarioclass scenes about/Cocos2dactions transitionsbetween/Transitionsbetweenscenes gameovercutscene/Gameovercutscene seekbehavior/Seekandflee SimpleDirectMediaLayer(SDL)/Pygameintegration Single-boardComputer(SBC)/ConvertingOpenCVimagesforwxPython slowingarea/Arrival SpaceInvadersdesign about/SpaceInvadersdesign PlayerCannonclass/SpaceInvadersdesign,ThePlayerCannonandGameLayer classes Alienclass/SpaceInvadersdesign AlienColumnclass/SpaceInvadersdesign AlienGroupclass/SpaceInvadersdesign Shootclass/SpaceInvadersdesign PlayerShootclass/SpaceInvadersdesign GameLayerclass/ThePlayerCannonandGameLayerclasses sprite/Cocos2dactions startmethod glutInit()/Initializingthewindow glutInitWindowPosition()/Initializingthewindow glutInitWindowsSize()/Initializingthewindow glutCreateWindow()/Initializingthewindow glEnable()/Initializingthewindow start_gamemethod/Startingthegame steeringbehaviors implementing/Implementingsteeringbehaviors seek/Seekandflee flee/Seekandflee arrival/Arrival pursuit/Pursuitandevade evade/Pursuitandevade wander/Wander obstacleavoidance/Obstacleavoidance steeringbehaviors,forautonomouscharacters referencelink/Implementingsteeringbehaviors subclasses,MenuItem ToggleMenuItem/Addingamainmenu MultipleMenuItem/Addingamainmenu EntryMenuItem/Addingamainmenu ImageMenuItem/Addingamainmenu ColorMenuItem/Addingamainmenu super(MyClass,self).__init__(arguments)/ThebasicGUIlayout syntacticsugar/Combiningactions T ThreadBuildingBlocks(TBB)/Mac TiledMapEditor URL/TiledMapEditor about/TiledMapEditor tilemaps about/Tilemaps TiledMapEditor/TiledMapEditor tiles,loading/Loadingtiles Tkinter URL/InstallingPython andPython2/InstallingPython TMXformat/TiledMapEditor towerdefenseactors about/Thetowerdefenseactors turret/Turretsandslots slots/Turretsandslots enemies/Enemies bunker/Bunker towerdefensegameplay about/Thetowerdefensegameplay U update()method/Buildingagameframework updatemethod/Movementandcollisions update_lives_textmethod/AddingtheBreakoutitems V values,wanderbehavior wander_angle/Wander circle_distance/Wander circle_radius/Wander angle_change/Wander W wanderbehavior about/Wander values/Wander Windows URL/Windows settingup/Windows wxPython URL/Windows OpenCVimages,converting/ConvertingOpenCVimagesforwxPython wxPythonPhoenix URL/Windows,Mac X XcodeCommandLineTools/Mac