Python Game Programming By Example - Programmer Books · Benjamin Johnson is an experienced Python...
Transcript of Python Game Programming By Example - Programmer Books · Benjamin Johnson is an experienced Python...
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
PythonGameProgrammingByExampleCopyright©2015PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthors,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:September2015
Productionreference:1230915
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78528-153-2
www.packtpub.com
CreditsAuthors
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
AbouttheAuthorsAlejandroRodasdePazisacomputerengineerandgamedeveloperfromSeville,Spain.
HecameacrossPythonbackin2009,whilehewasstudyingattheUniversityofSeville.AlejandrodevelopedseveralacademicprojectswithPython,fromwebcrawlerstoartificialintelligencealgorithms.Inhissparetime,hestartedbuildinghisowngamesinPython.HedidaminoringamedesignatHogeschoolvanAmsterdam,wherehecreatedasmall3Dgameenginebasedontheideashelearnedduringthisminor.
Hehasalsodevelopedsomeopensourceprojects,suchasaPythonAPIforthePhilipsHuepersonallightingsystem.YoucanfindtheseprojectsinhisGitHubaccountathttps://github.com/aleroddepaz.
Priortothispublication,AlejandrocollaboratedwithPacktPublishingasatechnicalrevieweronthebookTkinterGUIApplicationDevelopmentHotshot.
Iwouldliketothankmyparents,FelicianoandMaríaTeresa,fortheirabsolutetrustandsupport.Theyhavebeenaninspirationtomeandanexampleofhardwork.
Iwouldalsoliketothankmygirlfriend,Lucía,forherloveandforputtingupwithmewhileIworkedonthisbook.
JosephHowseisawriter,softwaredeveloper,andbusinessownerfromHalifax,NovaScotia,Canada.Computergamesandcodeareimbibedinhisearliestmemories,ashelearnedtoreadandtypebyplayingtextadventureswithhisolderbrother,Sam,andwatchinghimwritegraphicsdemosinBASIC.
Joseph’sotherbooksincludeOpenCVforSecretAgents,OpenCVBlueprints,AndroidApplicationProgrammingwithOpenCV3,andLearningOpenCV3ComputerVisionwithPython.Heworkswithhiscatstomakecomputervisionsystemsforhumans,felines,andotherusers.Visithttp://nummist.comtoreadaboutsomeofhislatestprojectsdoneatNummistMediaCorporationLimited.
IdedicatemyworktoSam,Jan,Bob,Bunny,andmycats,whohavebeenmylifelongguidesandcompanions.
Icongratulatemycoauthorforproducinganexcellentcompendiumofclassicexamplesofgamedevelopment.Iamgratefulfortheopportunitytoaddmychapteroncheckers(draughts)andcomputervision.
Iamalsoindebtedtothemanyeditorsandtechnicalreviewerswhohavecontributedtoplanning,polishing,andmarketingthisbook.IhavecometoexpectanoutstandingteamwhenworkingwithPacktPublishing,andonceagain,allofthemhaveguidedmewiththeirexperienceandsavedmefromsundryerrorsandomissions.Pleasemeetthetechnicalreviewersbyreadingtheirbiographieshere.
Finally,Iwanttothankmyreadersandeverybodyintheopensourcecommunity.Weareunitedinoureffortstobuildandshareallkindsofprojectsandknowledge,pavingthewayforbookssuchasthistosucceed.
AbouttheReviewersBenjaminJohnsonisanexperiencedPythonprogrammerwithapassionforgameprogramming,softwaredevelopment,andwebdesign.HeiscurrentlystudyingcomputerscienceatTheUniversityofTexasatAustinandplanstospecializeinsoftwareengineering.HismostpopularPythonprojectsincludeanadventuregameengineandaparticlesimulator,bothdevelopedusingPygame.YoucancheckoutBenjamin’slatestPygameprojectsandarticlesonhiswebsiteatwww.learnpygame.com.
IwouldliketothankPacktPublishingforgivingmetheopportunitytoreadandreviewthisexcellentbook!
DennisO’BrienisthedirectorofdatascienceatGameShowNetworkGames.HestudiedphysicsattheUniversityofChicagoasanundergraduateandcompletedhisgraduatestudiesincomputersciencefromtheUniversityofIllinois,Chicago.HewastheprincipalsoftwareengineeratElectronicArts,aseniorsoftwareengineeratLeapfrogEnterprises,andaleadgamedeveloperatJellyvisionGames.
Supportfiles,eBooks,discountoffers,andmoreForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
FreeaccessforPacktaccountholdersIfyouhaveanaccountwithPacktatwww.PacktPub.com,youcanusethistoaccessPacktLibtodayandview9entirelyfreebooks.Simplyuseyourlogincredentialsforimmediateaccess.
PrefaceWelcometoPythonGameProgrammingByExample.Ashobbyistprogrammersorprofessionaldevelopers,wemaybuildawidevarietyofapplications,fromlargeenterprisesystemstowebapplicationsmadewithstate-of-the-artframeworks.However,gamedevelopmenthasalwaysbeenanappealingtopic,maybesimplyforcreatingcasualgamesandnotjustforhigh-budgetAAAtitles.
IfyouwanttoexplorethedifferentwaysofdevelopinggamesinPython,alanguagewithclearandsimplesyntax,thenthisisthebookforyou.Ineachchapter,wewillbuildanewgamefromscratch,usingseveralpopularlibrariesandutilities.Bytheendofthisbook,youwillbeabletoquicklycreateyourown2Dand3Dgames,andhaveahandfulofPythonlibrariesinyourtoolbelttochoosefrom.
WhatthisbookcoversChapter1,Hello,Pong!,detailstherequiredsoftware,itsinstallation,andthebasicsyntaxofPython:datastructures,controlflowstatements,objectorientation,andsoon.Italsoincludesthefirstgameofthebook,theclassic“Hello,world”game.
Chapter2,CocosInvaders,introducesthecocos2dgameengineandexplainshowtobuildagamesimilartoSpaceInvaderstoputthisknowledgeintopractice.Here,youlearnthebasicsofcollisions,inputhandling,andscenesetup.
Chapter3,BuildingaTowerDefenseGame,iswhereyoulearntodevelopafull-fledgedgamewithcocos2d.Thisgameincludessomeinterestingcomponents,suchasaHUDandamainmenu.
Chapter4,SteeringBehaviors,coversseeminglyintelligentmovementsforautonomouscharacters.Youwillbeaddingthesestrategiesgradually,indifferentlevelsofabasicgamebuiltwithparticlesystems.
Chapter5,Pygameand3D,presentsthefoundationsof3DandguidesyouthroughthebasicstructureofanOpenGLprogram.
Chapter6,PyPlatformer,iswhereyoudevelopa3Dplatformergamewithallthetechniqueslearnedinthepreviouschapter.
Chapter7,AugmentingaBoardGamewithComputerVision,introducesthetopicofcomputervision,whichallowssoftwaretolearnabouttherealworldviaacamera.Inthischapter,youbuildasystemtoanalyzeagameofcheckers(draughts)inrealtimeasplayersmovepiecesonaphysicalboard.
WhatyouneedforthisbookTheprojectscoveredinthisbookassumethatyouhaveinstalledPython3.4onacomputerwithWindows,MacOSX,orLinux.Wealsoassumethatyouhaveincludedpipduringtheinstallationprocess,sinceitwillbethepackagemanagerusedtoinstalltherequiredthird-partypackages.
WhothisbookisforIfyouhaveeverwantedtocreatecasualgamesinPythonandyouwishtoexplorethevariousGUItechnologiesthatthislanguageoffers,thenthisisthebookforyou.ThistitleisintendedforbeginnersinPythonwithlittleornoknowledgeofgamedevelopment,anditcoversstepbystephowtobuildsevendifferentgames,fromthewell-knownSpaceInvaderstoaclassical3Dplatformer.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“Forinstance,onUbuntu,youneedtoinstallthepython3-tkpackage.”
Ablockofcodeissetasfollows:
new_list=[]
forelemincollection:
ifelemisnotNone:
new_list.append(elem)
Anycommand-lineinputoroutputiswrittenasfollows:
$python–-version
Python3.4.3
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:“MakesurethatyouchecktheTcl/Tkoptiontoincludethelibrary.”
NoteWarningsorimportantnotesappearinaboxlikethis.
TipTipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail<[email protected]>,andmentionthebook’stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.comforallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Additionally,up-to-dateexamplecodeforChapter7,AugmentingaBoardGamewithComputerVision,ispostedathttp://nummist.com/opencv.
DownloadingthecolorimagesofthisbookWealsoprovideyouwithaPDFfilethathascolorimagesofthescreenshots/diagramsusedinthisbook.Thecolorimageswillhelpyoubetterunderstandthechangesintheoutput.Youcandownloadthisfilefromhttps://www.packtpub.com/sites/default/files/downloads/B04505_Graphics.pdf.
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
Additionally,anyerrataforChapter7,AugmentingaBoardGamewithComputerVision,willbepostedathttp://nummist.com/opencv.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<[email protected]>,andwewilldoourbesttoaddresstheproblem.
Youcanalsocontacttheauthorsdirectly.AlejandraRodasdePaz,authorofChapters1to6,canbereachedat<[email protected]>.JosephHowse,authorofChapter7,canbereachedat<[email protected]>,andanswerstocommonquestionscanbefoundonhiswebsite,http://nummist.com/opencv.
Chapter1.Hello,Pong!Gamedevelopmentisahighlyevolvingsoftwaredevelopmentprocess,andithasimprovedcontinuouslysincetheappearanceofthefirstvideogamesinthe1950s.Nowadays,thereareawidevarietyofplatformsandengines,andthisprocesshasbeenfacilitatedwiththearrivalofopensourcetools.
Pythonisafreehigh-levelprogramminglanguagewithadesignintendedtowritereadableandconciseprograms.Thankstoitsphilosophy,wecancreateourowngamesfromscratchwithjustafewlinesofcode.ThereareaplentyofgameframeworksforPython,butforourfirstgame,wewillseehowwecandevelopitwithoutanythird-partydependency.
Inthischapter,wewillcoverthefollowingtopics:
InstallationoftherequiredsoftwareAnoverviewofTkinter,aGUIlibraryincludedinthePythonstandardlibraryApplyingobject-orientedprogrammingtoencapsulatethelogicofourgameBasiccollisionandinputdetectionDrawinggameobjectswithoutexternalassetsDevelopingasimplifiedversionofBreakout,apong-basedgame
InstallingPythonYouwillneedPython3.4withTcl/Tk8.6installedonyourcomputer.ThelatestbranchofthisversionisPython3.4.3,whichcanbedownloadedfromhttps://www.python.org/downloads/.Here,youcanfindtheofficialbinariesforthemostpopularplatforms,suchasWindowsandMacOS.Duringtheinstallationprocess,makesurethatyouchecktheTcl/Tkoptiontoincludethelibrary.
ThecodeexamplesincludedinthebookhavebeentestedagainstWindows8andMac,butcanberunonLinuxwithoutanymodification.NotethatsomedistributionsmayrequireyoutoinstalltheappropriatepackageforPython3.Forinstance,onUbuntu,youneedtoinstallthepython3-tkpackage.
OnceyouhavePythoninstalled,youcanverifytheversionbyopeningCommandPromptoraterminalandexecutingtheselines:
$python--version
Python3.4.3
Afterthischeck,youshouldbeabletostartasimpleGUIprogram:
$python
>>>fromtkinterimportTk
>>>root=Tk()
>>>root.title('Hello,world!')
>>>root.mainloop()
Thesestatementscreateawindow,changeitstitle,andrunindefinitelyuntilthewindowisclosed.Donotclosethenewwindowthatisdisplayedwhenthesecondstatementisexecuted.Otherwise,itwillraiseanerrorbecausetheapplicationhasbeendestroyed.
Wewillusethislibraryinourfirstgame,andthecompletedocumentationofthemodulecanbefoundathttps://docs.python.org/3/library/tkinter.html.
TipTkinterandPython2
TheTkintermodulewasrenamedtotkinterinPython3.IfyouhavePython2installed,simplychangetheimportstatementwithTkinterinuppercase,andtheprogramshouldrunasexpected.
AnoverviewofBreakoutTheBreakoutgamestartswithapaddleandaballatthebottomofthescreenandsomerowsofbricksatthetop.Theplayermusteliminateallthebricksbyhittingthemwiththeball,whichreboundsagainstthebordersofthescreen,thebricks,andthebottompaddle.AsinPong,theplayercontrolsthehorizontalmovementofthepaddle.
Theplayerstartsthegamewiththreelives,andiftheymisstheball’sreboundanditreachesthebottomborderofthescreen,onelifeislost.Thegameisoverwhenallthebricksaredestroyed,orwhentheplayerlosesalltheirlives.
Thisisascreenshotofthefinalversionofourgame:
ThebasicGUIlayoutWewillstartoutgamebycreatingatop-levelwindowasinthesimpleprogramweranpreviously.However,thistime,wewillusetwonestedwidgets:acontainerframeandthecanvaswherethegameobjectswillbedrawn,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()
TipDownloadingtheexamplecode
Youcandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.comforallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Throughthetkalias,weaccesstheclassesdefinedinthetkintermodule,suchasTk,Frame,andCanvas.
Noticethefirstargumentofeachconstructorcallwhichindicatesthewidget(thechildcontainer),andtherequiredpack()callsfordisplayingthewidgetsontheirparentcontainer.ThisisnotnecessaryfortheTkinstance,sinceitistherootwindow.
However,thisapproachisnotexactlyobject-oriented,sinceweuseglobalvariablesanddonotdefineanynewclassestorepresentournewdatastructures.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.TheclassGame(tk.Frame):definitionspecifiesthenameoftheclassandthesuperclassbetweenparentheses.
Ifyouarenewtoobject-orientedprogrammingwithPython,thissyntaxmaynotsoundfamiliar.Inourfirstlookatclasses,themostimportantconceptsarethe__init__methodandtheselfvariable:
The__init__methodisaspecialmethodthatisinvokedwhenanewclassinstanceiscreated.Here,wesettheobjectattributes,suchasthewidth,theheight,andthecanvaswidget.Wealsocalltheparentclassinitializationwiththesuper(Game,self).__init__(master)statement,sotheinitialstateoftheFrameisproperlyinitialized.Theselfvariablereferstotheobject,anditshouldbethefirstargumentofamethodifyouwanttoaccesstheobjectinstance.Itisnotstrictlyalanguagekeyword,butthePythonconventionistocallitselfsothatotherPythonprogrammerswon’tbeconfusedaboutthemeaningofthevariable.
Intheprecedingsnippet,weintroducedtheif__name__=='__main__'condition,whichispresentinmanyPythonscripts.Thissnippetchecksthenameofthecurrentmodulethatisbeingexecuted,andwillpreventstartingthemainloopwherethismodulewasbeingimportedfromanotherscript.Thisblockisplacedattheendofthescript,sinceitrequiresthattheGameclassbedefined.
Tip
New-andold-styleclasses
YoumayseetheMySuperClass.__init__(self,arguments)syntaxinsomePython2examples,insteadofthesupercall.Thisistheold-stylesyntax,theonlyflavoravailableuptoPython2.1,andismaintainedinPython2forbackwardcompatibility.
Thesuper(MyClass,self).__init__(arguments)isthenew-classstyleintroducedinPython2.2.Itisthepreferredapproach,andwewilluseitthroughoutthisbook.
Seethechapter1_01.pyscript,whichcontainsthiscode.Sincenoexternalassetsareneeded,youcanplaceitinanydirectoryandexecuteitfromthePythoncommandlinebyrunningchapter1_01.py.Themainloopwillrunindefinitelyuntilyouclickontheclosebuttonofthewindow,oryoukilltheprocessfromthecommandline.
Thisisthestartingpointofourgame,solet’sstartdivingintotheCanvaswidgetandseehowwecandrawandanimateitemsinit.
DivingintotheCanvaswidgetSofar,wehavethewindowsetupandnowwecanstartdrawingitemsonthecanvas.TheCanvaswidgetistwo-dimensionalandusestheCartesiancoordinatesystem.Theorigin—the(0,0)orderedpair—isplacedinthetop-leftcorner,andtheaxiscanberepresentedasshowninthefollowingscreenshot:
Keepingthislayoutinmind,wecanusetwomethodsoftheCanvaswidgettodrawthepaddle,thebricks,andtheball:
canvas.create_rectangle(x0,y0,x1,y1,**options)
canvas.create_oval(x0,y0,x1,y1,**options)
Eachofthesecallsreturnsaninteger,whichidentifiestheitemhandle.Thisreferencewillbeusedlatertomanipulatethepositionoftheitemanditsoptions.The**optionssyntaxrepresentsakey/valuepairofadditionalargumentsthatcanbepassedtothemethodcall.Inourcase,wewillusethefillandthetagsoption.
Thex0andy0coordinatesindicatethetop-leftcornerofthepreviousscreenshot,andx1andy1areindicatedinthebottom-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.Thisstringwillbeusefullatertofinditemsinthecanvaswithspecifictags.
WewillinvokeotherCanvasmethodstomanipulatetheitemsandretrievewidgetinformation.ThistablegivesthereferencestotheCanvaswidgetthatwillbeusedinthischapter:
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.ThecallbackhandlerreceivesoneparameterofthetypeTkinterevent.
canvas.unbind(event)Unbindstheinputeventsothatthereisnocallbackfunctionexecutedwhentheeventoccurs.
canvas.create_text(*position,
**opts)
Drawstextonthecanvas.Thepositionandtheoptionsargumentsaresimilartotheonespassedincanvas.create_rectangleandcanvas.create_oval.
canvas.find_withtag(tag) Returnstheitemswithaspecifictag.
canvas.find_overlapping(*position)Returnstheitemsthatoverlaporarecompletelyenclosedbyagivenrectangle.
Youcancheckoutacompletereferenceoftheeventsyntaxaswellassomepracticalexamplesathttp://effbot.org/tkinterbook/tkinter-events-and-bindings.htm#events.
BasicgameobjectsBeforewestartdrawingallourgameitems,let’sdefineabaseclasswiththefunctionalitythattheywillhaveincommon—storingareferencetothecanvasanditsunderlyingcanvasitem,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,abasicusageofthisclassanditsattributeswouldbelikethis:
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,wecreatedagreenrectangleandaGameObjectinstancewiththeresultingitem.Thenweretrievedthepositionoftheitemwithinthecanvas,movedit,andcalculatedthepositionagain.Finally,wedeletedtheunderlyingitem.
ThemethodsthattheGameObjectclassofferswillbereusedinthesubclassesthatwewillseelater,sothisabstractionavoidsunnecessarycodeduplication.Nowthatyouhavelearnedhowtoworkwiththisbasicclass,wecandefineseparatechildclassesfortheball,thepaddle,andthebricks.
TheBallclassTheBallclasswillstoreinformationaboutthespeed,direction,andradiusoftheball.Wewillsimplifytheball’smovement,sincethedirectionvectorwillalwaysbeoneofthefollowing:
[1,1]iftheballismovingtowardsthebottom-rightcorner[-1,-1]iftheballismovingtowardsthetop-leftcorner[1,-1]iftheballismovingtowardsthetop-rightcorner[-1,1]iftheballismovingtowardsthebottom-leftcorner
Arepresentationofthepossibledirectionvectors
Therefore,bychangingthesignofoneofthevectorcomponents,wewillchangetheball’sdirectionby90degrees.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,whentheothergameobjectshavebeendefinedandplacedinthegamecanvas.
ThePaddleclassThePaddleclassrepresentstheplayer’spaddleandhastwoattributestostorethewidthandheightofthepaddle.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()calculatesthecurrentcoordinatesofthepaddleTheself.canvas.winfo_width()retrievesthecanvaswidthIfboththeminimumandmaximumx-axiscoordinates,plustheoffsetproducedbythemovement,areinsidetheboundariesofthecanvas,thisiswhathappens:
Thesuper(Paddle,self).move(offset,0)callsthemethodwithsamenameinthePaddleclass’sparentclass,whichmovestheunderlyingcanvasitemIfthepaddlestillhasareferencetotheball(thishappenswhenthegamehasnotbeenstarted),theballismovedaswell
Thismethodwillbeboundtotheinputkeyssothattheplayercanusethemtocontrolthepaddle’smovement.WewillseelaterhowwecanuseTkintertoprocesstheinputkeyevents.Fornow,let’smoveontotheimplementationofthelastoneofourgame’scomponents.
TheBrickclassEachbrickinourgamewillbeaninstanceoftheBrickclass.Thisclasscontainsthelogicthatisexecutedwhenthebricksarehitanddestroyed:
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__methodisverysimilartotheoneinthePaddleclass,sinceitdrawsarectangleandstoresthewidthandtheheightoftheshape.Inthiscase,thevalueofthetagsoptionpassedasakeywordargumentis'brick'.Withthistag,wecancheckwhetherthegameisoverwhenthenumberofremainingitemswiththistagiszero.
AnotherdifferencefromthePaddleclassisthehitmethodandtheattributesituses.TheclassvariablecalledCOLORSisadictionary—adatastructurethatcontainskey/valuepairswiththenumberofhitsthatthebrickhasleft,andthecorrespondingcolor.Whenabrickishit,themethodexecutionoccursasfollows:
Thenumberofhitsofthebrickinstanceisdecreasedby1Ifthenumberofhitsremainingis0,self.delete()deletesthebrickfromthecanvasOtherwise,self.canvas.itemconfig()changesthecolorofthebrick
Forinstance,ifwecallthismethodforabrickwithtwohitsleft,wewilldecreasethecounterby1andthenewcolorwillbe#999999,whichisthevalueofBrick.COLORS[1].Ifthesamebrickishitagain,thenumberofremaininghitswillbecomezeroandtheitemwillbedeleted.
AddingtheBreakoutitemsNowthattheorganizationofouritemsisseparatedintothesetop-levelclasses,wecanextendthe__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.Wecandivideitintotwosections:
Gameobjectinstantiation,andtheirinsertionintotheself.itemsdictionary.Thisattributecontainsallthecanvasitemsthatcancollidewiththeball,soweaddonlythebricksandtheplayer’spaddletoit.Thekeysarethereferencestothecanvasitems,andthevaluesarethecorrespondinggameobjects.Wewillusethisattributelaterinthecollisioncheck,whenwewillhavethecollidingitemsandwillneedtofetchthegameobject.Keyinputbinding,viatheCanvaswidget.Thecanvas.focus_set()callsetsthefocusonthecanvas,sotheinputeventsaredirectlyboundtothiswidget.Thenwe
bindtheleftandrightkeystothepaddle’smove()methodandthespacebartotriggerthegamestart.Thankstothelambdaconstruct,wecandefineanonymousfunctionsaseventhandlers.SincethecallbackargumentofthebindmethodisafunctionthatreceivesaTkintereventasanargument,wedefinealambdathatignoresthefirstparameter—lambda_:<expression>.
Ournewadd_ballandadd_brickmethodsareusedtocreategameobjectsandperformabasicinitialization.Whilethefirstonecreatesanewballontopoftheplayer’spaddle,thesecondoneisashorthandwayofaddingaBrickinstance:
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.Theunderlyingitemcreatedwithcanvas.create_text()isreturned,anditcanbeusedtomodifytheinformation:
defdraw_text(self,x,y,text,size='40'):
font=('Helvetica',size)
returnself.canvas.create_text(x,y,text=text,
font=font)
Theupdate_lives_textmethoddisplaysthenumberoflivesleftandchangesitstextifthemessageisalreadydisplayed.Itiscalledwhenthegameisinitialized—thisiswhenthetextisdrawnforthefirsttime—anditisalsoinvokedwhentheplayermissesaballrebound:
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,andthislogicwillbeaddedinthenextsection.SincePythonrequiresacodeblockforeachmethod,weusethepassstatement.Thisdoesnotexecuteanyoperation,anditcanbeusedasaplaceholderwhenastatementisrequiredsyntactically:
defstart_game(self):
pass
Seethechapter1_02.pymodule,ascriptwiththesamplecodewehavesofar.Ifyouexecutethisscript,itwilldisplayaTkinterwindowliketheoneshowninthefollowing
MovementandcollisionsNowthatwehaveplacedallofourgameobjects,wecandefinethemethodsthatwillbeexecutedinthegameloop.Thislooprunsindefinitelyuntilthegameends,andeachiterationupdatesthepositionoftheballandchecksthecollisionthatoccurs.
WiththeCanvaswidget,wecancalculatewhattheitemsthatoverlapwiththegivencoordinatesare,sofornow,wewillimplementthemethodsthatareresponsibleformovingtheballandchangingitsdirection.
Let’sstartwiththemovementoftheballandtheconditionsforrecreatingthebouncingeffectwhenitreachesthecanvasborders:
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.Itstoresthevaluesinthecoordsandwidthlocalvariables,respectively.Ifthepositioncollideswiththeleftorrightborderofthecanvas,thehorizontalcomponentofthedirectionvectorchangesitssignIfthepositioncollideswiththeupperborderofthecanvas,theverticalcomponentofthedirectionvectorchangesitssignWescalethedirectionvectorbytheball’sspeedTheself.move(x,y)movestheball
Forinstance,iftheballhitstheleftborder,thecoords[0]<=0conditionevaluatestotrue,sothex-axiscomponentofthedirectionchangesitssign,asshowninthisdiagram:
Iftheballhitsthetop-rightcorner,bothcoords[2]>=widthandcoords[1]<=0evaluatetotrue.Thischangesthesignofboththecomponentsofthedirectionvector,likethis:
Thelogicofthecollisionwithabrickisabitmorecomplex,sincethedirectionoftherebounddependsonthesidewherethecollisionoccurs.
Wewillcalculatethex-axiscomponentoftheball’scenterandcheckwhetheritisbetweenthelowermostanduppermostx-axiscoordinatesofthecollidingbrick.Totranslatethisintoaquickimplementation,thefollowingsnippetshowsthepossiblechangesinthedirectionvectoraspertheballandbrickcoordinates:
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,sincethebrickisbeinghitfromabove,asshownhere:
Ontheotherhand,acollisionfromtheright-handsideofthebrickwouldbeasfollows:
Thisisvalidwhentheballhitsthepaddleorasinglebrick.However,theballcanhittwobricksatthesametime.Inthissituation,wecannotexecutethepreviousstatementsforeachbrick;ifthey-axisdirectionismultipliedby-1twice,thevalueinthenextiteration
ofthegameloopwillbethesame.
Wecouldcheckwhetherthecollisionoccurredfromaboveorbehind,buttheproblemwithmultiplebricksisthattheballmayoverlapthelateralofoneofthebricksand,therefore,changethex-axisdirectionaswell.Thishappensbecauseoftheball’sspeedandtherateatwhichitspositionisupdated.
Wewillsimplifythisbyassumingthatacollisionwithmultiplebricksatthesametimeoccursonlyfromaboveorbelow.Thatmeansthatitchangesthey-axiscomponentofthedirectionwithoutcalculatingthepositionofthecollidingbricks:
iflen(game_objects)>1:
self.direction[1]*=-1
Withthesetwoconditions,wecandefinethecollidemethod.Aswewillseelater,anothermethodwillberesponsiblefordeterminingthelistofcollidingbricks,sothismethodonlyhandlestheoutcomeofacollisionwithoneormorebricks:
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,sothehitcountersaredecreasedandthebricksareremovediftheyreachzerohits.
StartingthegameFinally,wehavebuiltthefunctionalityneededtorunthegameloop—thelogicrequiredtoupdatetheball’spositionaccordingtotherebounds,andrestartthegameiftheplayerlosesonelife.
NowwecanaddthefollowingmethodstoourGameclasstocompletethedevelopmentofourgame:
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,isresponsibleforunbindingtheSpacebarinputkeysothattheplayercannotstartthegametwice,detachingtheballfromthepaddle,andstartingthegameloop.
Stepbystep,thegame_loopmethoddoesthefollowing:
Itcallsself.check_collisions()toprocesstheball’scollisions.Wewillseeitsimplementationinthenextcodesnippet.Ifthenumberofbricksleftiszero,itmeansthattheplayerhaswon,andacongratulationstextisdisplayed.Supposetheballhasreachedthebottomofthecanvas:
Then,theplayerlosesonelife.Ifthenumberoflivesleftiszero,itmeansthattheplayerhaslost,andtheGameOvertextisshown.Otherwise,thegameisreset
Otherwise,thisiswhathappens:
Thepositionoftheballisupdatedaccordingtoitsspeedanddirection,andthegameloopiscalledagain.The.after(delay,callback)methodonaTkinterwidgetsetsatimeouttoinvokeafunctionafteradelayinmilliseconds.Since
thisstatementwillbeexecutedwhenthegameisnotoveryet,thiscreatestheloopnecessarytoexecutethislogiccontinuously:
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.SinceBall.collidereceivesalistofgameobjectsandcanvas.find_overlappingreturnsalistofcollidingitemswithagivenposition,weusethedictionaryofitemstotransformeachcanvasitemintoitscorrespondinggameobject.
RememberthattheitemsattributeoftheGameclasscontainsonlythosecanvasitemsthatcancollidewiththeball.Therefore,weneedtopassonlytheitemscontainedinthisdictionary.Oncewehavefilteredthecanvasitemsthatcannotcollidewiththeball,suchasthetextdisplayedinthetop-leftcorner,weretrieveeachgameobjectbyitskey.
Withlistcomprehensions,wecancreatetherequiredlistinonesimplestatement:
objects=[self.items[x]forxinitemsifxinself.items]
Thebasicsyntaxoflistcomprehensionsisthefollowing:
new_list=[expr(elem)forelemincollection]
Thismeansthatthenew_listvariablewillbealistwhoseelementsaretheresultofapplyingtheexprfunctiontoeacheleminthelistcollection.
Wecanfiltertheelementstowhichtheexpressionwillbeappliedbyaddinganifclause:
new_list=[expr(elem)forelemincollectionifelemisnotNone]
Thissyntaxisequivalenttothefollowingloop:
new_list=[]
forelemincollection:
ifelemisnotNone:
new_list.append(elem)
Inourcase,theinitiallististhelistofcollidingitems,theifclausefilterstheitemsthatarenotcontainedinthedictionary,andtheexpressionappliedtoeachelementretrievesthegameobjectassociatedwiththecanvasitem.Thecollidemethodiscalledwiththislistasaparameter,andthelogicforthegameloopiscompleted.
PlayingBreakoutOpenthechapter1_complete.pyscripttoseethefinalversionofthegame,andrunitbyexecutingchapter1_complete.py,asyoudidwiththepreviouscodesamples.
Whenyoupressthespacebar,thegamestartsandtheplayercontrolsthepaddlewiththerightandleftarrowkeys.Eachtimetheplayermissestheball,thelivescounterwilldecrease,andthegamewillbeoveriftheballreboundismissedagainandtherearenolivesleft:
Inourfirstgame,alltheclasseshavebeendefinedinasinglescript.However,asthenumberoflinesofcodeincreases,itbecomesnecessarytodefineseparatescriptsforeachpart.Inthenextchapters,wewillseehowitispossibletoorganizeourcodebymodules.
SummaryInthischapter,webuiltoutfirstgamewithvanillaPython.Wecoveredthebasicsofthecontrolflowandtheclasssyntax.WeusedTkinterwidgets,especiallytheCanvaswidgetanditsmethods,toachievethefunctionalityneededtodevelopagamebasedoncollisionsandsimpleinputdetection.
OurBreakoutgamecanbecustomizedaswewant.Feelfreetochangethecolordefaults,thespeedoftheball,orthenumberofrowsofbricks.
However,GUIlibrariesareverylimited,andmorecomplexframeworksarerequiredtoachieveawiderrangeofcapabilities.Inthenextchapter,wewillintroduceCocos2d,agameframeworkthathelpsuswiththedevelopmentofournextgame.
Chapter2.CocosInvadersInthepreviouschapter,webuiltagamewithaGraphicalUserInterface(GUI)packagecalledTkinter.SinceitispartofthePython’sstandardlibraries,itwaseasytosetuptheprojectandcreatetherequiredwidgetswithafewlinesofcode.However,thiskindofmodulefallsshortofprovidingthecorefunctionalityofgamedevelopmentbeyondthe2Dgraphics.
Theprojectcoveredinthischapterisdevelopedwithcocos2d.ThisframeworkhasforksfordifferentprogramminglanguagesbesidesPython,suchasC++,Objective-C,andJavaScript.
Thegamethatwewilldevelopisavariantofaclassicalgame,SpaceInvaders.Thisarcadewasasuccessofitstime,andithasbecomepartofthecultureofvideogaming.
Inthistwo-dimensionalgame,theplayermustdefeatwavesofdescendingaliensbyshootingthemwithalasercannon,whichcanbemovedhorizontallyandisprotectedbysomedefensebunkers.Theenemyfireattemptstodestroytheplayer’scannon,anditgraduallydamagesthebunkers.
Inourversion,wewillleaveoutthedefensebunkers,andthegamewilllookasfollows:
Withthisproject,youwilllearnthefollowing:
Thefoundationsofcocos2dHowtoworkwithspritesProcessinginputeventsHandlingmovementsandcollisionsComplementingcocos2dwiththePygletAPI
Installingcocos2dInourpreviousgame,wedidnotuseanythird-partypackagesinceTkinterwaspartofthePythonstandardlibrary.Thisisnotthecasewithournewgameframework,whichmustbedownloadedandinstalled.
ThePythonPackageIndex(alsocalledPyPI)isanofficialsoftwarerepositoryofthird-partypackages,andthankstopackagemanagersystems,thisprocessbecomesverystraightforward.Thepipisthepackagingtoolrecommendation,anditisalreadyincludedinthelatestPythoninstallations.
Youcancheckwhetherpipisinstalledbyrunningpip--version.Theoutputindicatestheversionofpipandwhereitislocated.ThisinformationmayvarydependingonyourPythonversionanditsinstallationdirectory:
$pip--version
pip6.0.8fromC:\Python34\lib\site-packages(python3.4)
Sincecocos2disavailableonthePyPI,youcaninstallitbyrunningthefollowingcommand:
$pipinstallcocos2d
Thiscommanddownloadsandinstallsnotonlythecocos2dpackagebutalsoitsdependencies(Pygletandsix).Oncerun,theconsoleoutputshouldprinttheprogressandindicatethattheinstallationwasexecutedsuccessfully.Toverifythatcocos2discorrectlyinstalled,youcanrunthiscommand:
$python-c"importcocos;print(cocos.version)"
0.6.0
Atthetimeofwritingthisbook,thelatestversionofcocos2dis0.6.0,sotheoutputmightbeagreaterversionthanthisone.
TipThePythonpackagingecosystem
Apartfrompip,thereareotherpackagingutilities,suchaseasy_installandconda.Eachtoolhasdifferentfeaturesandlimitations,butinthisbook,wewillstickwithpipduetoitseaseofuseandthefactthatitisincludedinthelatestPythoninstallationsbydefault.
Gettingstartedwithcocos2dAswithanyotherframework,cocos2discomposedofseveralmodulesandclassesthatimplementdifferentfeatures.InordertodevelopapplicationswiththisAPI,weneedtounderstandthefollowingbasicconcepts:
Scene:Eachofthestagesofyourapplicationisascene.Whileyourgamemayhavemanyscenes,onlyoneisactiveatanygivenpointintime.Thetransitionbetweenscenesdefinesyourgame’sworkflow.Layer:Everysheetcontainedinascene,whoseoverlaycreatesthefinalappearanceofthescene,iscalledalayer.Forinstance,yourgame’smainscenemayhaveabackgroundlayer,anHUDlayerwithplayerinformationandscores,andananimationlayer,whereeventsandcollisionsbetweenspritesarebeingprocessed.Sprite:Thisisa2Dimagethatcanbemanipulatedthoughcocos2dactions,suchasmove,scale,orrotate.Inourgames,spriteswillrepresenttheplayer’scharacter,enemies,andvisualinformation,suchasthenumberoflivesleft.Director:Thisisasharedobjectthatinitializesthemainwindowandcontrolsthecurrentsceneaswellastherestofthescenesthatareonhold.
AlloftheseclassesexceptDirectorinheritfromCocosNode.Thisclassrepresentselementsthatare—orcontain—nodesthatgetdrawn,anditispartofthecoreofthelibrary.
Thefunctionalityofthisclasscoversparent-childrelations,specialplacements,rendering,andscheduledactions.CocosNodesarealsonotifiedwhentheyareaddedasachildofanothernodeandwhentheyareremovedfromtheirparentnode.
Thistablehasasummaryofthemostrelevantmembersofthisclass:
Member Description
add(child,z=0,
name=None)Thisaddsachild.Itraisesanexceptionifthenamealreadyexists.
remove(child)Thisremovesachildgivenitsnameortheobject.Itraisesanexceptionifthechildisnotonthelistofchildren.
kill() Removesitselffromitsparent.
get_children() Returnsalistofthechildrenofthenode,orderedbytheirzindex.
parent Theparentnode.
position Thepositioncoordinatesrelativetotheparentnode.
x Thex-axispositioncoordinate.
y They-axispositioncoordinate.
schedule(callback) Thisschedulesafunctiontobecalledeveryframe.
on_enter()Thisiscalledwhenaddedasachildwhiletheparentisintheactivescene,orifthescenegoesactive.
on_exit()Thisiscalledwhenremovedfromitsparentwhiletheparentisintheactivescene,orifthescenegoesactive.
Withthesedefinitionsinmind,wecanbuildourfirstcocos2dapplication.Itwillbeasimpleapplicationinwhichtheplayermustpickupfourobjectsplacedaroundthescene.Theplayercharactercanbemovedwiththearrowkeys,andthereareneithermenusnordisplayinformation.
Theapplicationwilllooklikethis:
Ascanbeseen,thefunctionalitywillbequitebasic,sincethepurposeofthisexampleistofamiliarizeyourselfwiththecocos2dmodules.
Remembertosavethescriptinthesamedirectoryasthe'ball.png'image,sinceitisloadedbyyourgame.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.Wecanimportonlytherequiredmodulesseparately(cocos.sprite,cocos.layer,cocos.scene,andcocos.director),whichhastheadvantageofaquickerinitialization.However,forthisbasicapp,wewillfollowasimplerapproach.
Wehavedefinedacustomlayerbyinheritingfromthecocos.layer.Layerclass.Inthisexample,wecallitsparent__init__methodonly,butaswewillseelater,thereareotherLayersubclassesthatcanbeusedforspecializedfunctionality,suchasColorLayerandMenu.
Thelayercontainsasinglecocos.sprite.Spriteinstancewiththe'ball.png'image,whichwillrepresenttheplayercharacter.ThecolorattributeindicatestheRGBcolorappliedtothespriteimage,representedasatuplewhosevaluesrangefrom0to255.Thepositionattribute,declaredintheCocosNodeclass,setsthesprite’scentercoordinates.Itisalsopossibletopassthesevaluesaskeywordargumentstothe__init__method.
Themaincodeblockperformsthefollowingactions:
InitializesthemainwindowwiththeDirectorinstanceInstantiatesourLayersubclasswiththespriteCreatesanemptyscenethatcontainsthelayerRunstheDirectormainloopwiththescene
Sincedirector.initwrapsthecreationofaPygletwindow,thecompletelistofkeywordparametersthatcanbepassedtothismethodisavailableatthePygletWindowAPIReference.
Forourcocos2dgames,onlyasubsetoftheseparameterswillbeused,andthemostrelevantkeywordparametersarethefollowing:
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.
HandlinguserinputEachLayerisresponsibleforhandlinginputeventsthroughtheis_event_handlerflag,whichissettoFalsebydefault.IfthisflagissettoTrue,theLayerwillreceiveinputeventsandcallthecorrespondinghandlermethods.
TheseareeventlistenersfromtheLayerclass:
on_mouse_motion(x,y,dx,dy):The(x,y)arethephysicalcoordinatesofthemouse,and(dx,dy)isthedistancevectorcoveredbythemousesincethelastcallon_mouse_press(x,y,buttons,modifiers):Justasinon_mouse_motion,(x,y)arethephysicalcoordinatesofthemouse,butthisiscalledwhenamousebuttonispressedon_mouse_drag(x,y,dx,dy,buttons,modifiers):Acombinationofon_mouse_pressandon_mouse_motion,thisiscalledwhenthemousemovesoverwithoneormorebuttonspressedon_key_press(key,modifiers):Thisiscalledwhenakeyispressedon_key_release(key,modifiers):Thisiscalledwhenakeyisreleased
ThebuttonsparameterisabitwiseORofPygletbuttonconstantsthataredefinedinthepyglet.window.mouseandpyglet.window.keymodulesfortheon_mouseandon_keyevents,respectively.ThemodifiersparameterisabitwiseORofthepyglet.window.keyconstants,suchasShift,Option,andAlt.Youcanfindalltheconstantsdefinedinthismoduleathttps://pythonhosted.org/pyglet/api/pyglet.window.key-module.html.
WhenyoudefineoneofthesemethodsinyourLayersubclassandthecorrespondinginputeventoccurs,themethodiscalled.
Inthisiterationofoursamplegame,wewillprintamessagewhenakeyispressedorreleased:
TipNotethatsincePygletisusedinternallyforeventhandling,weneedthesymbol_stringfunctionfromthepyglet.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))
TipWehaveomittedtheif__name__=='__main__'block,butremembertoincludeitinyourscriptsinordertoexecutetheapplication.
Whenyoulaunchit,thekeyboardeventsmustbeprintedontheconsole.Ournextstepwillbetoreflecttheseeventsinthegamestatebymodifyingthesprite’sposition.
TipThePygleteventframework
Eventhandlingisimplementedincocos2dwiththePygleteventframework.Thiseventframeworkallowsyoutodefineemitterswithagiveneventname(suchason_key_press)andregisterlistenersovertheseevents.
RememberthatPygletisthemultimedialibraryonwhichcocos2drelies,sowewillseemoreusagesofthisAPIsincemajorcocos2dapplicationsusePygletmodules.
UpdatingthesceneSofar,ourgameonlydisplaysthesamecontent,asobjectsarebeingmodified.InawaysimilartotheTkintercallbackthatwasloopingduringtheexecutionofourlastgame,weneedtorefreshourcocos2dapplicationperiodically.
ThisisachievedwiththeschedulemethodfromtheCocosNodeclass.Itschedulesafunctionthatiscalledoneveryframe,andthefirstargumentthatthisfunctionreceivesistheelapsedtimeinsecondssincethelastclocktick.
Whileitispossibletopassadditionalargumentstothiscallbackfunction,wewilldefineanewmethodintheMainLayerclasswiththeelapsedtimeparameteronly:
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_releasesothattheysavethekeystrokeindefaultdict,aspecialdictionaryfromthecollectionsmodule.Thisdatastructurereturnsthecorrespondingvalueifthekeyispresentorthedefaultvalueofthetypeifthekeydoesnotexist.Inourcase,sinceitisdefaultdict(int),thedefaultvaluefortheabsentkeysis0.
Wehavescheduledtheupdatemethod,andforeachframe,itchecksthevaluesofthe
ProcessingcollisionsIncocos2d,proximityandcollisionsamongactorsarecalculatedwithaninterfaceofthecocos.collision_modelpackagecalledCollisionManager.Thisinterfacecanbeusedtodeterminewhethertwoobjectsareoverlapping,orwhichobjectsarecloserthanacertaindistancefromagivenobject.
Actorsshouldbeaddedfirsttothesetofobjectsthataremanagedbythecollisionmanager,alsocalledknownentities.Thesetofmanagedobjectsisinitiallyempty.
TheCollisionManagerinterfaceisimplementedbytwoclasses:
CollisionManagerBruteForce:Thisisastraightforwardimplementation.Init,allknownentitiesareusedtocalculateproximityandcollisions.Itisintendedfordebuggingpurposes,anditdoesnotscaleifthesetofknownentitiesgrowsinsize.Itdoesnotneedanyargumentsinitsinitialization.CollisionManagerGrid:Thisdividesthespaceintorectangularcellswithagivenwidthandheight.Whencalculatingthespatialrelationsbetweenobjects,itonlyconsiderstheobjectsfromtheknownentitiesthatoverlapthesamecell.Itsperformanceisbettersuitedforalargenumberofknownentities.The__init__methodrequirestheminimumandmaximumcoordinatesofthespace,thecellwidth,andthecellsize.Therecommendedcellsizeisthemaximumobjectwidthandheightmultipliedby1.25.Notethatasthisfactorincreasesitsvalue,moreobjectsoverlapthesamecellandtheperformancestartstodegrade.
ThereferenceoftheCollisionManagermethodsthatwearegoingtouseinthischapteristhefollowing:
Method Description
add(obj) Makesobjaknownobject
clear() Emptiesthesetofknownobjects
iter_colliding(obj) Returnsaniteratorofobjectscollidingwithobj
knows(obj) Thisistrueifobjisaknownobject
AnobjectcanbecollidedwithifithasamembercalledCshapeanditsvalueisaninstanceofcocos.collision_model.Cshape.Therearetwoclassesincocos2dthatinheritfromCshape:
CircleShape:Thisusesacircleasthegeometricspace.Whenaninstanceiscreated,thecenterofthecircleanditsradiusmustbespecified.TwocirclescollideiftheEuclideandistancebetweentheircentersislessthanthesumoftheirradius.AARectShape:Thisusesarectangleasthegeometricspace.Therectangleisaxis-aligned,whichmeansthatitssidesareparalleltothexandyaxes.Thiscanbewellsuitediftheactorsdonotrotate.InsteadoftheEuclideandistance,itusestheManhattandistancetocalculateacollisionbetweentworectangles.
Inourexample,wewillusecircularshapesforouractors.Inordertotestthecollisionmanager,fourspritesareaddedtotheLayer,andtheywillactaspickups—theywillbedestroyedwhentheycollidewiththeplayer’ssprite.
Besides,sinceCshaperequiresacocos.euclid.Vector2forthecentercoordinates,wewillreplacethetuplesfortheVector2instances.
Beforeaddingthecollisionmanager,wedefineanewclasstoreplaceoursprites.Theactorsofourgamesharetheseattributes,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,wecanaddtheseobjectswiththerequiredcshapemember.ThecollisionmanagerimplementationthatwewilluseinallourapplicationsisCollisionManagerGrid,sincethebrute-forceimplementationisonlyintendedforreferenceanddebugging:
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
Nowtheupdatemethodmustperformthefollowingstepsinordertobeabletodetectcollisionsofmovingobjects: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
Notethestatementinwhichthecshapecenteroftheplayerspriteisupdatedwiththenewposition.Withoutthisline,thecollisionmanagerwillnotdetectanycollisionbetweentheplayerandthepickups,sincetheentitycenterwillalwaysbeintheinitialposition.
Inthechapter2_01.pyscript,youcanfindthecompletecode.Forthisapplication,weonlyneededasinglesprite.However,mostgamesuseseveralimagestorepresentdifferentgamecharactersandtheiranimations.
Inthenextsection,wewillseethespritesthatourSpaceInvadersgamewillhave.
CreatinggameassetsWewillneedafewbasicimagesforourcocos2dsprites.Theywillbethevisualrepresentationsoftheplayer’scannonandthedifferenttypesofaliens.
Thefollowingimageshowshowthesespritescanbedrawnwithanimageeditorprogram,withthehelpofagrid:
Unfortunately,theusageofimagemanipulationtoolsisbeyondthescopeofthisbook.Thereisawidevarietyintheseprograms,andforbasicsprites(suchastheonesshowninthepreviousimage),youcanuseMSPaintonWindowssystems.
Undertheimgfolder,youcanfindthedifferentimagesthatwewilluseinourgame.Feelfreetoeditthemandchangetheircolors.However,donotmodifytheirsizeasitisusedtodeterminethewidthandtheheightoftheSpriteanditsCshapemember.
TipCreatingyourown“pixelart”
Therearemoreadvancedapplicationsthatcanberunonanyplatform.ThisisthecaseofGIMP,anopensourceprogramthatispartoftheGNUproject.ThespritesusedinthischapterhavebeendrawnwithGIMP2,thecurrentversion.
Youcandownloaditforfreefromhttp://www.gimp.org/downloads/.
SpaceInvadersdesignBeforewritingourgamewithcocos2d,weshouldconsiderwhichactorsweneedtomodelinordertorepresentourgameentities.InourSpaceInvadersversion,wewillusethefollowingclasses:
PlayerCannon:Thisisthecharactercontrolledbytheplayer.Thespacebarisusedtoshoot,andthehorizontalmovementiscontrolledwiththerightandleftarrowkeys.Alien:Eachoneofthedescendingenemies,withdifferentlooksandscoresdependingonthealientype.AlienColumn:Therearecolumnsoffivealiensintowhicheverygroupofaliensisdivided.AlienGroup:Awholegroupofenemies.Itmoveshorizontallyordescendsuniformly.Shoot:Asmallprojectilelaunchedbytheenemiestowardstheplayercharacter.PlayerShoot:AShootsubclass.Itislaunchedbytheplayerratherthanthealiengroup.
Apartfromtheseclasses,weneedacustomLayerforthegamelogic,whichwillbenamedGameLayer,andabaseclassforthelogicthatissharedamongtheactors—similartotheGameObjectclassfromourpreviousgame.
WewillcallitActor,aswedidinourpreviouscocos2dapplication.Infact,itisquitesimilartothatone,exceptthatthisimplementationaddsthedefinitionoftwomethods:updateandcollide.Theywillbecalledwhenourgameloopupdatesthepositionofthenodesandwhenaknownentityhitstheactor,respectively.
Thismoduleisnew,apartfromthecodewesawintheprevioussection,anditwillcontainthefollowingcode:
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,wehavesetuptheinterfaceforcollisiondetectionandthemovementsofallourgameobjects.
ThePlayerCannonandGameLayerclassesAswesawearlier,PlayerCannonisoneofourgameactors.Itmustrespondtotheleftandrightkeystrokestocontrolthehorizontalmovementofthesprite,andtheobjectisdestroyedifanenemy’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,asinourbasiccocos2dapplication.Inthiscase,itisaclassattributeofPlayerCannon.Thehorizontalmovementislimitedsothatthecharactercannotleavethecollisionmanagergrid.
TheGameLayerclasswillbethemainlayerofourgame,anditwillberesponsiblefordoingthefollowing:
KeepingtrackofthenumberofplayerlivesleftandthecurrentscoreHandlinginputkeyeventsbysettingitsis_event_handlerflagtotrueCreatingthegameactorsandaddingthemaschildnodesRunningthegameloopbyexecutingascheduledfunctionforeachframewherethecollisionsareprocessedandtheobjectpositionsareupdated
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.Itwillbecalledeachtimetheplayercharacterneedstoberespawned,whileupdate_scoreincrementsthescorebyaddingthepointsforeachalienthatisdestroyed.Thedefaultvalueofthescore=0argumentinthemethodsignaturemeansthatifnoargumentsarepassed,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.Sincetherequiredclasseshavenotbeenimplementedyet,wewillleaveitwiththepassstatementfornow:
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,theknowsmethodcheckswhetheranobjectisinthesetofknownentities—whichshouldbealwaystruesinceallofthelayer’schildrenhavebeenadded.
However,whenanobjectisoutsideofthesurfacecoveredbythecollisionmanager,itdoesnotoverlapwithanycellofthegridanditisconsideredtobe“notknown.”Inthis
way,objectsthatmoveoutofthisareaareautomaticallyremoved:
defcollide(self,node):
ifnodeisnotNone:
forotherinself.collman.iter_colliding(node):
node.collide(other)
returnTrue
returnFalse
Finally,thecollidemethodencapsulatesthecalltoiter_collidingandcheckswhetherthenodeisareferencetoavalidobject—thePlayerShootinstancecanbeNoneifthereisnocurrentshoot.
Asusual,wewritethemainblockforthemomentwhenourscriptisrunasthemainmodule:
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,andAlienGroup.
Outoftheseclasses,onlyAlieninheritsfromActorbecauseitistheonlyentitythatisdrawnandcollideswithotherobjects.Insteadofastaticimage,thespritewillbeabasicanimationwhereineachimagewillbeshownfor0.5seconds.ThisisachievedbyloadinganImageGridandcreatinganAnimationfromthisspritegrid.Sincetheseclassesbelongtothepyglet.imagemodule,weneedtoimportthemfirst.
Anotherfunctionofouralienswillbenotifyingitscolumnthattheobjecthasbeenremoved.Thankstothis,thecolumnofaliensknowswhatthebottomoneisandstartsshootingfromitsposition.
YoulearnedfromtheCocosNodereferencethattheon_exitmethodiscalledwhenanodeisremoved,soyouwillbeoverridingittoinformitscorrespondingcolumn.Notethatareferencetothecolumnispassedtothe__init__method.WecouldhaveimplementedthesamefunctionalitywithaPygleteventhandlermechanism,butthatwouldrequirepushingthehandlerstoeveryAlieninstance:
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,atthebeginningofthegame.Thescoreisthenumberofpointsearnedwhentheenemyisdestroyed,whilethecolumnattributecontainsareferencetothecolumntowhichthealienbelongs.
TheAlienColumnclassisresponsibleforinstantiatingthecolumnsofalienswithagivenpattern,likethis:
SincewehavedefinedtheAlien.from_typeutilitymethod,wecallitintheproperordertoplaceeachtypeofalien,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,thecolumnhasreachedthesideofthescreenornot.ItwillreturnFalseiftherearenoaliensleftinthecolumn:
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,anditdelegatestheshootinglogictoeachcolumn.Thismovementisimplementedwiththespeedanddirectionobjectattributes.
Whilethealiengroupisbeingupdated,itsumstheelapsedtimesbetweenframes.Whenacertainperiodhaselapsed,thewholegroupismoveddownorlaterally.Thedirectionwilldependonwhetheranycolumnhasreachedthelateralsidesornot:
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,whichisaspecialmethodthatisinvokedwhenyouiterateoveranobject.Inthisway,wecancallforalieninalien_groupintherestofourcode.
Nowthatwehaveimplementedthelogicofourenemiesandthemovementofthewholegroup,wecanadditsinstantiationtotheGameLayerclass:
defcreate_alien_group(self,x,y):
self.alien_group=AlienGroup(x,y)
foralieninself.alien_group:
self.add(alien)
Thiscreatesanewaliengroupandaddsalltheenemiestothelayeraschildnodes.Thechapter2_02.pyscriptcontainsthecodethatwehavewrittensofar,anditsexecutionwilllooklikethis:
Shoot’emup!TheShootactor,byitself,isquitebasic;itonlyrequiresaspeedattributeandoverridestheupdatemethodsothattheobjectismovedbythedistancedeterminedbythisspeedandtheelapsedtimebetweenframes:
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,sincetheplayercannotshootuntilthepreviousbeamhashitanenemyorreachedtheendofthescreen.
Aswewanttoavoidglobalvariables,wewilluseaclassattributetoholdthereferencetothecurrentshot.Whentheshotleavesthescene,thisreferencewillbesettoNoneagain.
WewilloverridethecollidemethodfromtheActorclasssothatboththebeamandthealienaredestroyedwhenacollisionoccurs.ThisisdonebycallingthekillmethoddefinedintheSpriteclass.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(),whichreturnsarandomfloatbetweenthesemi-openrangeof(0.0,1.0).Wewillsetalowprobability.Thisisbecausethisfunctionwillbecalledseveraltimespersecond.TherandommoduleispartofthePythonstandardlibrary,soyoucanstartplayingaroundwithitbyaddingtheimportrandomstatementatthebeginningofthescript:
defshoot(self):
ifrandom.random()<0.001andlen(self.aliens)>0:
pos=self.aliens[0].position
returnShoot(pos[0],pos[1]-50)
returnNone
NowtheupdatemethodoftheGameLayerclasscancalculatewhichobjectscollidewiththecannonandtheplayer’scurrentshot,aswellasrandomlyshootfromthealiencolumns:
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,anditunschedulesupdateiftherearenolivesleft.Thisstopsthemainloopandrepresentsthegameoversituation:
defrespawn_player(self):
self.lives-=1
ifself.lives<0:
self.unschedule(self.update)
else:
self.create_player()
Inthisiteration,thegamewehavebuiltisverysimilartothefinalversion.Theplayablecharactercanbemovedwiththearrowkeysandisabletoshoot.Theenemiesmoveuniformly,andtheloweralienofeachrowcanshootaswell.Thecodeforthisimplementationcanbefoundinthechapter2_03.pyscript.
Now,theonlydetailleftisdisplayingthegameinformation:thecurrentscoreandthenumberoflivesleft.
AddinganHUDOur“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,anditsmethodswillbemodifiedsothattheycancalltheHUDdirectly:
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
#...
Thehudattributewillbeusedwhentheplayerneedstoberespawnedandwhenthescoreisupdated:
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,wepassthezindextoindicatetheorderingofthechildrenlayers:
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–themysteryshipTocompleteourgame,wewilladdafinalingredient:themysteryshipthatrandomlyappearsatthetopofthescreen:
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,wewillrandomlyaddthisalientoourscheduledfunction:
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!
SummaryInthischapter,weintroducedcocos2danditsmostrelevantmodules.Wedevelopedourfirstapplicationtogetstartedwiththelibrary,andlaterwebuiltasimplifiedversionofSpaceInvaders.
Thisversioncanbeextendedbyaddingdefensebunkersormorerandomvaluestothepossiblescoresofthemysteryship.Besides,ifyouwanttochangethevisualappearanceoftheinvaders,youcaneditthespritesandcreateyourownenemies!
Inthenextchapter,wewilldevelopacompletetowerdefensegame,withtransitionsbetweenscenes,enhanceddisplayinformation,andcomplexmenus.
Chapter3.BuildingaTowerDefenseGameInthepreviouschapter,youlearnedthefundamentalsofcocos2d,andnowyouareabletodevelopabasicgamewiththislibrary.However,mostgamesusemorethanasinglescene,andtheircomplexityitisnotlimitedtocollisionsandinputdetection.
Withournextgame,wewilldiveintothecocos2dmodulesthatprovideustheadvancedfunctionalitywearelookingfor:menus,transitions,scheduledactions,andanefficientwayofstoringlevelgraphics.
Inthischapter,wewillcoverthesetopics:
Howtomanipulatespriteswithcocos2dactionsUsingtilemapsasbackgroundlayersAnimatedtransitionsbetweengamescenesHowtocreatescenesthatactasmenusandcutscenesBuildingafull-fledgedcocos2dapplicationwithalltheingredientsyouhavelearnedsofar
ThetowerdefensegameplayThegenreoftowerdefensechallengestheplayertostopenemycharactersfromreachingacertainpositionbyplacingstrategicallydifferenttowerssothattheycandefeattheenemiesbeforetheyarriveatthatpoint.Thetowersshootautonomouslytowardstheenemiesthatarewithintheirfiringrange.Thegameisoverwhenaconcretenumberofenemiesreachtheendpoint.
Inourversion,thescenariowillbeameanderingroadinthedesert,andwehavetoprotectabunkerthatisplacedattheendofthisroad,asshowninthefollowingscreenshot.Atthefarend,enemytankswillbespawningrandomly,andwemustplaceturretsthatdestroythembeforetheyreachourbunker.Theturretscanbeplacedinspecificslots,andeachturretspendspartofourlimitedresources.
Cocos2dactionsInourpreviousgame,wemanipulatedourspritesdirectlythroughtheirmembers,especiallythepositionattribute.Thegameloopupdatedeachactorwiththeelapsedtimefromthepreviousframe.
However,ourtowerdefensegamewillbebasedmainlyoncocos2dactions,whichareorderstomodifyobjectattributessuchastheposition,rotation,orscale.Theyareexecutedbycallingthedo()methodoftheCocosNodeclass.Therefore,anysprite,layer,orscenecanbeavalidtargetofanaction.
Theactionsthatwewillcoverinthissectioncanbedividedintotwomaingroups:intervalactionsandinstantactions.
IntervalactionsTheseactionshaveaduration,andtheirexecutionendsafterthatcertainduration.Forinstance,ifwewanttomoveaspritetoacertainposition,wedonotwantittohappenimmediatelybutlastafixedamountoftime,givingtheimpressionthatitmoveswithadeterminedspeed.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),toposition(250,300).ThesecondargumentofMoveToindicatestheduration,whichis3seconds.Ifwewantedtousearelativeoffsetinsteadoftheabsolutecoordinates,wecouldhaveusedtheMoveByaction.TheequivalentinthisexamplewouldbeMoveBy((50,100),3).
Anothercommonactionisrotatinganode,andforthispurpose,cocos2dofferstheRotateByandRotateToactions,whichtaketheangleindegreesandthedurationinseconds:
#...
sprite=cocos.sprite.Sprite('tank.png',position=(200,200))
sprite.do(ac.RotateBy(180,5))#Rotate180degreesin5sec
LikeMoveToandMoveBy,thedifferencebetweenRotateToandRotateByistheuseofabsoluteandrelativerotations.
TipThemathmodule
ThePythonstandardlibrarycontainsamodulewithcommonlyusedmathematicalfunctions.Sincecocos2dworkswithdegreesforrotationactions,themath.degrees(radians)andmath.radians(degrees)conversionutilitiesincludedinthemathmodulecanbeextremelyuseful.
Seetheentirefunctionalityprovidedbythismoduleathttps://docs.python.org/3.4/library/math.html.
Thecompletereferenceofinstantactionsincludedinthecocos.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
InstantactionsWehavejustintroducedintervalactions.Theseareappliedoveradurationoftime.Nowwewillcoverinstantactions,whichareappliedimmediatelytotheCocosNodeinstance.Actually,theyareinternallyimplementedasintervalactionswithzeroduration.OneexampleofaninstantactionisCallFunc,whichinvokesafunctionwhentheactionisexecuted:
importcocos.actionsasac
defupdate_score():
print('Updatingthescore…')
sprite=cocos.sprite.Sprite('tank.png',position=(200,200))
sprite.do(ac.CallFunc(update_score))
Aswewillseelater,thisactionwillbehandywhenwewanttocallaspecificfunctionduringasequenceofactions.
Notethattheupdate_scoreargumentinthelastlineisnotfollowedby().Thisisasubtlebutimportantdifference;itmeansthatwearecreatingCallFuncwithareferencetotheupdate_scorefunction,insteadofactuallyinvokingit.
Thefollowingtablecontainsalltheactionsdefinedinthecocos.actions.instant_actionsmodule:
Instantaction Description
Place Placesthetargetintheposition(x,y)
CallFunc Callsafunction
CallFuncS Callsafunctionwiththetargetasthefirstargument
Hide HidesthetargetbysettingitsvisibilitytoFalse
Show ShowsthetargetbysettingitsvisibilitytoTrue
ToggleVisibility Togglesthevisibilityofthetarget
CombiningactionsBynow,youknowhowtoapplyactionsseparately,butyouneedtocombinetheminordertoachieveamorecomplexbehavior.
TheActionclass,definedinthecocos.actions.base_actionsmodule,isthebaseclassthatInstantActionandIntervalActioninheritfrom.Itimplementsthe__add__specialmethod,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,andfinally,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,andduringthefirst2seconds,itrotates90degreesclockwise.
The__add__and__or__specialmethodscanbecombinedtoproduceasequenceofactionsthatrunbothinparallelandsequentially:
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,butwhentheparallelactionsaredone,itremovesthesprite.
TipPython’sspecialmethods
Cocos2dimplementsactionsequencesbyemulatingnumericoperators,butthesecouldhavebeenimplementedwithnormalmethods.Inprograming,thiskindofshortcutofferedbyalanguageiscalledsyntacticsugar,anditsuccinctlyexpressesafunctionalitythatcanalsobeimplementedinamoreverbosemanner.
CustomactionsWehaveafulllistofactionsincludedinthelibrary,butwhatifnoneoftheseactionsperformthevisualeffectthatwedesireandwewanttodefineanewaction?
Wecancreateinstantorintervalactionsbyextendingthecorrespondingclass.Forourgame,wewantanactionthatindicatesvisuallythatanenemytankhasbeenhitbyapplyingaredfiltertothetank,anditshouldgraduallyreturntotheoriginalcolor.
Inthiscase,wedefineanIntervalActionsubclasscalledHit,keepinginmindthatthefollowingstepswillbeperformedinternallybycocos2d:
Theinit(*args,**kwargs)iscalled.Oneofthesekeywordargumentsshouldbethedurationoftheaction,soyoucansetitasyourdurationattribute.Donotconfusethiswiththe__init__specialmethod.Acopyoftheinstanceismade;usually,thisshouldnothaveanysideeffect.Thestart()iscalled.Fromhere,theself.targetattributecanbeused.Theupdate(t)iscalledseveraltimes,wheretisthetimeinthe(0,1)range.Then,update(1)iscalled.Thestop()iscalled.
Itisnotnecessarytooverrideallofthesemethods,butonlythosethatarerequiredtoimplementyouraction.Forinstance,weonlyneedtostorethedurationandupdatethecolorofthespritedependingontheelapsedtime,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),whichlooksasifaredfilterisbeingappliedtotheimage.Theredtonewilldecreaseastheelapsedtime,t,risesmonotonically.
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,youcanseethissnippetwithalloftheusualcoderequiredtorunthescene.
AddingamainmenuTheSpaceInvadersversionthatwedevelopedinthepreviouschapterstartsanewgameassoonastheapplicationisloaded.Inmostgames,aninitialscreenisdisplayedandtheplayercanchoosebetweendifferentoptionsapartfromstartinganewgame,suchaschangingthedefaultcontrolsortakingalookatthehighscores.
Thecocos.menucocos2dmoduleoffersaLayersubclassnamedMenu,whichservesexactlythispurpose.Byextendingit,youcanoverrideits__init__methodandsetthestyleofthetitle,themenuitems,andtheselectedmenuitem.
TheseitemsarerepresentedasalistofMenuIteminstances.Oncethislistisinstantiated,youcancallthecreate_menumethod,whichbuildsthefinalmenuwiththeactionsthatareexecutedwhenamenuitemisselected.
WhilethebasicMenuItemonlydisplaysastaticlabel,thereareseveralMenuItemsubclassesfordistinctinputmodes:
ToggleMenuItem:TogglesaBooleanoptionMultipleMenuItem:SwitchesbetweenmultiplevaluesEntryMenuItem:ThisisthemenuitemforenteringatextinputImageMenuItem:ShowsaselectableimageinsteadofatextlabelColorMenuItem:Thisisthemenuitemforselectingacolor
AlloftheseclassesexceptImageMenuIteminvokeacallbackfunctionwhenitsvaluechangestothisnewvalueasthefirstargument.
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,wecanpassacocos2dactionthatisappliedwhenamenuitemisactivatedandwhenitisdeactivated.Thecocos.menumoduleincludesafewactions,andweimportedtheshake()andshake_back()actions,whichperformalittleshakemovementatthecurrentmenuitemandreturntotheoriginalstylewhentheoptionisdeselected,respectively.
Therestofthecodehasbeenomittedforbrevity;youcanfindthecompletescriptinChapter3_02.pywiththeclassmethodsthatarecalledwhenamenuitemisactivated.Notethereferencetothepyglet.app.exitfunction,whichfinalizesthecocos2dapplication.
Withjustafewlinesofcode,wehavebuiltamenuwheretheplayercaninputtheirname,setthegamedifficulty,andtoggletheFPSdisplay.Themenuitemscanbeselectedwiththearrowkeysorthemousepointer.
Thisisthetypeofmainmenuwearelookingforforourtowerdefensegame.Sincethisisonlyonelayer,wecanlateraddabackgroundlayerthatimprovesthevisualappearanceofthemenu.
NowthatyouhaveunderstoodthemenuAPI,let’smoveontothedesignofthemainscene.
TilemapsThetechniqueoftilemapsbecameasuccessfulapproachtostoringlargeamountsofinformationaboutgameworldswithsmall,reusablepiecesofgraphics.In2Dgames,tilemapsarerepresentedbyatwo-dimensionalmatrixthatreferencestoatileobject.Thisobjectcontainstherequireddataabouteachcelloftheterrain.
Theinitialsheetusedbythetilemapcontainsthe“buildingblocks”ofourscenario,anditlookslikewhatisshowninthefollowingscreenshot:
Startingfromthissimpleimage,wecanbuildagridmapinwhicheachcellisoneofthesquaresthesheetisdividedinto.
TiledMapEditorWewilluseTiledMapEditor,ausefultoolformanipulatingtiledmaps.Itcanbedownloadedforfreefromhttp://www.mapeditor.org/,anditcanberunonmostoperatingsystems,includingWindowsandMacOS.
Thissoftwareisalsowell-suitedforleveldesign,sinceyoucaneditandvisualizetheresultingworldinasimpleway.
Oncewehaveinstalledandlaunchedtheprogram,wecanloadtheimagethatwewilluseforourtilemap.Forthisgame,wehaveusedaPNGimagethatisalreadybundledwiththeMapEditorinstallation.
Inthemenubar,gotoFile|New…tocreateanewmap.Whenanewmapiscreated,youwillbepromptedforsomebasicinformation,suchasthemaporientation,mapsize,sizeofthepattern,andspacingbetweenthecells.Inourgame,wewilluseanorthogonalmapof640x480pixelsand32x32pixelsforeachcell,asshowninthefollowingscreenshot:
NowgotoMap|NewTileset…,enterthenamemap0,andloadthetmw_desert_spacing.pngimagefromtheexamplesfolder,asshowninthisscreenshot:
YoucanusethetilesshownintheTilesetviewtodrawamaplikethisone:
Oncethemapisdrawn,wecansaveitinseveralfileformats,suchasCSV,JSON,orTXT.WewillchoosetheTMXformat,whichisanXMLfilethatcanbeloadedbycocos2d.GotoFile|Saveas…tostorethetiledmapwiththisformat.
LoadingtilesThankstothecocos.tilesmodule,wecanloadourtilemapsaslayersandmanipulatethemlikeanyotherCocosNodeinstance.IfourTMXfileandthecorrespondingPNGimagearestoredintheassetsfolder,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)
ThescenariodefinitionOncewehaveloadedthetilemap,weneedtolinktheresultingimagewiththegameinformation.Ourscenarioclassshouldcontainthefollowing:
ThepositionswheretheturretscanbeplacedThepositionofthebunkerTheinitialpositionforenemyspawningThepaththattheenemiesmustfollowtoreachthebunker
Inthefollowingscreenshot,wecanseethisdataoverlaidontopofourTMXmap:
Therectanglesrepresenttheslotsinwhichtheplayercanplacetheturrets.Thescenariostoresonlythecentersofthesesquares,becausethegamelayerwilltranslatethesepositionsintoclickablesquares.
Thelinesovertheroadrepresentthepaththattheenemytanksmustfollow.ThismovementwillbeimplementedbychainingtheMoveByandRotateByactions.Wewilldefinetwoconstantsforrotationtowardtheleftortheright,andanauxiliaryfunctionthatreturnsaMoveByactionwhosedurationmakestheenemiesmoveuniformly:
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
willbechainedtorepresentthecompletelistofstepsrequiredtofollowthegreenpathshowninthepreviousscreenshot.Forinstance,thiscouldbeahypotheticalusageoftheseutilitiestocomposeachainofactions:
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,butalsomakesourcodemoreexpressiveandreadable.Nowlet’sencapsulatethislogicinaclassthatwillholdthisdata.
TipDomain-specificlanguages
Thisuseofabstractionstorepresenthigh-levelconceptsiswidelyfoundinprogramming,andgamedevelopmentisnotanexception.Whenthissyntaxreachesawholedomainofspecializedfeatures,theresultinglanguageiscalledaDomain-specificLanguage(DSL).
WhilethissmalldomainonlycoversspritemovementsandcannotbeconsideredaDSLbyitself,advancedgameenginesincludespecificscriptinglanguagesthatarefocusedongamedevelopment.
ThescenarioclassLet’sstartthedefinitionofthescenariomodulebycreatinganemptyfilecalledscenario.pyandopeningitwithourfavoritetexteditor.ThismodulewillcontainthedefinitionoftheScenarioclass,whichgroupsthepreviouslydiscussedinformation,suchasthebunker’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,wewillusethe@propertydecorator.Itallowsustoaccesstoanattributebyinvokingfunctionsthatwillactasgettersandsetters:
@property
defactions(self):
returnself._actions
@actions.setter
defactions(self,actions):
self._actions=ac.RotateBy(90,0.5)
forstepinactions:
self._actions+=step
Thisdecoratorwrapstheaccesstotheinternal_actionsmember.Givenaninstanceofthisclassnamedscenario,theretrievalofthescenario.actionsmemberwouldtriggerthegetterfunction,whileanassignmenttoscenario.actionswouldtriggerthesetterfunction.
Nowwecandefineafunctionthatinstantiatesascenarioandsetitsmembersbasedonthedispositionofour'map0'.Ifwehaddesignedmorelevels,wecouldhaveaddedmorefunctionsthatcreatethesenewscenarios:
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:fromscenarioimportget_scenario.Thenextscriptthatwewillwriteisresponsiblefordefiningourgame’smainmenu.
TransitionsbetweenscenesInaprevioussection,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:
AnoptionforstartinganewgameAtoggleitemforshowingtheFPSlabelAquitoption
Tostartanewgame,wewillneedtheinstanceofthemainscene.Itwillbereturnedbycallingthenew_gamefunctionofmainscene.py,whichhasnotbeendevelopedyet.Besides,wewilladdatransitionwiththeFadeTRTransitionclassfromthecocos.scenes.transitionsmodule.
Atransitionisascenethatperformsavisualeffectbeforesettingthecontrolofanewscene.Itreceivesthissceneasitsfirstargumentandsomeoptions,suchasthetransitiondurationinseconds:
fromcocos.scenes.transitionsimportFadeTRTransition
frommainsceneimportnew_game
game_scene=new_game()#Instanceofcocos.scene.Scene
transition=FadeTRTransition(game_scene,duration=2)
Now,toreplacethecurrentscenewiththenewonedecoratedwithatransition,wecalldirector.push.Inthisway,theon_start_menumethodofourMainMenuclassisimplemented:
defon_new_game(self):
director.push(FadeTRTransition(new_game(),duration=2))
Thevisualeffectproducedbythistransitionisthatthecurrentscene’stilesfadefromtheleft-bottomcornertothetop-rightcorner,asshownhere:
Youcanfindthecompletecodeofthismoduleinthemainmenu.pyscript.
GameovercutsceneTocreateacutscenewhenthegameisover,wewillneedasimplelayerandatextlabel.SinceasceneisaCocosNode,wecanapplyactionstoit.Toholdthisscreenforamoment,itwillperformaDelayactionthatlasts3seconds,andthenitwilltriggeraFadeTransitiontothemainmenu.
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’shealthpointsdecreasetozero.
ThetowerdefenseactorsWewilldefineseveralclassesintheactors.pymoduletorepresentthegameobjects.
Aswithourpreviousgame,wewillincludeabaseclass,fromwhichtherestoftheactorclasseswillinherit:
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,whenthespritewasdisplacedbycallingthemove()method,bothcshapeandpositionwereupdatedatthesametime.However,actionssuchasMoveByonlymodifythespriteposition,andtheCShapecenterisnotupdated.
Tosolvethisissue,wewraptheaccesstotheCShapememberthroughthecshapeproperty.Withthisconstruct,whentheactor.cshapevalueisread,theinternal_cshapeisupdatedbysettingitscenterwiththecurrentspriteposition.
ThissolutionispossiblebecauseanobjectonlyneedsacshapememberinordertobeavalidentityforaCollisionManager,andapropertyisexposedinthesamewayasanyotherattributethatisdirectlyaccessed.
Nowthatouractorbaseclasshasbeendefined,wecanstartimplementingtheclassesfortheturretsandtheenemytanks.
TurretsandslotsTurretsaregameobjectsthatactautonomously;thatis,thereisnoneedtobindinputeventswiththeiractions.Thecollisionshapeisnotthespritesizebutthefiringrange.Therefore,acollisionbetweenaturretandatankmeansthattheenemyisinsidethisrangeandcanbeconsideredavalidtarget:
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)
Theshootinglogicisimplementedbyschedulingafunctionthatincrementsthereloadcounter.Whenthesumofelapsedsecondsreachestheperiodvalue,thecounterisdecreasedandtheturretcreatesashootspritewhoseaimisatthecurrenttarget:
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,acollisionwiththecircularshapethatrepresentsthefiringrangealsorotatestheturret,givingtheimpressionthatitisaimingatthetarget.
Tocalculatetheanglebywhichthespritemustrotate,wecalculatethevectorthatrunsfromtheturrettothetarget.Withtheatan2functionfromthemathmodule,wecalculatetheanglebetweentheπ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.Throughasequenceofactions,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,wewillneedaclasstorepresenttheslotsinwhichtheturretscanbeplaced.SincewewilltakeadvantageoftheCollisionManagerfunctionalitytodetectwhetheraCShapehasbeenclickedon,thisclassonlyneedsacshapemember:
classTurretSlot(object):
def__init__(self,pos,side):
self.cshape=cm.AARectShape(eu.Vector2(*pos),side*0.5,side*0.5)
InstancesofthisclasswillbeaddedtoadifferentCollisionManagersothattheydonotconflictwiththerestofthegameobjects.
EnemiesAnenemytankisanactorthatfollowsthescenariopathuntilitreachesthebunkerattheendoftheroad.Itisdestroyediftheturretshotsreduceitshealthpointstozero.
Theplayerincrementstheirscoreeachtimeatankisdestroyed,soapartfromhealth,wewillneedascoreattributetoindicatehowmanypointstheplayerearns:
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)
Sincewemustdifferentiatebetweenwhetherthetankhasexplodedbecauseithasbeendefeatedbytheturretsorbecauseithasreachedtheendpoint,wewilluseadestroyedflag.Thisflagwillbesettotrueonlyifthetankisdestroyedbyaturret.
WealsochecktheCocosNodeflagcalledis_running,sinceitissettofalsewhenthenodeisremoved.Thus,wecanpreventthetankfrombeingremovedwhenithasalreadybeenkilled:
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,anexplosionwillbesimulatedwithafastsequenceofspritesthatlastsfor0.07seconds.Toavoidhavingtoomanyspriteinstancesinourlayer,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))
BunkerYoumightrememberthedescriptionofthegameplay.Weneedtokeeptheenemiesawayfromaspecificarea.Inourversion,itisrepresentedbyabunkerthatisplacedattheendoftheroad.
Thebunkerinstanceonlyneedstoprocesstheenemycollisionsanddecreaseitshealthpoints.Theinitialnumberofhealthpointsis100,andeachcollisionsubtracts10pointsfromthistotal:
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_runningissettotruetoavoidcallingkill()whenthebunkerhasalreadybeenremoved.
GamesceneThegamelayercontainsseveralattributesforholdingthereferencetotheHUDlayer,thescenario,andthegameinformation,suchasthescoreorthenumberofpointsthatcanbespenttobuildnewturrets.
Theseclassesareaddedtothegamelayermodule,whichhascontainedonlythegameovertransitionsofar:
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)
Anotherdifferencefromourpreviouscocos2dgameistheusageoftwocollisionmanagers:coll_manforthegameactorsandcoll_man_slotsfortheturretslots.Whilethefirstoneisupdatedduringeachiterationofthegameloop,thesecondonecontainsstaticshapesthatdonotconflictwiththeactors’collisions.Asaresult,weavoidunnecessaryadditionsandremovalsfromtheturretslotshapes;thus,improvingthecollisioncheckingperformance.
Bothpointsandscorearepropertiesthat,apartfromaccessingthe_pointsand_scoreinternalattributes,updatetheHUDwiththenewnumericvalue.Here,wewillshowthepointsproperty,butscorehasasimilarimplementation:
@property
defpoints(self):
returnself._points
@points.setter
defpoints(self,val):
self._points=val
self.hud.update_points(val)
Thegameloopisquitesimplesincethelogicofthegameismainlyimplementedwithactions:
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.Sincetheonlyentitiesmanagedbyself.coll_manaretheenemytanks,wedonothavetoworryaboutverifyingthattheotherobjectisatankandnotaturret.Enemiesarerandomlyspawnedwithagivenprobability.Weleftastaticvalue,butitcouldhavebeencalculateddependingonthenumberofenemiesdefeatedsothatthegamebecomesincreasinglymoredifficult.
Thecreate_enemy()methodplacesanewtankattheinitialposition.Topreventthemfromalwaysspawningatthesamecoordinates,wewillapplyarandomoffsetof+/-10pixelsinboththexandycomponents:
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,andwewillregisteronlyonemethodtoprocessmouseevents.Withtheobjs_touching_point()method,wewillknowwhetheranyslothasbeenclickedon.Iftheplayerhasenoughpoints,anewturretinstanceisplacedatthecenteroftheslot’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()methodtodetectwhichtypeofobjecthasbeenremoved:
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,andthedirectorreplacesthecurrentscenewiththeGameOvercutscene.Ifthenodeisanenemytank,thescoreandthepointsareupdatedbeforeactuallyremovingtheobject.
TheHUDclassThislayerisresponsiblefordisplayingthegameinformation,afunctionalitysimilartotheHUDwebuiltinourpreviousgame:
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
AssemblingthesceneToconcludeourmainscene.pymodule,wewilldefinethenew_game()function.Thisfunctionisusedbythemainmenuwhenanewgameisstarted.Itreturnsthescenewiththetilemap,theHUDlayer,andthegamelayerinitializedanddisplayedinthecorrectorder:
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,wewillloadtheOswaldfontandaddtheimagestotheresourcepathwiththePygletAPI:
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.Thefollowingscreenshotshowsthefinalprojectlayout:
SummaryWiththisproject,wesawhowtoincludemenus,transitions,andactionsinourcocos2dapplications.Ourcodeissplitintomultiplemodules,enhancingabetterorganizationofourgames.
Thisgamecanbethebaseforamorecomplextowerdefensegame.Youcancustomizethenumberofpointsrequiredtocreateanewturret,orchangetheprobabilityofspawningenemies.Asanexercise,trytocreateanewTMXmapwithMapEditoranddefineacustomscenariobasedonthisbackground.Yourimaginationisthelimit!
Chapter4.SteeringBehaviorsOurpreviouscocos2dgamewasbasedonactions,somovementsandrotationswerepredefinedandthecharacterswerenotinfluencedbythecurrentstateofthegame.However,arecurringproblemingamedevelopmentishowtorecreatelife-likeanimations,suchaspursuingatargetoravoidingmovingobstacles.
Now,youwilllearnhowtoapplysteeringbehaviors,atechniqueusedtocreateseeminglyintelligentmovementsforautonomouscharacters.Theimplementationofthesestrategiesachievestheabilitytonavigatethroughthegameworldwithimprovisedpatterns.
Finally,wewillputthesestrategiesinpracticewithparticlesystems,aCocos2dmodulethatwehavenotworkedwithsofar.Sincewewillusesimpleshapes,theseparticlesystemswillrepresentourcharacters,withtheadvantageofusnotrequiringexternalassetsforourapplications.
Inthischapter,wewillcoverthesetopics:
BasicconceptsofsteeringbehaviorsBehaviorsforindividualsandgroupsMixingthesestrategiesHowtorenderparticlesystemswithcocos2d
NumPyinstallationTousetheparticlesystemsupportofcocos2d,itisnecessarytoinstallNumPy,aPythonpackageusedtooperatewithlargearraysandmatricesinanefficientway.SinceitcontainsseveralCmodules,itmightbedifficulttoinstallitonWindowssystemsbecauseyoumightnothavetheappropriatecompiler.
YoucandownloadtheofficialbinariesforWindowsandMacOSXfromtheNumPysiteathttp://sourceforge.net/projects/numpy/files/NumPy/1.9.2/.
AnotheroptionistodownloadtheunofficialcompiledbinariesfromChristophGohlke’swebsiteathttp://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy.Here,thepackagesareuploadedas.whlfiles.Thisistheextensionofthewheelformatandcanbeinstalledwithpip:
$pipinstallnumpy‑1.9.2+mkl‑cp34‑none‑win32.whl
Inbothcases,remembertoinstallthebinariesforPython3.4,sincetheversionsforPython2.7and3.3areavailablefordownloadaswell.
Youcancheckwhethertheinstallationwassuccessfulbyrunningthiscommand:
$python-c"importnumpy;print(numpy.version.version)"
1.9.2
OnceNumPyisinstalled,youhavealltherequirementsforsupportingparticlesystemsincocos2d.
TheParticleSystemclassThebaseclassforcocos2dparticlesystemsisParticleSystem,asdefinedinthecocos.particlemodule.Thefollowingtablelistssomeofthemostrelevantclassmembers.Notethattheonesendingin_varindicatethatrandomvariancecanbeappliedtothebasevalue.
Classmember Description
Active Indicateswhethertheparticlesystemisspawningnewparticlesornot.
DurationThedurationofthesysteminseconds.Thisvalueis-1forinfiniteduration.
Gravity Gravityoftheparticles.
angle,angle_var Theangulardirectionoftheparticlesmeasuredindegrees.
speed,speed_var Thespeedoftheparticles.
tangential_accel,
tangential_accel_valTangentialacceleration.
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.
Youcancreateyourownparticlesystemsbyextendingthisclassandredefiningthevaluesoftheseclassmembers.
Cocos2dincludesanothermodule,cocos.particles_systems,withsomepredefinedParticleSystemsubclasses,eachproducingadifferentvisualeffect.ThenamesoftheseclassesareFireworks,Spiral,Meteor,Sun,Fire,Galaxy,Flower,Explosion,andSmoke.
AquickdemonstrationThiscodeisabasicexamplethatshowshowtoaddastaticParticleSystemtoourlayerwithoutprocessinganyuserinputoractions:
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()orps.Fireworks().ManyIntegratedDevelopmentEnvironments(IDE),suchasPyCharmorPyDevm,offercodecompletionandwilllistallthenamesthataredefinedinamoduleaftertypingitsname.
ImplementingsteeringbehaviorsThestrategiesthatwewillcoverherehavebeentakenfromCraigReynolds’spaperSteeringBehaviorsforAutonomousCharacters,writtenin1999.Ithasbecomeawell-knownreferenceforimplementingautonomousmotioninaneasymannerfornon-playablecharacters.
Youcancheckouttheonlineversionathttp://www.red3d.com/cwr/steer/gdc99/.Inthissection,youwilllearnhowtoimplementthefollowingkindsofbehavior:
SeekandfleeArrivalPursuitandevadeWanderObstacleavoidance
SeekandfleeTheseekbehaviormovesthecharactertowardsaspecificpositioninthespace.Thisbehaviorisbasedonthecombinationoftwoforces:thecharacter’svelocityandthesteeringforce.Thisforceiscalculatedasthedifferencebetweenthedesiredvelocity(thedirectionfromthecharactertothetarget)andthecharacter’scurrentvelocity.
Notethatthisisnotaforceinthestrictphysicsdefinition;itisjustanothervelocityvector.Youcanthinkofitasacorrectionofthecharacter’svelocity,andtheresultingpathwillbeasmoothcurvethatadjuststhevelocityuntilitisalignedtowardsthetarget.
Fleeistheinverseofseek,anditmakesthecharactermoveawayfromthetarget.Itmeansthatthesteeringforcepushesthecharacterawayfromthetarget.
Youcanseebothseekandfleerepresentedgraphicallyinthefollowingdiagram.Thecurvedpathsshowthefinaldirectiononcetheforcesarecombined,therightonetowardsthetarget(seek),andtheleftoneawayfromthetarget(flee).
Forourimplementation,weneedaclassthatrepresentstheautonomouscharacterwiththefollowingmembers:
velocity:Atwo-dimensionalvectorrepresentingtheactor’svelocityspeed:Theactor’sspeed,measuredinthenumberofpixelsperframemax_force:Themaximummagnitudeofthesteeringforcemax_velocity:Themaximummagnitudeofthevelocityvectortarget: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.Thisgivesusthesteeringforce.Truncatethisforcewiththemaximumforce.Sumthesteeringforcewiththecurrentvelocityandlimitittothemaximumvelocitythatcanbereached.Updatethepositionwiththevelocityperframe.
Thisfigureshowshowthesevectorsareaddedtoachievetheresultingpathtothetarget:
Thetruncate()functionlimitsthemagnitudeofavector,preventingthevelocityfromreachingagreatermodulethanallowed:
deftruncate(vector,m):
magnitude=abs(vector)
ifmagnitude>m:
vector*=m/magnitude
returnvector
Ifthevectormoduleisgreaterthanthemaximumvalue,thevectorisnormalizedandscaledbythisvalue.
Thecharacter’stargetwillbethemousepointer,andthetargetcoordinatesmustbeupdatedwhenthemouseismoved.Todoso,wewilldefineacustomlayerthathandlesthemousemotioneventsandsetstheactor’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,thecharacterwillseekthemousepointerwhenyoumoveitoverthewindow.
Forthefleebehavior,simplychangethesignofthevelocitywhenthepositionisupdated:
defupdate(self,dt):
ifself.targetisNone:
return
#...
self.position+=self.velocity*dt*-1
Thiswillmakethecharacterfleefromthemousepointer,andeventuallyleavethewindowifthemouseisclosertothecenterthanthecharacter.Intheseek_and_flee.pyscript,youcanfindacombinationoftheseimplementations,whereitispossibletoswitchthemwiththemouseclick.
ArrivalYoumaynoticethatwiththeseekbehavior,ifthecharacter’svelocityistoohigh,itwillpassthroughthetargetandcomebacksmoothly.
Thearrivalbehaviorlowersthevelocityoncethecharacterhasreachedaminimumdistanceclosetothetarget,decreasingitgradually.Thisdistancegivesustheradiusoftheslowingarea,acirclecenteredatthetargetpositionthatcausesthecharacter’svelocitytodecrease.
Theimplementationofthisisquitesimilartothatoftheseekbehavior.Weneedtoaddtheslow_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()methodsothatthesteeringforceisdecreasedlinearlybytherampfactor.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,therampfactorequals0andthesteeringforcebecomes-self.velocity.
Checkoutthearrival.pyfilewiththecompleteimplementationofthebehavior.
PursuitandevadeThepursuitbehaviorissimilartoseek,withthedifferencebeingthatthecharacterwillmovetowardsthepositionatwhichthetargetwillbeinthefuturebasedonthetarget’scurrentvelocity.
Thefuturepositionisthesumofthetargetpositionplusthevelocityvectormultipliedbyaunitoftime.Itgivesthecoordinateswherethetargetwillbeplacedwithin1secondifitdoesnotmodifyitsvelocity:
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
Ifyouwanttosetthefuturepositionasthecoordinateswherethetargetwillbewithin2seconds,multiplythetarget’svelocityby2.
Inthisexample,wewillreplaceourmousepointertargetwithamovingnode.Itwillhavealinearvelocity,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,weneedtochangeonlythelaststatementoftheupdate()methodtoself.position+=self.velocity*dt*-1,exactlyaswedidforthefleebehavior.
Thecodeofthisbehaviorisincludedinthepursuit.pyscript.
WanderThewandersteeringsimulatesarandomwalkwithoutanyspecifictarget.Itproducesacasualmovementwithoutsharpturnsorpredictablepaths.
Itcanbeimplementedwithaseekbehaviorwithtargetscalculatedrandomly.However,amoreorganicsolutionistogeneratearandomsteeringforcetowardsapointonacircumferenceplacedaheadofthecharacter.
Thisforceiscalculatedperframe,givingsmallrandomdisplacementsthatproducethevisualeffectofthecharacterwanderingaround.Thisbehaviorcanbeparameterizedwiththefollowingvalues:
wander_angle:Thecurrentangleofdisplacement,towhichthesmallvariationswillbeaddedcircle_distance:Thedistanceofthecharacter’spositionfromthewandercircle’scentercircle_radius:Theradiusofthewandercircleangle_change:Thefactorbywhichtherandomvaluewillbemultipliedtoproduceachangeinthewanderangle
Nowthatwehaveseenhowtheseforcesarecalculated,wecanimplementourwanderbehavior.
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’svelocity.Wecalculatethedisplacementwithwander_angle,anditisscaledaspertheradiusofthecircle.Givenarandomvaluebetween0.0and1.0,wesubtract0.5sothatthevalueisbetween-0.5and0.5,thuspreventingtheanglefromalwayschangingbyapositivevalue.Wemultiplythisvaluebyangle_changeandaddittothewander_anglemember.
Youmaynoticeanotherstatementafterthepositionisupdatedwiththevelocity.Thislaststatementofthemethodisusedtorelocatethecharacter,sinceitisverylikelythatthecharacterwillleavethescreenduetotherandomnessofitsmovement.
Feelfreetotweakthecircle_distance,circle_radius,andangle_changevalues,andobservethedifferencesthattheycausetotheplayer’smovement.
Ourmainlayerissimplifiedsinceitonlyneedstoaddtheactor:
classMainLayer(cocos.layer.Layer):
def__init__(self):
super(MainLayer,self).__init__()
self.actor=Actor(320,240)
self.add(self.actor)
Thewander.pyscriptcontainsthecompleteimplementationofthisbehavior.
ObstacleavoidanceSofar,wehavenotplacedanybarrierinourworld,sothecharactercanfreelymovearoundwithoutavoidinganyarea.
However,mostgamesplacesomeobstacles,andthecharacterscannotpassthroughthem.Theobstacleavoidancebehaviorgeneratesasteeringforcethatcausesthecharactertotryanddodgetheseblockingitems.Notethatthisdoesnotperformcollisiondetections,soifthecurrentspeedishighandthesteeringforcemoduleisnotenoughtocounteractthecharacter’svelocity,thecharactermightoverlapwiththeshapeoftheobstacle.
Todetectthepresenceofobstaclesahead,thecharacterwillkeepanimaginaryvectoralongitsforwardaxis,whichrepresentsthedistanceuptowhichitcanseeahead.Theclosestobstaclethatcollideswiththissegment,ifany,willbethethreattoavoid.
WewilldefineanObstacleclasstorepresentthecircularobstaclesthatwewilluse.Eachofthemcanhaveadifferentradius,andwewillkeeptrackofallthecreatedinstanceswithaclassmemberlist:
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,themaximumdistanceatwhichthecharactercandetectthepresenceofanobstacle,andmax_avoid_force,themaximumforcethatcanbeappliedtododgeanobstacle:
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,withthedifferencethattheavoidforceisappliedaswell:
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.Tocheckwhethertheaheadvectorintersectseachobstacle,wewillcalculatetheminimumdistancefromthecircle’scentertothevector.Ifthisdistanceislowerthanthecircle’sradiusanditistheminimumdistance,theobstacleistheclosestone.
Tofindthedistancebetweenavectorandapoint,wemustprojectthatpointtothevectorandseeifitfallsintoitslength.Ifitdoes,wecalculatethedistancebetweenthesetwopoints,whichisthedistancebetweenthevectorandthepoint.Otherwise,itmeansthatthepointisbehindorbeyondtheprojectionofthepointonthevector.
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,itmeansthatitdoesnotseeanythingaheadandtheavoidanceforceis(0,0).Foreachobstacle,wekeeptrackofthedistancebetweentheaheadvectorandthecircle’scenter.Ifthisdistanceislowerthanitsradiusanditistheclosestobstacle,wesetitastheobstacletoavoid.Finally,ifthereisanyobstacleselected,wescaletheavoidforcebythemax_avoid_forcefactor.
WewilladdsomeobstaclestoourMainLayer,andtheactorwillavoidthemwithoutanyfurtherreferencetothesenewobjects,sincetheyareautomaticallyaddedtotheObstacle.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.Runitandmovethemousepointerabovethescreentoseehowthecharactertriestoavoidtheobstaclesrepresentedbythegreencircles.
GravitationgameToputintopracticewhatyouhavelearnedaboutsteeringbehaviors,wewillbuildabasicgame.Theplayer’sobjectiveistocollectsomepickupitemsthatrotatearounddifferentplanetsplacedintheworld,whileescapingfromenemies.Theenemiesarenon-playablecharactersthatseektheplayablecharacterandavoidtheplanets.
Wewillrepresentthecharactersasparticlesystems,andthefinalversionofthegamewilllooklikethis:
BasicgameobjectsAsusual,wewilldefineabaseclassthatwrapstheaccesstotheCircleShapeattributewiththeupdatedcenter:
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,wewilldefineanotherclasstoupdatethepositionoftheactorbasedonarotationalmovement.Thisclasswillhaveareferencetotheplanetitisrotatingaround,theangularspeed,andthecurrentangleofrotation:
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,becausewhenwesetplanet,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
Thescheduledmethodincrementstherotationanglewiththeelapsedtimeandupdatestheactor’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)
PlanetsandpickupsOurPlanetclasswillrepresentthestaticactorsaroundwhichthepickupsandtheplayerrotate.Wewillkeeptrackofalltheinstances,aswedidwiththeObstacleclasspreviously:
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,whichhasimplementedthisfunctionality:
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)
PlayerandenemiesTheplayercharacterrevolvesaroundtheplanetslikethepickupsdo,butwiththedifferencethatitcanswitchfromoneplanettoanotherwhenthespacebarispressed.
Thisisthecontrolthattheplayerhasovertheactor,sowewillneedtoaddalinearspeedtoimplementthiskindofmovement,whichextendsthelogicofferedbytheMovingActorclass:
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()methodperformsalinearmovementwhentheplayerisnotrevolvingaroundanyplanet:
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,weneedtoimplementthemethodthattriggerstheswitchfromoneplanettoanother.Itcalculatesthedirectionvectordependingonthecurrentangleofrotationwithrespecttotheplanet:
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.Thisgivesvisualfeedbackofthecurrentdirectionofthecharacterandthepositionitispointingto.
TheimplementationoftheEnemyclassisalmostthesameastheonewesawwiththeobstacleavoidancebehavior.ThemaindifferencesarethatthisclassusesthePlanet.instances:listandtheradiusoftheobstaclesisretrievedfromthecshapememberofeachplanet:
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
ExplosionsTherearesomecollisionsthatcantriggeragameevent.Forinstance,whenanitemispickedupbytheplayer,itmustberemoved,orwhentheplayablecharacterhitsanenemyoraplanet,itisdestroyedandspawnsagaininitsoriginalposition.
Onewaytoenhancethesesituationsisthroughexplosions;theygivevisualfeedbacktotheplayer.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
ThegamelayerAllthegameobjectsthatwehavedevelopedwillbeaddedtothemaingamelayer.Asusual,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)
Wealsodefinedacoupleofhelpermethodstoavoidrepeatingthesamecodeduringinitialization:
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,andwhatactorsarerotatingaroundeachplanet,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,specificallythespacebarpressthattriggerswhentheplayer’scharacterstopsrotatingaroundaplanetandleavestheorbitinaperpendiculardirection.
Whenthespacebarispressedagain,thecollisionmanagercalculateswhattheclosestplanetiswithinaspecificdistance—whichwesetupinthe__init__method—andifthereisany,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.Thegamedoesnotrequireanyextraassets,sincealltherenderedelementsarebasedonthecocos2dparticlesystems.
SummaryInthischapter,youlearnedseveralkindsofsteeringbehaviorthatproducerealisticandautonomousnavigationsaroundyourgameworld.Thesestrategiesmightbemixed,causingcomplexpatternsandseeminglymoreintelligentactions.
Trytochangethevaluesoftheconstantsusedineachbehaviortoseehowtoachievedifferentvelocitiesandforces.Keepinmindtheimportanceofgeneratingsmoothmovementsinsteadofpronouncedtwistssothattheexperienceisvisuallyengaging.
Finally,weappliedthisknowledgeinabasicgame.Sinceitisademonstrationofaddinganon-playablecharacter,thegamecanbecomplementedwithalltheingredientsthatwecoveredinthepreviouscharacter(suchasmenus,transitions,andsoon).
Inthenextchapter,wewilljumpinto3DgamedevelopmentwithOpenGL,anewtopicwithmoreadvancedfeaturesanddetailsofalowerlevel.
Chapter5.Pygameand3DInourpreviouschapters,wedevelopedour2DgameswithPythonmodulesthatarebuiltontopofagraphicaluserinterfacelibrary,suchasTkinterandPyglet.Thisallowedustostartcodingourgameswithoutworryingaboutthelower-leveldetails.
Nowwewilldevelopourfirst3DgamewithPython,whichwillrequireanunderstandingofsomebasicprinciplesofOpenGL,apopularmultiplatformAPIforbuilding2Dand3Dapplications.YouwilllearnhowtointegratetheseprogramswithPygame,aPythonlibrarycommonlyusedtocreatesprite-basedgames.
Inthischapter,wewillcoverthefollowingtopics:
AsteadyapproachtoPyOpenGLandPygameInitializinganOpenGLcontextUnderstandingthedifferentmodesthatcanbeenabledwithOpenGLHowtorenderlightsandsimpleshapesIntegratingOpenGLwithPygameDrawingprimitivesandperformanceimprovements
InstallingpackagesPyOpenGLisapackagethatoffersPythonbindingstoOpenGLandrelatedAPIs,suchasGLUandGLUT.ItisavailableonthePythonpackageIndex,soyoucaneasilyinstallitviapip:
$pipinstallPyOpenGL
However,wewillneedfreeglutforourfirstexamples,beforeweintegrateOpenGLwithPygame.Freeglutisathird-partylibrarythatisnotincludedifyouinstallthepackagefromPyPI.
OnWindows,analternativeistodownloadandinstallthecompiledbinariesfromhttp://www.lfd.uci.edu/~gohlke/pythonlibs/#pyopengl.RemembertoinstalltheversionforPython3.4.
Pygameistheotherpackagethatwewillneedinthischapter.Itcanbedownloadedfromtheofficialwebsiteathttp://www.pygame.org/download.shtml.Youcaninstallitfromsourceifyouwantto;thecompilationpagecontainsthestepsforbuildingPygameondifferentplatforms.
WindowsuserscandirectlyusetheMSIforPython3.2ordownloadUnofficialWindowsBinariesfromtheChristophGohlke’swebsite(http://www.lfd.uci.edu/~gohlke/pythonlibs/).
MacintoshuserscanfindtheinstructionsrequiredtocompileitfromsourceonthePygamewebsiteathttp://pygame.org/wiki/macintosh.
GettingstartedwithOpenGLOpenGLisabroadtopicinitself,anditispossibletofindplentyoftutorials,books,andotherresources,usuallytargetedatCorC++.
Sincethischapterisnotintendedtobeacomprehensiveguideforthisspecification,wewilltakeadvantageofGLUT,whichstandsforOpenGLUtilityToolkit.Itiswidelyusedinsmallapplicationsbecauseofitssimplicityandportability,andthebindingsareimplementedinPyOpenGL.
GLUTwillhelpusperformsomebasicoperations,suchascreatingwindowsandhandlinginputevents.
TipGLUTlicensing
Unfortunately,GLUTisnotinthepublicdomain.Thecopyrightismaintainedbyitsauthor,MarkKilgard,whowroteitforthesampleprogramsincludedinRedBook,theofficialOpenGLprogrammingguide.
Thisisthereasonweareusingfreeglut,oneoftheopensourcealternativesthatimplementtheGLUTAPI.
InitializingthewindowThefirstlinesofourscriptwillbetheimportstatementsaswellasthedefinitionofourAppclassandits__init__method.
ApartfromtheOpenGLAPIandGLUT,weimporttheOpenGLUtilityLibrary(GLU).GLUisusuallydistributedwiththebasicOpenGLpackage,andwewilluseacoupleoffunctionsofferedbythislibraryinourexample:
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.Itrepresentsabinarystring,anditisoneofthedifferencesbetweenPython2and3.Therefore,ifyoufindaGLUTprogramwritteninPython2,rememberthatthestringtitleofthewindowmustbedefinedasabinarystringinordertoworkwithPython3.
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.Whileitispossibletopassparameterstothisfunction,wewillleavethiscallwithoutanyarguments.glutInitDisplayMode():Thissetsthedisplaymodeofthetop-levelwindowthatwewillcreate.ThemodeisthebitwiseORofafewGLUTdisplaymodemasks.GLUT_DOUBLEisthemodeforthedoublebuffer,whichcreatesseparatefrontandbackbuffers.Whileoneofthesebuffersisbeingdisplayed,theotheroneisbeingrendered.Ontheotherhand,GLUT_DEPTHrequestsadepthbufferforthewindow.Itstoresthezcoordinateofeachgeneratedpixel,andifthesamepixelisrenderedforasecondtimebecausetwoobjectsoverlap,itdetermineswhichobjectisclosertothe
camera,thatis,reproducingthedepthperception.glutInitWindowPosition()andglutInitWindowsSize():Thesesettheinitialpositionofthewindowanditssize.Accordingtoourwidthandheightinstancemembers,itindicatestocreateawindowof800x600pixelswithanoffsetof50pixelsinthexandyaxesfromthetop-leftcornerofthescreen.glutCreateWindow():Thiscreatesthetop-levelwindowofourapplication.Theargumentpassedtothisfunctionisabinarystringforuseasthewindowtitle.glEnable():ThisisthefunctionusedtoenabletheGLcapabilities.Inourapp,wecallitwiththefollowingvalues:
GL_DEPTH_TEST:Thisperformsdepthcomparisonsandupdatesthedepthbuffer.GL_LIGHTING:Thisenableslighting.GL_LIGHT0:ThisenablesLight0.PyOpenGLdefinesaspecificnumberoflightconstants—fromGL_LIGHT0toGL_LIGHT8—buttheparticularimplementationofOpenGLthatyouarerunningmightallowmorethanthisnumber.
TipLightingandcolors
Whenlightingisenabled,thecolorsarenotdeterminedbytheglColorfunctionsbutbythecombinationofthelightingcomputationandthematerialcolorssetbyglMaterial.TocombinelightingwithglColor,itisrequiredthatyouenableGL_COLOR_MATERIALfirst:
glEnable(GL_COLOR_MATERIAL)
#...
glColor4f(r,g,b,a)
#Drawpolygons
OncewehaveinitializedGLUTandenabledtheGLcapabilities,wecompleteourstart()methodbyspecifyingtheclearcolor,settingtheperspective,andstartingthemainloop:
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,eachpixelwillhavethisvalueifnoothercolorisrenderedinthispixel.glMatrixMode():Thissetsthematrixstackmodeformatrixoperations,inthiscase
totheprojectionmatrixstack.OpenGLconcatenatesmatrixoperationsforhierarchicalmodes,makingiteasytocomposethetransformationofachildobjectrelativetoitsparent.WithGL_PROJECTION,wesetthematrixmodefortheprojectionmatrixstack.gluPerspective():Thepreviousstatementsetstheprojectionmatrixstackasthecurrentstack.Withthisfunction,wecangeneratetheperspectiveprojectionmatrix.Theparametersthatgeneratethismatrixareasfollows:
fovy:Theviewangleindegreesintheydirection.aspect:Thisistheaspectratioofthefieldofview.Itistheratiooftheviewportwidthtotheviewportheight.zNear:ThedistancefromtheviewertotheNearplane.zFar:ThedistancefromtheviewertotheFarplane.
WithglMatrixMode(GL_MODELVIEW),wesetthemodelviewmatrixstack,whichistheinitialvalue,asthecurrentmatrixmode.
ThelastthreeGLUTcallsdothefollowing:
glutDisplayFunc():Thisreceivesthefunctionthatwillbeinvokedtodisplaythewindow.
glutSpecialFunc():Thissetsthekeyboardcallbackforthecurrentwindow.NotethatthiscallbackwillbetriggeredonlywhenthekeysrepresentedbytheGLUT_KEY_*constantsarepressed.glutMainLoop():Thisstartsthemainloopoftheapplication.
WiththeOpenGLcontextinitialized,weareabletocalltheOpenGLfunctionsthatwillrenderourscene.
TipTheOpenGLandGLUTreference
Asyoumayhavealreadynoticed,theOpenGLandGLUTspecificationsdefinealargenumberoffunctions.YoucanfindthebindingsoftheseAPIsimplementedbyPyOpenGLontheofficialwebsiteathttp://pyopengl.sourceforge.net/documentation/manual-3.0/index.html.
DrawingshapesOurdisplay()functionperformstheverycommontasksofamaingameloop.
Itfirstclearsthescreen,thensetsupaviewingtransformation(wewillseewhatthismeansafterthesnippet),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,thisclearsthecoloranddepthbuffers.glLoadIdentity():Thisloadstheidentitymatrixasthecurrentmatrix.Theidentitymatrixisa4x4matrixwithonesinthemaindiagonalandzeroseverywhereelse.Thismakesthestackmatrixstartoverattheorigin,whichisusefulifyouhavepreviouslyappliedsomematrixtransformations.gluLookAt():Thiscreatesaviewingmatrix.Thefirstthreeparametersarethex,y,andzcoordinatesoftheeyepoint.Thenextthreeparametersarethex,y,andzcoordinatesofthereferencepoint,thatis,thepositionthecameraislookingat.Finally,thelastthreeparametersspecifythedirectionoftheupvector(usually,itis0,1,0).glLightfv():Thissetstheparametersoflightsource0(GL_LIGHT0).Thefollowingparametersarespecifiedinourexample:
GL_POSITION:Thisdefinesthepositionofthelight
GL_DIFFUSE:ThissetstheRGBAintensityofthelightGL_CONSTANT_ATTENUATION:ThisspecifiestheconstantattenuationfactorGL_LINEAR_ATTENUATION:Thisspecifiesthelinearattenuationfactor
Oncethelightingattributesareset,wecanstartrenderingbasicshapeswithGLUT.Ifwedrawtheobjectsfirst,lightingwillnotbeappliedcorrectly:
glPushMatrix():Thispushesanewmatrixintothecurrentmatrixstack,identicaltotheonebelowit.Whilewedothis,wecanapplytransformationssuchasglTranslateandglRotate,tothismatrix.Wewillrenderourfirstsphereattheorigin,butthesecondonewillbetransformedwithglTranslate.glTranslate():Thismultipliesthecurrentmatrixbythetranslationmatrix.Inourexample,thetranslationvaluesforthesecondsphereare4forthexaxis,and2fortheyaxis.glMaterialfv():Thissetsthematerialparametersofthefrontface,asitiscalledwithGL_FONT.WithGL_DIFFUSE,wespecifythatwearesettingtheRGBAreflectanceofthematerial.glutSolidSphere():ThroughGLUT,thisroutineallowsustoeasilydrawasolidsphere.Itreceivesthesphere’sradiusasthefirstargument,andthenumberofslicesandstacksintowhichthespherewillbesubdivided.Thegreaterthesevaluesare,therounderthespherewillbe.glPopMatrix():Thispopsthecurrentmatrixfromthestack.Ifwedidnotdothis,eachnewobjectrenderedwouldbeachildofthepreviousone.
Finally,weswitchthebufferswithglutSwapBuffers().Ifdoublebufferingwasnotenabled,weshouldcallthesinglebufferequivalent—glFlush().
RunningthedemoAsusual,wecheckwhetherthemoduleisthemainscriptforstartingtheapplication:
if__name__=='__main__':
app=App()
app.start()
Ifyourunthecompleteapplication,theresultwilllooklikewhatisshowninthefollowingscreenshot:
RefactoringourOpenGLprogramAsyoumayhaveseen,thisexampleusesenoughOpenGLcallstogrowoutofcontrolifwedonotstructureourcode.That’swhywearegoingtoapplysomeobject-orientedprinciplestoachieveabetterorganization,withoutmodifyingtheorderofthecallsorlosinganyfunctionality.
ThefirststepwillbetodefineaLightclass.Itwillholdtheattributesneededtorenderthelight:
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:changingthecolorofthelight.Wesetthecurrentcolorindexto0,andwewilliterateoverthedifferentcolorsdefinedinLight.colorseachtimetheswitch_color()methodiscalled.
Therender()methodrespectstheoriginalimplementationoflightingfromournon-refactoredversion:
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()methodandaclassattribute:
defenable(self):
ifnotLight.enabled:
glEnable(GL_LIGHTING)
Light.enabled=True
glEnable(self.light_id)
AnotherimprovementisthecreationofaSphereclass.Thisclasswillallowustocustomizetheradius,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,wecanadaptourAppclassandcreatetheinstancesthatwewillrenderinthemainloop:
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,weneedtoenableOpenGLlightingthroughthelight.enable()method:
defstart(self):
#...
glEnable(GL_DEPTH_TEST)
self.light.enable()
#...
Nowthedisplay()methodbecomessuccinctandexpressive,sincetheapplicationdelegatestheOpenGLcallstotheobjectinstances:
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.Itallowstheplayertorotatethecameraaroundthespheresandmoveforwardorawayfromthecenterofthescene.
ProcessingtheuserinputAswesawearlier,glutSpecialFunctakesacallbackfunctionthatreceivesthepressedkeyasthefirstargument,andthexandycoordinatesofthemousewhenthekeywaspressed.
Wewillusetherightandleftarrowkeystomovearoundthespheres,andtheupanddownarrowkeystoapproximateormoveawayfromthespheres.Besidesallofthis,thecolorofthelightwillchangeifwepressF1,andtheapplicationwillbeclosediftheInsertkeyispressed.
Todoso,wewillcheckthevaluesofthekeyargumentwiththerespectiveGLUTconstants:
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,soitsvalueisalwaysbetween10and20,andself.angleisalsoalwaysbetween0and2π.Tonotifythatthecurrentwindowneedstoberedisplayed,wecallglutPostRedisplay().
YoucancheckouttheChapter5_02.pyscript,whichcontainsthisrefactoredversionofourapplication.
Whenyourunit,pressthearrowkeystorotatearoundthespheresandF1toseehowthelightingaffectsthespheres’materials.
AddingthePygamelibraryWithGLUT,wecanwriteOpenGLprogramsquickly,primarilybecauseitwasaimedtoprovideroutinesthatmakelearningOpenGLeasier.However,theGLUTAPIwasdiscontinuedin1998.Nonetheless,therearesomepopularsubstitutesinthePythonecosystem.
Pygameisoneofthesealternatives,andwewillseethatitcanbeseamlesslyintegratedwithOpenGL,evensimplifyingtheresultingcodeforthesameprogram.
Pygame101BeforeweintegratePygameintoourOpenGLprogram,wewillwriteasample2DapplicationtogetstartedwithPygame.
WewillimportPygameanditslocalsmodule,whichincludestheconstantsthatwewillneedinourapplication:
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,sowewilldefinetheattributewithoutaddingb.Anotherchangeisthenumberofframespersecond(FPS),whichwewilllaterfindouthowtocontrolviaPygame’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(),andthenwecreateascreenwithagivenwidthandheight.TheDOUBLEBUFflagispassedsoastoenabledoublebuffering,whichhasthebenefitswementionedpreviously.
Themaineventloopisimplementedwithawhileblock,andwiththeClockinstance,wecancontroltheframerateandcalculatetheelapsedtimebetweenframes.Thisvaluewill
bemultipliedbythespeedofmovement,sothecirclewillmoveatthesamespeediftheFPSvaluechanges.
Withpygame.event.get(),weretrievetheeventqueue,andifaQUITeventoccurs,thewindowisclosedandtheapplicationfinishesitsexecution.
Thepygame.key.get_pressed()returnsalistwiththepressedkeys,andwiththekeyconstants,wecancheckwhetherthearrowkeysarepressed.Ifso,thecircle’spositionisupdatedanditisdrawnonthenewcoordinates.
Finally,pygame.display.flip()updatesthescreen’ssurface.
TheChapter5_03.pyscriptcontainsthefullcodeofthisexample.
TipThePygamedocumentation
SincePygameisdividedintoseveralmodules,eachonewithvariousfunctions,classes,andconstants,theofficialdocumentationisausefulreference.
Weareusingsomefunctionsfromthekeymodule;youcanfindfurtherinformationaboutitathttps://www.pygame.org/docs/ref/key.html.Thesameappliesforthedisplayandtimemodules.
PygameintegrationLet’sseehowitispossibletoimplementthesamefunctionalitywithPygame.ThefirststepistoreplacetheOpenGL.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,andtheyarereplacedbythePygameinitialization.ApartfromDOUBLEBUF,wewilladdtheOPENGLflagtocreateanOpenGLcontext:
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()methodupdatesthesceneandtheinstanceattributesbyretrievingtheeventsfromtheeventqueueandprocessingthepressedkeys.
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()
AnotherconsequenceofremovingGLUTisthatwecannotuseglutSolidSpheretorenderourspheres.
Fortunately,wecansubstituteitwiththegluSphereGLUfunction.TheonlydifferenceisthatweneedtocreateaGLUquadraticobjectfirst,andthencallthefunctionwiththisargumentandtheusualradius,numberofslices,andnumberofstacksintowhichthesphereisdivided:
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.Checkoutthechapter5_04.pyscripttoseethecompleteimplementation.
TipOpenGLandSDL
ByincludingPygame,wereplacetheGLUTAPIwithSimpleDirectMediaLayer(SDL),whichisthelibrarythatPygameisbuiltover.Likefreeglut,itisanothercross-platformalternativetoGLUT.
DrawingwithOpenGLUntilnow,wehavealwaysrenderedourobjectswithautilityroutine,butmostOpenGLapplicationsrequiretheuseofsomedrawingprimitives.
TheCubeclassWewilldefineanewclasstorendercubes,andwewillusethefollowingrepresentationtobetterunderstandthevertices’positions.From0to7,theverticesareenumeratedandrepresentedina3Dspace.
Thesidesofacubecannowberepresentedastuples:thebackfaceis(0,1,2,3),therightfaceis(4,5,1,0),andsoon.
Notethatwearrangetheverticesincounterclockwiseorder.Aswewilllearnlater,thiswillhelpusenableanoptimizationcalledfaceculling,whichconsistsofdrawingonlythevisiblefacesofapolygon:
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,aswellasthevertexcoordinateswithrespecttothecenterpositionofthecube:
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.ThisfunctionisexecutedbetweentheglBegin()andglEnd()calls.Theydelimittheverticesthatdefineaprimitive.TheGL_QUADSmodetreatseachgroupoffourverticesasanindependentquadrilateral.
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()
EnablingfacecullingEventhoughacubehassixfaces,wecanseeamaximumofonlythreefacesatonce,andonlytwooronefromcertainangles.Therefore,ifwediscardthefacesthatarenotgoingtobevisible,wecanavoidrenderingatleast50percentofthefacesofourcubes.
Byenablingfaceculling,OpenGLcheckswhichfacesarefacingthevieweranddiscardsthefacesthatarefacingbackwards.Theonlyrequirementistodrawthefacesofthecubeinthecounterclockwiseorderofthevertices,whichisthedefaultfrontfaceinOpenGL.Theimplementationpartiseasy;weaddthefollowinglinetoourglEnablecalls:
#...
glEnable(GL_LIGHTING)
glEnable(GL_CULL_FACE)
#...
Inournextapplication,wewilladdsomecubesandenablefacecullingtoseethisoptimizationinpractice.
BasiccollisiondetectiongameWithalloftheseingredients,youarenowabletowriteasimplegamethatdetectssimplecollisionsbetweenshapes.
Thesceneconsistsofaninfinitelane.Blocksappearrandomlyattheendofthelaneandmovetowardstheplayer,representedasthesphereinthefollowingscreenshot.Heorshemustavoidhittingtheblocksbymovingthespherefromrighttoleftinthehorizontalaxis.
Thegameisoverwhentheplayer’scharactercollideswithoneoftheblocks.
Thisgameplayisdirectanduncomplicated,anditwillallowustodevelopa3Dgamewithoutworryingtoomuchaboutmorecomplicatedphysicscalculations.
SincewearegoingtoreusetheLight,Cube,andSphereclasses,weneedtodefineanewclassonlytorepresentourgameblocks:
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()methodsimplymovestheblocktowardstheplayerbyupdatingitszcoordinatewithuniformspeed.
OurAppclasssetstheinitialvaluesoftheattributesthatwewillneedduringtheexecutionofourgame,andcreatestheLightandthegameobjectinstancesasinourpreviousexamples:
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),aswementionedpreviously:
defstart(self):
pygame.init()
#...
glMatrixMode(GL_MODELVIEW)
glEnable(GL_CULL_FACE)
self.main_loop()
Themain_loop()methodisnowaseparatemethodandincludestherandomgenerationofblocks,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)
Wewillimplementcollisiondetectionbycomparingtheboundariesoftheclosestblockswiththeextremesofthesphere.Sincethesphere’swidthissmallerthantheblocksize,ifoneoftheseextremesisbetweentherightandleftboundariesofablock,itwillbeconsideredasacollision:
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.Itaccumulatestheelapsedtimeinmillisecondsbetweenframes,anditwilltrytospawnanewblockonlyifthesumisgreaterthan800milliseconds:
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,anewblockisaddedtotheblocklistandtherandom_dtcounterisresetto0.Inthisway,theminimumelapsedtimebetweentwoblockscanbe0.8seconds,givingenoughtimetoleaveatolerabledistancefromoneblocktoanother.
Anotheroperationthatthemainloopperformsisremovingtheblocksthatarelocatedbehindthecameras’viewingarea,avoidingthecreationoftoomanyBlockinstances:
defclear_past_blocks(self):
blocks=filter(lambdax:x.position[2]>5,
self.blocks)
forblockinblocks:
self.blocks.remove(block)
delblock
Thecodefordisplayingthegameobjectsstaysassuccinctasusual,thankstothetransferofthedrawingprimitivestotherespectiverender()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.Thischangeisstraightforward,sinceweonlyneedtoupdatethexcomponentofthecharacter’spositionandtrimitsothatitcannotmoveoutofthelane:
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.Runitandfeelfreetomodifyandimproveit!Youcanaddpickupitemsandkeeptrackofthescore,orgivetheplayeranumberoflivesbeforethegameisover.
SummaryInthischapter,youlearnedhowitispossibletoworkwithPythonandOpenGL,andwithbasicknowledgeaboutOpenGLAPIs,wewereabletodevelopasimple3Dgame.
Wesawtwocross-platformalternativesforcreatinganOpenGLcontext:GLUTandPygame.Youcandecidewhichonebettersuitsyour3Dgames,dependingonthetrade-offsofeachoption.Keepthisinmind:anadvantageofusingbothisthatyoumayadaptexistingexamplesfromonelibrarytotheother!
Withthesefoundationsof3Dcovered,inthenextchapter,wewillseehowtodevelopa3Dplatformerbasedonthesetechnologies.
Chapter6.PyPlatformerSofar,youhavelearnedhowtoimplementgamesinPythonwithdifferentlibraries,withtheprimaryfocusbeingonthepracticeofgamedevelopment.
Now,wewillintroducesometheoreticalconceptsthatwillnotonlycomplementthepractice,butwillalsohelpusdevelopgamesefficiently.Thistheorywillaidusinunderstandingwhygamesareconceptuallydesignedthewaytheyare.
Inthischapter,wewillcoverthesetopics:
FoundationsofthegametheoryObject-orientedprinciplesappliedtogamedevelopmentHowtoimplementasmall3DgameframeworkModularizingourfunctionalitywithreusablecomponentsAddingaphysicsenginetosimulaterigidbodies’interactionsCreatingthebuildingblocksofaplatformergame
AnintroductiontogamedesignThereareseveralacademicdefinitionsofwhatagameis;however,mostofthemsharethekeyterms,suchasrules,objectives,andplayers.Assumingthatallgamessharetheseconcepts,wemayasksomeinterestingquestionswhileanalyzingagame:whatisthegame’smainobjective?Whataretherulesthattheplayermustfollow?Isitdifficulttorecognizethegoalandtherulesofthesystem?
Otherdefinitionsmakereferencestoconceptssuchasresourcemanagementandinefficiencies,becausethedecisionsoftheplayerareusuallyconditionedbythelimitationsofsomeusefultokensinthegame.
Thedecisionswemakewhenwecreateagamearedeeplyrelatedwiththeseconcepts,andaswewillseelater,itisagoodexercisetothinkaboutthemevenwhenwearestartingwiththedevelopment.
LeveldesignInordertosuccessfullyengageourplayers,ourgameneedstograduallyaddnewchallengesthatpreservetheirinterest.However,theseingredientsshouldbeintroducedinacoherentordersothattheplayerdoesnotbecomeconfusedbecausesheorhedoesnotknowhowtoreacttothegame’soutput.
Thismeansthattheplayerisnotonlyplayingyourgamebutalsolearninghowtoplayit.Thislearningcurveshouldbecarefullyconsideredintutorialsandthefirstlevel,becauseincorrectguidancecanleadtofrustration.
PlatformerskillsInourplatformergame,thefirstactionthattheplayerwilllearnishowtomovetheircharacter,whichisintuitivelyperformedbypressingthearrowkeys.Sincethereisnotanythreatnearby,theplayercanexperimentmovingaroundandcanbecomefamiliarwiththecontrolkeys.
Next,theplayerwillfaceanobstacle,andsheorheneedstolearnhowtojumpoverittocontinue.Thereisnogapbetweentheobstacleandtheground,sothereisnoriskoffallingfromtheplatformifthecharacter’sjumpistooshort,asshowninthefollowingscreenshot:
Oncethefirstobstacleiscleared,theplayerfacesacoupleofplatformswithgapsinbetween,asshowninthenextscreenshot.Nowitisrequiredtomeasurethejumpingforces,orelsethecharacterwillfallintothevoid.
Ifthecharacterdoesnotreachtheplatformandfalls,itwillrespawnattheinitialposition.That’sanotherlessonthattheplayerwilllearn:movewithcare,orelseyouwillhavetostartoveragain!
Onthenextplatform,theplayerwillencounteranewcharacter.Ifthecharactercollideswithit,itwillmovebacktothespawnposition.Therefore,theplayerlearnsthattheseobjectsshouldbeavoidedandtheymustjumpoverittoadvance.
Afterthis,theplayercanreachaplatformonwhichthereisaspinningbox.Itisplacedinthemiddleofanarrowplatform,soitisveryeasytocollidewith,asshowninthisscreenshot:
Whenthishappens,theplayer’sammoisincreased,sonowthecharacterisabletoshootbypressingthespacebar.Onthenextplatform,theplayercanfindanotherenemytotesttheirshootingability.Withthepickup,theplayercanshootuptofivetimes,sotheycantryoutthisabilityseveraltimesbeforerunningoutofammo.
Nowthatwehaveanalyzedtheskillsthatourplayerswillneedtolearn,wecanmoveontothearchitecturaldesignofourgame.
Component-basedgameenginesWhenwestartdevelopingagamefromscratch,thefirststepmaybetodefineabasicclasswithcommonattributesforallthegameobjects,suchasitspositioncoordinates,thecolor,thespeed,andsoon.ThiscouldbethebasicGameObjectorActorclasswedeclaredinpreviouschapters.
Thenyouneedtoaddothergameobjectswithmoreconcretekindsofbehavior,suchasthecharacterthatwillbecontrolledbytheplayer,ortheenemiesthatrandomlyshoottheplayablecharacter.Ifyourepresenttheseentitieswithseparateclasses,eachoneimplementsthatfunctionalitywithacustommethodinthecorrespondingclass.
Consequently,everyspecializationofanexistingentitymightbetranslatedintoanewsubclass.Supposewewanttoaddaspecialtypeofenemythatmovesalongaparticularroute,whichwewillcallPatrolEnemy.Inourclassdiagramshownhere,thisclasswillextendourEnemyclass:
Whilethisapproachmightworkforsmallgames,itbecomesmoredifficulttoscaletheorganizationoftheprojectwhentheinheritancehierarchygrows.Imaginethatwewanttoaddanothertypeofenemythatshootsonlywhenitislocatedwithinacertaindistancefromtheplayer,andathirdenemythatcombinesthisnewbehaviorwithPatrolEnemy.Thisisillustratedinthefollowingdiagram:
Ifsomegameobjectsfollowthebehavioroftheirancestorsandsharethefunctionalitywithotherentities,youmayneedtousemultipleinheritanceordefineintermediateclassesthatwouldbeunnecessaryotherwise.
Ontheotherhand,acomponent-baseddesignfollowstheprincipleoffavoringobjectcompositionoverclassinheritance.Thismeansthatthefunctionalityiscontainedinotherclasses,insteadofreusingitwithasubclass.
Translatedintoourexample,thisarchitectureleavesuswiththefollowingdiagram.Besides,aswewillseelater,acomponentbaseclassisalsoadded.
TheclassesundertheGameObjectsareaareGameObjectsubclasses,buttheirbehaviorisdeterminedbythecompositionoftheirComponentssubclasses.
Inthisway,wedonotintroducemultipleinheritancesolutionsandkeepthesepiecesoffunctionalityseparatedinsmallclasses,withtheadvantageofbeingabletoaddorremove
themdynamically.
Inlatersections,wewillseehowthesecomponentsarealsousedtorenderobjectsinourOpenGLcontext,butnowwewillmoveontothelibrarythatwewillusetosimulatephysicsinourgame.
IntroducingPymunkWewillimplementphysicsinourgamewithPymunk,a2DphysicsenginebuiltontopofChimpunk2D.
EventhoughweareusingOpenGLfor2Dgraphics,ourplatformergamewillrecreateatwo-dimensionalspace,sowewillworkontheplaneinwhichthezaxisequals0.
YoucaninstallPymunkdirectlyfrompip:
$pipinstallpymunk
TheChimpunk2DlibraryiswritteninC,soyoumightneedtocompileitonyourplatformifthedistributiondoesnotshipwiththeprecompiledlibrary.On32-bitWindowsand32-bitand64-bitLinuxversions,PymunkwillincludetheChimpunk2Dbinaries.
Forotherplatforms,orifyouwanttocompilethelibraryyourself,youcancheckouttheinstallationguideandthestepsforcompilingChimpunk2Dathttp://pymunk.readthedocs.org/en/latest/installation.html.
TipDon’treinventthewheel!
Implementingarigidbodylibraryisalaborioustask,especiallyifyouaredevelopingacasualgamefromscratch—aswearedoinginthischapter.
Fortunately,thePymunkAPIissimpleandwell-documented,soyoucangetstartedquicklyandavoidlosingtimecraftingacustomphysicsengine.
Themainclassesofthepymunkpackagearethefollowing:
Space:Atwo-dimensionalspaceinwhichthephysicssimulationwilloccur.WewilluseaSpaceinstancefortheentiregame,andwewillupdateeachframethroughitsstep()method.Body:Thisrepresentstherigidbody,thebasicunitofsimulation.Ithasapositionmember,whichwillalsorepresentthepositionofthegameobjectthatcontainstherigidbody.Theothermembersthatwewillcoverarethevelocityandforcevectors.Shape:Thebaseclassforallshapes.Wewilllimitourselvestocircleandboxshapes,definedbytheCircleandPolyclassesrespectively.Arbiter:Thisrepresentsacollisionpairbetweenshapesandisusedincollisioncallbacks.Italsocontainsinformationaboutthecontactpointsofthecollision.
Inthenextsection,wewillrelyontheseclassesandtheirattributestoholdinformationaboutgameobjectsandtheircollisions.
BuildingagameframeworkTheComponentandGameObjectclassesformthecoreofourgameengine.TheinterfaceofferedbytheComponentclassisaseasyasthis:
classComponent(object):
__slots__=['gameobject']
defstart(self):
pass
defupdate(self,dt):
pass
defstop(self):
pass
defon_collide(self,other,contacts):
pass
Thesemethodsdefinethelifecycleofourcomponents:
start():Thisiscalledwhenthecomponentisaddedtothegameobjectinstance.Thecomponentkeepsareferencetothegameobjectasself.gameobject.update(dt):Thisiscalledforeachframe,wherethedtargumentistheelapsedtimeinsecondssincethepreviousframe.on_collide(other,contacts):Thisiscalledwhenthecomponent’sgameobjectcollideswithanotherrigidbody.Theotherargumentistheothergameobjectofthecollisionpair,andcontactsincludesthecontactpointsbetweentheobjects.ThislastargumentisdirectlypassedfromthePymunkAPI,andwewillseelaterhowtoworkwithit.stop():Thisiscalledwhenthecomponentisremovedfromthegameobjectinstance.
ThesewillbeinvokedfromourGameObjectclass.Aswementionedearlier,agameobjectinternallyusesarigidbodytorepresentitspositioninthexandyaxesandanextravariableforthezaxis.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,andtherotationisextendedwiththeangleofrotationinthexandyaxiswiththe_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’spositioncanbemodifiedbyapplyinganimpulseoraforcevectortotheunderlyingrigidbody:
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—inaflexiblewaywiththefollowingmethods:
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.TheinterfaceofthisComponentsubclassincludearender()method,whichiscalledinthedisplayloopforeachactivegameobject.
Theupdate()methoddirectlydelegatestheexecutiontoeveryattachedcomponent:
defrender(self):
forcomponentinself.components:
ifisinstance(component,Renderable):
component.render()
defupdate(self,dt):
forcomponentinself.components:
component.update(dt)
Finally,whenweremoveagameobject,weneedtodeletetheshapeofitsrigidbodyfromthePymunkspace;otherwise,thevisualresultwillbesuchthattheentitiesstillkeepcollidingwithaninvisibleobject:
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)
Youcanfindtheimplementationofthesetwoclassesinthepyplatformer/enginecomponents.pyandpyplatformer/engine/__init__.pyscripts.
TipSavingspaceconsumption
Bydefault,eachobjectinstancehasaninternaldictionaryforattributestorage.The__slots__classvariabletellsPythontoallocatespaceonlyforeachvariable,savingspaceiftheinstancehasfewattributes.
Sincecomponentsaresmallclasses,weaddedthisoptimizationtoavoidinstantiatingunnecessarydictionaries.
Nowwewillmoveontothephysicsmodule,wheretheInputclassandtheshapecomponentsaredefined.
AddingphysicsThismoduleisresponsibleforinitializingthePymunkSpace.Thishappensatthebeginningofthefile,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)
ThecollisionhandlerreceivesthespacewherethecollisionoccursasthefirstparameterandanArbiterinstancewiththecollisioninformation.Here,weobtainthegameobjectattachedtotheshapeandtriggerthecollide()method.
ThePhysicsclassissimilartoInput,sinceitcontainsacoupleofclassmethodsthatwrapthebasicfunctionalitywearelookingfor:
classPhysics(object):
@classmethod
defstep(cls,dt):
space.step(dt)
@classmethod
defremove(cls,body):
space.remove(body)
TheRigidbodyclassisabasecomponentthatreplacestheinitialstaticbodythateverygameobjecthaswithanon-staticone.Italsoaddsareferencetothegameobjectfromtheshape,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)
NoteNotethatstaticbodiesarenotaddedtothespace;onlytheirshapesare.
TheBoxColliderandSphereCollidersubclassescreatethecorrespondingshapebycallingthePymunkAPI:
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,butyoucaneasilyaddanotherrigidbodycomponentbyfollowingthesamepattern.
RenderablecomponentsThecomponentsmoduleincludesthedefinitionoftheRenderableclass,whichwementionedearlierwhenwelookedathowtodisplaygameobjects.
Itpushesamatrixtothecurrentstackandperformssomebasicoperationsbeforedelegatingtherenderingtothe_render()method,savingusfromrepeatingthisboilerplatecode:
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
TheCubeandSpheresubclassesareanadaptationoftheimplementationwecoveredinChapter5,PyGameand3D,andalongwiththeLightclass,theyhavebeenomittedforbrevity.
TheCameracomponentYoumayrememberhowtosetupthecameraperspectivewithGLU,whichwasoneofthefirstactionsofthedisplayloopinthepreviouschapter.
Nowwewilluseacomponenttoperformthesametask,butitwilldifferfromothercomponentsinthewayinwhichitiscalledinthemainloop.ThishappensbecausethesematrixoperationsneedtobethefirstonesoftheOpenGLmatrixstackifwewanttosetupthecamerapositioncorrectly:
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)
Thecamerawilllookatthegameobjectwithwhichitisattachedandthedistancealongtheyandzaxesareparameterizedinits__init__method.
Thisimplementationtakesthelastcamerathathasbeeninstantiatedasthecurrentone.Inourgame,wewillonlyuseasinglecameracomponent,andthiscanbeconsideredacommonscenarioinbasicgames.
However,ifyouneedtoswitchbetweenmultiplecameras,youcandefineamorecomplexcomponent.Asyoumaynotice,theseimplementationsareverycleanandconcisethankstotheassumptionofthesesimplifications.
TheInputManagermoduleTohandleinputevents,wecreateaseparatemodulethatwillwraptheprocessingofourpygameevents.SincewewillregisterkeystrokesandthespecialQUITeventonly,thisclassisquiteuncomplicated:
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,itisstraightforwardtogetthepressedkeyswithinanycomponent,likethisforinstance:
classHorizontalMovement(Component):
defupdate(self,dt):
direction=Input.get(K_RIGHT)-Input.get(K_RIGHT)
self.gameobject.move(dt*direction*5,0)
Wecansubtractbooleanvaluesbecauseboolisasubclassofint,andTruecorrespondsto1,whileFalsecorrespondsto0.
TheGameclassTheGameclassisresponsibleforbootstrappingtheapplication—settingtheinitialvaluesofthewindowattributes,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,somainloop()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,andthankstothemicro-frameworkthatwehavedeveloped,itwillconsistofadozenshortclassesonly.
DevelopingPyPlatformerWiththisarchitecturalbackground,wecanstartcraftingthecustomcomponentsofourplatformergame.TheComponentAPImaylooksimple,butitallowsustoimplementthefunctionalityinasuccinctmanner.
CreatingtheplatformsEachplatformofourgameisnothingbutacubewithastaticrigidbody.However,sincewewillcreateseveralinstanceswiththesecomponents,itisconvenienttodefineaclasstoavoidrepeatingthiskindofinstantiation:
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))
AddingpickupsInplatformergames,itiscommonthattheplayerisabletocollectsomeitemsthatgivevaluableresources.Inourgame,whenthecharacterhitsoneofthesepickups,itsammoisincrementedbyfiveunits.
Todecoratethistypeofgameobject,wewilladdacomponentthatrotatestheattachedgameobjectarounditsyaxis:
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,wecandefineaGameObjectsubclassthatwrapstheinstantiationofthesecomponents:
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,ifweneedtocheckthegameobject’stype,wedon’tneedtorelyontheisinstancefunctionandthehierarchymodel.
Shooting!Whentheplayercollideswithoneofthesepickups,itisdestroyedanditisnolongerdisplayedonthescene.Withaphysicsengine,thisalsomeansremovingthegameobjectanddisablingitsrigidbody.RememberthatwedefinedtheGameObject.remove()methodforthispurpose.
Tosimulatethegradualdisappearanceoftheobject,wewilldecreaseitsscaleuntilitdoesnotbecomevisible,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.TheabilitytoshootandcollecttheseitemsisdefinedintheShootercomponent:
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,agameobjectisinstantiatedandmovestowardsthecurrentgameobject’sdirection.IthasanauxiliaryShootcomponent.Thiscomponentremovestheshootinstancewhenitcollideswithotherrigidbodies.
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))
ThePlayerclassanditscomponentsApartfromtherigidbodyandtheShooter,ourplayercharacterhastwomaincomponents:
Respawn:Thisspawnstheplayerinitsinitialpositionifitfallsintothevoidorcollideswithanenemy.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:Thisqueriestheinputstateandcheckswhatforcescanbeappliedtothecharacter’srigidbodytomoveithorizontallyormakeitjump.HereisanexampleofthePlayerMovementclass:
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)
NoteNotehowweusethelistofcontactpointstocheckwhethertheplayerhastouchedtheground,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))
ThePyPlatformerclassFinally,wedefineaGamesubclassthatinstantiatesallofourgameobjectsandplacestheminthescene:
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.pyscript.
SummaryInthischapter,youlearnedaboutthebenefitsofacomponent-baseddesignandhowitallowsyoutobuildsmallpiecesthatcanbeadded,removed,andcombinedwithseveralgameobjects.
Notethatthemostimportantexerciseofthischapterisnothowtoimplementaplatformergame,buthowitispossibletosetupacomponent-basedframeworkandprovidethebasicbuildingblocksofagame.
Withthesefoundations,youcanaddsomebasicfunctionality,suchasmovingtheenemiesandtheplatforms,displayingmoreinformationabouttheammo,orkeepingtrackofascorebasedonthenumberofliveslostandenemiesdestroyed.
Asusual,thefinalversionofthischapter’sgameisnothingbutthestartingpointofamorecomplexapplication!
Inthenextchapter,wewillinteractwithareal-wordcheckersgameviaawebcam.ThisapplicationwillbedevelopedwithOpenCV,across-platformcomputervisionlibrary.
Chapter7.AugmentingaBoardGamewithComputerVisionComputervisionisthescienceandengineeringofsmartcamerasystems(ormorebroadly,smartimagesystems,sinceimagescancomefromanothersourcebesidesacamera).Examplesofsubtopicsincomputervisionincludefacerecognition,licenseplaterecognition,imageclassification(asusedinGoogle’sSearchbyimage),motioncapture(asusedinXboxKinectgames),and3Dscanning.
Computervision,likegamedevelopment,hasbecomemoreaccessibleinrecentyearsandisnowalmostaubiquitoustopic.“Howcanweleveragepeople’sinterestincameras?”or“Howcanweleverageallthecamerasthatareinourbuilding,ourcity,orourcountry?”isasnaturalaquestionas“Howcanweleveragepeople’sinterestingamesandsimulations?”andtheimplicationsextendbeyondentertainment.
Camerasareeverywhere.Manyofthemareattachedtopowerfulhostcomputersandnetworks.Welive,work,andplayamidstanarmyofdigitaleyes,includingwebcams,cameraphones,camera-controlledgameconsolesandsmartTVs,securitycameras,drones,andsatellites.Imagesofyoumaybecaptured,processed,ortransmittedmanytimesdaily.
ThesongwriterJohnLennonaskedusto“imagineallthepeoplelivingfortoday”but,foramoment,let’simagineallpixelsinstead.Asingleimagemaycontainmillionsofpixels,amountingtomorebytesofdatathanLeoTolstoy’sWarandPeace(anepic1500-pagenovelaboutRussiansocietyduringtheNapoleonicWars).Asinglecameramaycaptureavideostreamcontainingthousandsoftheseepic-sizedimagesperminute.Billionsofcamerasareactiveintheworld.Networksanddiskdrivesarecongestedwiththeaccumulationofimagedata,andonceanimageisonline,copiesofitmayremainincirculationindefinitely.Whenweusesoftwaretoacquire,edit,analyze,andreviewstreamsofimages,wemaynoticethatthecomputer’sCPUusagesoarswhileitsbatterypowerplummets.
Asgamedevelopers,weknowthatagoodgameenginesimplifiesalotofoptimizationproblems,suchasbatchingspritesor3DmodelstosendtotheGPUforrendering.Goodcomputervisionlibraries(andmorebroadly,goodnumericandscientificlibraries)alsosimplifyalotofoptimizationproblemsandhelpusconserveCPUusageandbatterylifewhileprocessingvideoinput(orotherlargestreamsofdata)inrealtime.Sincethe1990s,computervisionlibraries,likegameengines,havebecomemorenumerous,faster,easiertouse,andoftenfree.Thischapterleveragesthefollowinglibrariestocapture,process,anddisplayimages:
NumPy:Thisisanumericlibrary,whichwepreviouslyusedinChapter4,SteeringBehaviors.Itsdocumentationisathttp://docs.scipy.org/doc/.OpenCV:Thisisacross-platformlibraryforcomputationalphotography,computervision,andmachinelearning.OpenCV’sPythonversionrepresentsimagesasNumPyarrays.However,beneaththePythonlayer,OpenCVisimplementedinC++code
withawiderangeofhardware-specificoptimizations.Thankstotheseoptimizations,OpenCVfunctionstendtorunfasterthantheirNumPyequivalents.TheOpenCVdocumentationisathttp://docs.opencv.org/.scikit-learn:Thisisamachinelearninglibrary.ItprocessesNumPyarrays.Foritsdocumentation,refertohttp://scikit-learn.org/stable/documentation.html.WxPython:Thisisacross-platformGUIframework.Oneachplatform,WxPythoniscertaintohaveanativelookandfeelbecauseitusesnativeGUIwidgets(incomparison,manycross-platformGUIframeworks,suchasTkinter,usetheirownnon-nativewidgetswithaconfigurable“skin”thatmayemulateanativelook).WxPythonisawrapperaroundaC++librarycalledWxWidgets.ForWxPython’sdocumentation,refertohttp://wxpython.org/onlinedocs.php.
NoteLaterinthischapter,intheSettingupOpenCVandotherdependenciessection,wewilldiscusstheversionrequirementsandsetupstepsforeachlibrary.
Followingthepatternofotherchaptersinthisbook,wewillapplycomputervisiontoaclassicgame:checkers,alsoknownasdraughts.Thisboardgamehasmanyvariantsfromallculturesandallperiodsofhistoryinthepast5,000years.Itisthegrandfatherofallstrategygames.Acrossmostvariants,thegamehasthefollowingfeatures:
Therearetwoplayers,whoaresometimescalledlightanddark.Therearetwokindsofplayingpieces,sometimescalledpawnsandkings.Apawnisashort,circularplayingpiece.Akingisatall,circularplayingpiecemadebystackingtwopawns.Theboardisagridofalternatinglightanddarksquares.Piecesmayonlyoccupythedarksquares.Thesizeofthegriddependsonthevariantofthegame.Atthestartofthegame,eachplayerhasanarmyofpawns,occupyingmultipleadjacentrowsononesideoftheboard.Thetwoarmiesstartonoppositesidesoftheboardwithtwovacantrowsbetweenthem.Apiecemaycaptureanopposingpiecebyjumpingoveritintoanunoccupiedsquare.Apiecemaymakemultiplejumpsinoneturn.Onreachingthefarthestrow,apawnispromotedtoaking.Thedifferencebetweenapawnandakingdependsonthevariant.Insomevariants,onlyakingcanmovebacktowarditsstartingside.Moreover,insomevariants,apawncancrossonlyoneunoccupiedsquareatatimewhileakingcancrossmultipleunoccupiedsquares.Thelatterarecalledflyingkings.
NoteFordescriptionsofmanyinternationalvariantsofcheckers,ordraughts,refertohttps://en.wikipedia.org/wiki/Draughts.
Wewillbuildanapplicationthatmonitorsareal-worldgameofcheckersviaawebcam.Theapplicationwilldetectacheckerboardandclassifyeachsquareasanemptysquare,alightpawn,alightking,adarkpawn,oradarkking.Furthermore,theapplicationwillalterthewebcam’simagestocreateabird’s-eyeviewoftheboard,withlabelstoshowthe
resultsoftheclassification.Thisisasimplecaseofaugmentedreality,meaningthattheapplicationappliesspecialeffectsandannotationstoareal-timeviewofareal-worldobject.Wewillcallthisapplication,quitesimply,Checkers.
NoteThecompletedprojectforthischaptercanbedownloadedfromhttp://nummist.com/opencv/4507_07.zip,andanyFAQanderratacanbefoundathttp://nummist.com/opencv.
PlanningtheCheckersapplicationLet’sgivefurtherthoughttothereal-worldscenethatourCheckersprogramwillexpectandthevirtualsceneitwillcreate.Considerthefollowingclose-upphotograph,showingpartofan8x8checkerboard.Fromthetop-leftcornertoright-handside,weseealightking,alightpawn,adarkpawn,andadarkking.
Thischeckerboardisjustasheetofmattepaperonwhichblackandwhitesquaresareprinted(thepaperisgluedtoafoamboardtomakeitrigid).Theplayingpieceshappentobepokerchips—redchipsforthedarksideandgraychipsforthelightside.Astackoftwopokerchipsisapawn,whileastackoffourisaking.Asthisexamplesuggests,peoplemayplaycheckerswithhomemadeorimprovisedsets.Thereisnoguaranteethattwocheckerssetswilllookalike,sowewilltrytoavoidrigidassumptionsaboutthecolorscheme.However,highcontrastisgenerallyhelpfulincomputervision,andthisexampleisidealbecauseitusesfourcontrastingcolorsfordarksquares,lightsquares,darkpieces,andlightpieces.
Wewillassumethatalightbordersurroundsthecheckerboard(seetheprecedingimage).Thisassumptionmakesiteasiertodetecttheboard’sedge.
Lookcarefullyatthewhitesquarestotheleftofthekings.Sincethekingsaretallerthanthepawns,thekingscastlongershadowsintoadjacentlightsquares.Wewillrelyonthisobservationtodifferentiatebetweenpawnsandkings.Importantly,inabird’s-eyeviewoftheboard,theheightsoftheplayingpieceswillnotbevisiblebuttheshadowswillbe.Wewillassumethattheshadowsareapproximatelyorthogonal(notdiagonal),andaresufficientlylongtoreachanadjacentsquare.
Tofurthersimplifyourcomputervisionwork,wewillrequirethatthecameraandcheckerboardremainstationaryrelativetoeachother.Thecamerashouldhaveaviewoftheentireboardplusasmallmargin.Toachievethis,thewebcamwillneedtobeapproximately2feet,or0.6meters,awayfromtheboard(however,theexactrequirementwillvarydependingonthewebcam’sfieldofviewandtheboard’ssize).Asseeninthe
followingphotograph,atripodisagoodwaytokeepthewebcamstationaryinahighpositionfromwhereitcanviewthewholeboard:
Asthefollowingscreenshotshows,ourCheckersapplicationwilldisplayboththeunmodifiedcameraview(left)andanidealizedbird’s-eyeview(right):
Thebird’s-eyeviewcontainsannotations,orinotherwords,textforshowingtheclassificationresults.Theannotation1meansadarkpawn,11adarkking,2alightpawn,and22alightking.Alackofannotationmeansanemptysquare.
NotetheGUIwidgetsatthebottomofthescreenshot.Thesecontrolswillenabletheusertoadjustcertainparametersofourcheckerboarddetectorandsquareclassifier.Specifically,thefollowingadjustmentswillbesupported:
RotateboardCCW:Clickonthisbuttontorotatethebird’s-eyeview90degreescounterclockwise(CCW)FlipboardX:Clickonthisbuttontoflipthebird’s-eyeviewhorizontallyFlipboardY:Clickonthisbuttontoflipthebird’s-eyeviewverticallyShadowdirection:Selectoneoftheradiobuttons—up,left,down,orright—tospecifythedirectionofthekings’shadowsinthebird’s-eyeviewEmptythreshold:Movethisslidertothelefttoclassifymoresquaresasnonempty,ortotherighttoclassifymoresquaresasemptyPlayerthreshold:Movethisslidertothelefttoclassifymorepiecesaslight,ortotherighttoclassifymorepiecesasdarkShadowthreshold:Movethisslidertothelefttoclassifymorepiecesaskings,ortotherighttoclassifymorepiecesaspawns
WewilldivideourimplementationoftheCheckersapplicationintosixPythonmodules:
Checkers.py:ThismoduleimplementstheGUIusingWxPython.CheckersModel.py:ThismoduleusesOpenCV,NumPy,andscikit-learntocapture,analyze,andaugmentimagesofacheckersgameinrealtime.WxUtils.py:ThismoduleprovidesautilityfunctiontoconvertimagesfromOpenCVformatstoadisplayableWxPythonformat.TheimplementationincludesaworkaroundforaWxPythonbugthataffectsfirst-generationRaspberryPicomputers.ResizeUtils.py:UsingOpenCV,thismoduleprovidesautilityfunctiontotrytosetacamera’sresolutionandreturntheactualresolution.ColorUtils.py:UsingNumPy,thismoduleprovidesutilityfunctionstoextractacolorchannel(suchastheredcomponentofanimage)andquantifydifferencesthebetweencolors.CVBackwardCompat.py:ThismoduleprovidesaliasessothatthingsinOpenCV2.xappeartohavethesamenamesastheirequivalentsinOpenCV3.x.
Beforewewriteanyofthesemodules,let’ssetupthedependencies.
SettingupOpenCVandotherdependenciesThischapter’sprojectwillworkwithOpenCV2.xor3.xandPython2.7or3.4.NotethatOpenCV2.xdoesnotsupportPython3.x.However,onmostLinuxsystems,itismoreconvenienttoinstallOpenCV2.x(andusePython2.7)becauseOpenCV3.xdoesnothavebinarypackagesformostLinuxsystemsyet.
ThefollowingsubsectionscoveronlythesimplestwaystosetupourdependenciesonWindows,Mac,andseveralLinuxdistributions.Otherapproaches(andotherUnix-likeplatforms)canalsowork.Forexample,advancedusersmaywishtoconfigureandbuildOpenCV3.0’ssourcecodeonLinuxsystemsthatdonotyethaveprepackagedbuildsofOpenCV3.x.Forguidanceonalternativesetups,refertooneofPacktPublishing’sdedicatedbooksonOpenCV,suchasLearningOpenCV3ComputerVisionwithPythonbyJoeMinichinoandJosephHowse.
WindowsChristophGohlke,fromtheUniversityofCalifornia,Irvine,providesmanyreliableprebuiltversionsofscientificPythonlibrariesforWindows.Gotohisdownloadsiteathttp://www.lfd.uci.edu/~gohlke/pythonlibs/.IfyouhadnotinstalledNumPyinChapter4,Steeringbehaviors,getthelatestNumPy+MKLwhlfilefromGohlke’ssite(MKLisanIntelMathKernelLibrarythatprovidesoptimizations).Atthetimeofwritingthisbook,itisoneofthefollowing:
Python2.7,32-bit:numpy‑1.9.2+mkl‑cp27‑none‑win32.whlPython2.7,64-bit:numpy‑1.9.2+mkl‑cp27‑none‑win_amd64.whlPython3.4,32-bit:numpy‑1.9.2+mkl‑cp34‑none‑win32.whlPython3.4,64-bit:numpy‑1.9.2+mkl‑cp34‑none‑win_amd64.whl
OpenCommandPromptandusepiptoinstallNumPyfromthewheelfile.Therelevantcommandwillbesomethinglikethis:
$pipinstallnumpy‑1.9.2+mkl‑cp27‑none‑win32.whl
TheoutputfromthiscommandshouldincludeSuccessfullyinstallednumpy-1.9.2+mklorasimilarline.
NoteIfyourunintoproblemslaterwhileimportingmodulesfromOpenCVorscikit-learn,tryuninstallinganypreviousversionofNumPyfromChapter4,SteeringbehaviorsandusingthelatestNumPy+MKLwhlfilefromGohlke’ssite.
Similarly,getGohlke’slatestwhlfileforscikit-learnandinstallitwithpip.Atthetimeofwritingthisbook,itisoneofthefollowing:
Python2.7,32-bit:scikit_learn‑0.16.1‑cp27‑none‑win32.whlPython2.7,64-bit:scikit_learn‑0.16.1‑cp27‑none‑win_amd64.whlPython3.4,32-bit:scikit_learn‑0.16.1‑cp34‑none‑win32.whlPython3.4,64-bit:scikit_learn‑0.16.1‑cp34‑none‑win_amd64.whl
Also,atthetimeofwritingthisbook,GohlkeoffersdownloadsofOpenCV2.4forPython2.7andOpenCV3.0forPython3.4.Thefollowingarethelatestversions:
Python2.7,32-bit:opencv_python‑2.4.11‑cp27‑none‑win32.whlPython2.7,64-bit:opencv_python‑2.4.11‑cp27‑none‑win_amd64.whlPython3.4,32-bit:opencv_python‑3.0.0‑cp34‑none‑win32.whlPython3.4,64-bit:opencv_python‑3.0.0‑cp34‑none‑win_amd64.whl
IfyouaresatisfiedwithusingGohlke’slatestbuildofOpenCV,downloadthewhlfileandinstallitwithpip.
Alternatively,togetOpenCV3.xwithPython2.7bindings,wecandownloadanofficialbuildfromtheOpenCVsiteandperformsomeinstallationstepsmanually.TheavailabledownloadsofOpenCVarelistedathttp://opencv.org/downloads.html.GetthelatestversionforWindows.Atthetimeofwritingthisbook,thelatestfileiscalledopencv-
3.0.0.exe,anditincludesprebuiltbindingsforPython2.7butnot3.4.Runit,andwhenprompted,enterthefolderintowhichyouwanttoextractOpenCV.Wewillrefertothislocationas<opencv_unzip_path>.Amongotherthings,theextractedsubfolderscontainthepydanddllfiles,whicharecompiledlibraryfilesthatthePythoninterpretercanloadatruntime.Next,wemustensurethatPythoncanfindthesefiles.
CopyoneofthefollowingpydfilestothePythonsite-packagesfolder:
Python2.7,32-bit:<opencv_unzip_path>/opencv/build/python/x86/cv2.pydPython2.7,64-bit:<opencv_unzip_path>/opencv/build/python/x64/cv2.pyd
NoteTypically,thePython2.7site-packagesfolderwouldbelocatedatC:\Python27\Lib\site-packages.
Now,editthesystem’sPathvariabletoincludeoneofthefollowingfolders(whichcontaindllfilesofOpenCVandsomeofitsdependencies):
Python2.7,32-bit:<opencv_unzip_path>/build/x86/vc12/binPython2.7,64-bit:<opencv_unzip_path>/build/x64/vc12/bin
NoteThePathvariablecanbeeditedbygoingtoControlPanel|SystemandSecurity|System|Advancedsystemsettings|EnvironmentVariables….EdittheexistingvalueofPathbyappendingsomethinglike;<opencv_unzip_path>/build/x86/vc12/bin.Notetheuseofthesemicolonasaseparatorbetweenpaths.
Finally,werequirewxPython.ForPython2.7,wxPythoninstallersareavailableathttp://wxpython.org/download.php.Downloadandrunthelatestinstaller.Atthetimeofwriting,itisoneofthefollowingexefiles:
Python2.7,32-bit:wxPython3.0-win32-3.0.2.0-py27.exePython2.7,64-bit:wxPython3.0-win64-3.0.2.0-py27.exe
ForPython3.4,wemustusewxPythonPhoenix,whichisamoreactivelydevelopedforkofwxPython.SnapshotbuildsofwxPythonPhoenixarelistedathttp://wxpython.org/Phoenix/snapshot-builds/.DownloadthelatestwhlfileforPython3.4onWindows,andinstallitwithpip.Atthetimeofwritingthisbook,itisoneofthefollowing:
Python3.4,32-bit:wxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-none-win32.whl
Python3.4,64-bit:wxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-none-win_amd64.whl
Now,wehaveallthenecessarydependenciesfordevelopingourprojectonWindows.
MacTheMacPortspackagemanagerprovidesaneasycommand-linetooltoconfigure,build,andinstallopensourcesoftware,suchasOpenCV.MacPortsitselfisanopensourcecommunityproject,butitdependsonApple’sXcodedevelopmentenvironment,includingtheXcodeCommandLineTools.TosetupXcode,theXcodeCommandLineTools,andthenMacPorts,followtheinstructionsgivenathttps://www.macports.org/install.php.
OnceMacPortsissetup,wecanopentheterminalandstartinstallingpackages,alsoknownasports.Atypicalinstallationcommandhasthefollowingformat:
$sudoportinstall<package>+<variant_0>+<variant_1>...
Thevariantsarealternativeconfigurationsoftheport.Forexample,OpenCVcanbeconfiguredtobuilditsPythonbindingsandtouseoptimizationsforcertainpiecesofhardware.TheseoptimizationsaddmoredependenciestoOpenCVbutmaymakeitsfunctionsrunfaster.WewilltellOpenCVtooptimizeitselfviathreeframeworks:Eigen(forCPUvectorprocessing),IntelThreadBuildingBlocks(TBB,forCPUmultiprocessing),andOpenCL(forGPUmultiprocessing).TheOpenCLoptimizationsarenotusedbythePythoninterface,butitisgoodtohavethemjustincaseyouworkinfuturewithOpenCVinotherlanguages.
TipTosearchforportsbyname,runacommandsuchasthefollowing:
$portlist*cv*
Theprecedingcommandwilllistallportswhosenamescontaincv,notablytheopencvport.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,therelevantPythonversionandNumPyversionarealsoinstalled.AllMacPortsPythoninstallationsareseparatefromMac’sbuilt-inPythoninstallation(sometimescalledApplePython).TousetheMacPortsPython2.7installationasyourdefaultPythoninterpreter,runthefollowingcommand:
$sudoportselectpythonpython27
Alternatively,tomaketheMacPortsPython3.4installationyourdefaultPythoninterpreter,runthiscommand:
$sudoportselectpythonpython34
IfiteverbecomesnecessarytomakeApplePythonthedefaultagain,runacommandsuchasthefollowing:
$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,whichisamoreactivelydevelopedforkofwxPython.Atthetimeofwritingthisbook,MacPortsdoesnotcontainanywxPythonPhoenixpackages,sowewillfirstsetuppipandthenuseittoinstallthelatestsnapshotbuildofwxPythonPhoenix.ToinstallpipforPython3.4,runthefollowingcommand:
$sudoportinstallpy34-pip
Wecanmakethenewlyinstalledpipthedefaultpipexecutable:
$sudoportselectpippip34
SnapshotbuildsofwxPythonPhoenixarelistedathttp://wxpython.org/Phoenix/snapshot-builds/.DownloadthelatestwhlfileforPython3.4onMac.Atthetimeofwritingthisbook,itiswxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-cp34m-macosx_10_6_intel.whl.Toinstallthispackage,runacommandsuchasthefollowing:
$sudopipinstallwxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-cp34m-
macosx_10_6_intel.whl
TheoutputfromthiscommandshouldincludeSuccessfullyinstalledwxPython-Phoenix-3.0.3.dev1764+9289a7corasimilarline.
Now,wehaveallthenecessarydependenciesfordevelopingourprojectonMac.
Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintForPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,opentheterminalandrunthefollowingcommand:
$sudoapt-getinstallpython-opencvpython-scikits-learnpython-wxgtk2.8
Notethatthepython-opencvpackagedependsonthepython-numpypackage,sopython-numpywillalsobeinstalled.
Asanalternative,youcaninstallwxPythonPhoenixinsteadofthepython-wxgtk2.8package.TosetupwxPythonPhoenixanditsdependencies,trythefollowingcommands:
$sudoapt-getinstalllibwxbase3.0-devlibwxgtk3.0-devwx-common
libwebkit-devlibwxgtk-webview3.0-devwx3.0-exampleswx3.0-headerswx3.0-
i18nlibwxgtk-media3.0-dev
$pipinstall--upgrade--pre-fhttp://wxpython.org/Phoenix/snapshot-
builds/--trusted-hostwxpython.orgwxPython_Phoenix
However,ifindoubt,justusethepython-wxgtk2.8package.
Fedoraanditsderivatives,includingRHELandCentOSForPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,opentheterminalandrunthefollowingcommand:
$sudoyuminstallopencv-pythonpython-scikit-learnwxPython
Notethattheopencv-pythonpackagedependsonthenumpypackage,sonumpywillalsobeinstalled.
OpenSUSEanditsderivativesAgain,forPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,opentheterminalandrunthefollowingcommand:
$sudoyuminstallpython-opencvpython-scikit-learnpython-wxWidgets
Notethatthepython-opencvpackagedependsonthepython-numpypackage,sopython-numpywillalsobeinstalled.
SupportingmultipleversionsofOpenCVOpenCV2.xandOpenCV3.xbothusethenamecv2fortheirtop-levelPythonmodule.However,insidethismodule,someclasses,functions,andconstantshavebeenrenamedinOpenCV3.x.Moreover,somefunctionalityisentirelynewinOpenCV3.x,butourprojectreliesonlyonfunctionalitythatispresentinbothmajorversions.
Tobridgeafewofthenamingdifferencesbetweenversions2.xand3.x,wecreateamodule,CVBackwardCompat.Itbeginsbyimportingcv2:
importcv2
OpenCV’sversionstringisstoredincv2.__version__.Forexample,itsvaluemaybe2.4.11or3.0.0.Wecanusethefollowinglineofcodetogetthemajorversionnumberasaninteger,suchas2or3:
CV_MAJOR_VERSION=int(cv2.__version__.split('.')[0])
ForOpenCV2.x(orearlier),wewillinjectnewnamesintotheimportedcv2modulesothatallthenecessaryOpenCV3.xnameswillbepresent.Specifically,weneedtocreatealiasesforseveralconstantsthathavenewnamesinOpenCV3.x,asseeninthefollowingcode:
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.OurothermoduleswillbeabletoimporttheCVBackwardCompatinstanceofcv2anduseanyofthealiasesthatwemayhaveinjected.Wewillseeanexampleinthenextmodule—ResizeUtils.
ConfiguringcamerasOpenCVprovidesaclass,calledcv2.VideoCapture,thatrepresentsastreamofimagesfromeitheravideofileoracamera.Thisclasshasmethodssuchasread(image)forexposingthestream’snextimageasaNumPyarray.Italsohastheget(propId)andset(propId,value)methodsforaccessingpropertiessuchasthewidthandheight(inpixels),colorformat,andframerate.Thevalidpropertiesandvaluesmaydependonthesystem’svideocodecsorcameradrivers.
Acrosscameras,thedefaultpropertyvaluesmaydifferdramatically.Forexample,onecameramightdefaulttoanimagesizeof640x480,whileanothermaydefaultto1920x1080.Forgreaterpredictability,weshouldtrytosetcrucialparametersratherthanrelyonthedefaults.Let’screateamodulecalledResizeUtilscontainingautilityfunctiontoconfiguretheimagesize.
TheResizeUtilsmodulebeginsbyimportingtheCVBackwardCompatinstanceofcv2,whichmaycontainaliases(dependingontheOpenCVversion).Hereistheimportstatement:
fromCVBackwardCompatimportcv2
ThepropertyIDsforwidthandheightarestoredinconstantscalledcv2.CAP_PROP_FRAME_WIDTHandcv2.CAP_PROP_FRAME_HEIGHT,respectively(referringtotheprevioussection,notethattheconstants’originalnamesinOpenCV2.xwerecv2.cv.CV_CAP_PROP_FRAME_WIDTHandcv2.cv.CV_CAP_PROP_FRAME_HEIGHT,butwecreatedaliasestomatchtheOpenCV3.xnames).Notetheuseoftheseconstantsinthefollowingimplementationoftheutilityfunction:
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.VideoCaptureobjectcalledcaptureandatwo-dimensionaltuplecalledpreferredSize.WetrytoconfigurecapturetousethepreferredwidthandheightinpreferredSize.Thepreferreddimensionsmayormaynotbesupported,soaswiththefeedback,wereturnatupleoftheactualwidthandheight.
TheResizeUtilsmodulewillbeusefulforourCheckersModelmodule,asCheckersModelisresponsibleforinstantiatingandreadingfromcv2.VideoCapture.
WorkingwithcolorsNormally,whenOpenCVobtainsanimagefromafileorcamera,itputstheimageintheblue-green-red(BGR)colorformat.Morespecifically,theimageisa3DNumPyarrayinwhichimage[y][x][0]isapixel’sbluevalue(intherangeof0to255),image[y][x][1]isitsgreenvalue,andimage[y][x][2]isitsredvalue.Theyandxindicesstartfromthetop-leftcorneroftheimage.Ifweconvertanimageintograyscale,itbecomesa2DNumPyarray,inwhichimage[y][x]isapixel’sgrayscalevalue.
Let’swritesomeutilityfunctionsintheColorUtilsmoduletoworkwithcolordata.OurfunctionswillusePython’sstandardmathmoduleandNumPy,asseeninthefollowingimportstatements:
importmath
importnumpy
Let’swriteafunctionthatallowsustocopyasinglecolorchannelfromasourceimage(whichisinBGRformat)toadestinationimage(whichisingrayscaleformat).IfthespecifieddestinationimageisNoneoritsformatiswrong,ourfunctionwillcreateit.Tocopythechannel,wejusttakeaflatviewofthesourceimage,sliceitwithastrideof3,andassigntheresulttoafullsliceofthedestinationimage,asseeninthefollowingimplementation:
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,theredchannelshowsahighcontrastbetweendarkandlightsquares.Later,wewillusethisobservationtoouradvantage.
Ontheotherhand,partsofouranalysiswillrelyonfullcolorsinsteadofonlyonechannel.WewillneedtoquantifythecontrastordistancebetweentwoBGRcolorsinordertodecidewhetherasquarecontainsalightpiece,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,thisapproachassumesthatallcolorchannelshavethesamescaleandthescaleislinear.Thisassumptiondefiescommonsense.Forexample,mostpeoplewouldagreethatthecoloramber(b=0,g=191,r=255)isnotashadeofred(b=0,g=0,r=255)butthatthecoloremerald(b=191,g=255,r=0)isashadeofgreen(b=0,g=255,r=0),eventhoughthesetwopairingsrepresentthesameEuclideandistance(191).Manyalternative
formulationsassumethatthechannelsarestilllinearbuthavedifferentscales—typically,theyassumethatgreenhasthebiggestunit,followedbyred,andthenblue.Theseformulationsyieldhighcontrastbetweenyellowandblue(forexample,betweensunlightandshade),andyetyieldlowcontrastbetweenshadesofblue.Thisisstillratherunsatisfactorybecausemostpeoplearegoodatdistinguishingshadesofblue.ThiadmerRiemersmacomparesseveraldistanceformulationsathttp://www.compuphase.com/cmetric.htm,andproposesanalternativeinwhichthegreenscaleislinearwhiletheredandbluescalesarenonlinear,withreddifferenceshavingmoreweightincomparisonsofreddishcolorsandbluedifferenceshavingmoreweightincomparisonsofnon-reddishcolors.Let’sreplaceourpreviouscolorDistfunctionwiththefollowingimplementationofRiemersma’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,thedistancebetweenblackandwhiteisapproximately764.8.Thescaleofthisdistanceisnotintuitive.Wemightprefertoworkwithnormalizedvalues,wherebythenormalizeddistancebetweenblackandwhiteisdefinedas1.0.Thefollowingfunctionreturnsanormalizedcolordistance:
defnormColorDist(color0,color1):
#Normalizebasedonthedistancebetween(0,0,0)and
#(255,255,255).
returncolorDist(color0,color1)/764.8333151739665
NowwehavesufficientutilityfunctionstosupportourupcomingimplementationoftheCheckersModelmodule,whichwillcaptureandanalyzeimages.
BuildingtheanalyzerTheCheckersModelmoduleistheeyesandthebrainofourproject.ItbringstogethereverythingexcepttheGUI.Specifically,itdependsonNumPy,OpenCV,scikit-learn,andourColorUtilsandResizeUtilsmodules,asreflectedinthefollowingimportstatements:
importnumpy
importsklearn.cluster
fromCVBackwardCompatimportcv2
importColorUtils
importResizeUtils
NoteAlthoughwearecombiningimagecapturingandanalysisintoonemodule,theyarearguablydistinctresponsibilities.Forthisproject,theyshareadependencyonOpenCV.However,inafutureproject,youmightcaptureimagesfromacamerathatrequiresanotherlibrary,orfromanentirelydifferenttypeofsource,suchasanetwork.Youmightevensupportawidevarietyofcapturingtechniquesinoneproject.Wheneveryoufeelthatimagecaptureisacomplexprobleminitsownright,considerdedicatingatleastoneseparatemoduletoit.
Tomakeourcodemorereadable,wewilldefineseveralconstantsinthismodule.Theseconstantsrepresentthepossiblestatesoftheboard’srotation,theshadows’direction,andeachsquare’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.ThisisimportantforPython2.xcompatibilitybecausewearegoingtousepropertygetterandsettermethods,asdiscussedinthenextsubsection.UnlikePython3.x,Python2.xdoesnotsupport
ProvidingaccesstotheimagesandclassificationresultsThemembervariablesoftheCheckersModelclasswillincludecurrentimagesofthescene(thatis,everythingthatthewebcamcansee)inBGRandgrayscale,aswellasacurrentimageoftheboardinBGR.Unlikethescene,theboardwillbeabird’s-eyeviewandmaycontaintextinordertoshowtheclassificationresults.Wewillprovidepropertygetterssothatothermodulesmayreadtheimagesandtheirsizes,asseeninthefollowingcode:
@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
NotePropertygettersandsetterssimplyprovideashorthandnotationsothatamethodlookslikeanon-callablevariable.Hereisanexamplethatdemonstrateshowtouseapropertygetter:
bm=BoardModel()
scene=bm.scene#Callsgetter,bm.scene()
Here,wehaveimplementedonlyagetterandnotasetterbecauseothermodulesshouldnotsettheimages.Thus,apieceofcodesuchasthefollowingwillproduceanerror:
bm.scene=None#Callssetter,bm.scene(None)
#Error!Thereisnosuchsetter.
Thenextsubsection,Providingaccesstoparametersfortheusertoconfigure,willgiveexamplesonhowtoimplementsettermethods.
Wealsoprovideagetterfortheclassificationresultsasa2DNumPyarray:
@property
defsquareStatuses(self):
returnself._squareStatuses
Forexample,ifPlayer1hasakinginthetop-leftcorneroftheboard,boardModel.squareStatuses[0,0]willbe11(thevalueoftheSQUARE_STATUS_KING_PLAYER_1constant,whichwedefinedearlier).
AlltheaforementionedpropertiesrepresenttheresultsoftheCheckersModelmodule’swork,soothermodulesshouldtreatthemasread-only(andthustheyhaveonlygetters,notsetters).Next,let’sconsiderotherpropertiesthatrepresenttheparametersoftheCheckersModelmodule’swork,andhavebothgettersandsetterssothatothermodulesmayreconfigurethem.
ProvidingaccesstoparametersfortheusertoconfigureTounderstandwhyCheckersModeloffersparametersthatarereconfigurableatruntime,let’slookatapreviewofthethingsthatwewillnotautomateinthisproject:
Thebird’s-eyeviewofthecheckerboardmayberotatedandflippedinawaydifferentfromwhattheuserexpects.ThisproblemisdiscussedintheCreatingandanalyzingthebird’s-eyeviewoftheboardsubsection.Weallowtheusertospecifyadifferentrotation(0,90,180,or270degrees)andflip(X,Y,neither,orboth).Theshadows’directionisnotdetectedautomatically.Bydefault,weassumethattheshadows’directionisup(negativey).Weallowtheusertospecifyadirection(up,right,down,orleft).Toclassifythecontentsofthesquares,wecomparethetwodominantcolorsinthesquare,andthiscomparisonreliesoncertainthresholdvalues.Forsomelightingconditionsandsomecolorsofcheckerssets,thedefaultthresholdsmightnotbeappropriate.Weallowtheusertospecifydifferentthresholds.TheprecisemeaningofeachthresholdisdiscussedintheAnalyzingthedominantcolorsinasquaresubsection.
Theboard’srotationisexpressedasaninteger,correspondingtooneoftheconstantsthatwedeclaredatthestartofthismodule.Thefollowingcodeimplementsthegettersandsettersfortherotation,andthesetterusesthemodulusoperatortoensurethattherotationswraparound:
@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,sowewillsimplyimplementthemasplainoldmembervariables.Wecanseetheirdeclarationsinthefollowingcode,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.
InitializingtheentiremodelofthegameAswehavepreviouslydiscussed,theroleoftheCheckersModelclassistoproduceimagesofthesceneandboardandclassifythecontentsofeachsquare.Theremainderofthe__init__methoddeclaresvariablesthatpertaintoimagingandclassification.
TheimagesofthesceneandboardwillinitiallybeNone,asseenhere:
self._scene=None
self._sceneGray=None
self._board=None
Lookingbackattheprevioussubsection,notethatpatternSizeisoneoftheinitializer’sarguments.Thisvariablereferstothenumberofinternalcornersintheboard,suchas(7,7)inastandardAmericancheckerboardwitheightrowsandeightcolumns.Later,wewillseethatthenumberofinternalcornersisimportantforcertainOpenCVfunctions.Let’sputthecornerdimensionsandcountintothemembervariables,likethis:
self._patternSize=patternSize
self._numCorners=patternSize[0]*patternSize[1]
Later,asthestepstowardclassification,wewillmeasurethefrequencyanddistanceofthetwodominantcolorsineachsquare.ThistaskisdiscussedintheAnalyzingthedominantcolorsinasquaresubsection.Fornow,wewillonlycreateemptyNumPyarraysforthisdata:
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,andwewillfillinthisarraywiththeSQUARE_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-learncalledsklearn.cluster.MiniBatchKMeans.Thisclassrepresentsastatisticalprocesscalledk-meansclustering,whichseparatesdataintoagivennumberofgroupsandfindsthecentroidofeachgroup.Fornow,weonlyneedtoconstructaninstanceoftheclasswithasingleargument,n_clusters,whichindicatesthenumberofgroupsthattheclustererwilldistinguish(2inthiscase,becausewewanttoknowthetwodominantcolors):
self._clusterer=sklearn.cluster.MiniBatchKMeans(2)
Tosupportwebcaminput,wewillcreateaVideoCaptureobjectandsetitscapturedimensionsusingourutilityfunctionfromtheResizeUtilsmodule:
self._capture=cv2.VideoCapture(cameraDeviceID)
self._sceneSize=ResizeUtils.cvResizeCapture(
self._capture,sceneSize)
w,h=self._sceneSize
Later,aswecaptureimages,wewilltrytofindtheinternalcornersofthecheckerboard,andwewillstorethesuccessfullydetectedcornerssothatwedonothavetoperformthissearchfromscratchforeveryframe.Thefollowingtwomembervariableswillholdthepreviouslydetectedcorners(initiallyNone)andagrayscaleimageofthesceneatthetimeofthepreviousdetection(initiallyanemptyimage):
self._lastCorners=None
self._lastCornersSceneGray=numpy.empty(
(h,w),numpy.uint8)
Thescene’sdetectedcornercoordinateswillimplyahomography(amatrixthatdescribesadifferenceinperspective)whenwecomparethemwiththeknowncornercoordinatesinabird’s-eyeviewoftheboard.Likethecorners,thehomographydoesnotneedtoberecomputedforeveryframe,sowewillstoreitinthefollowingmembervariable(initiallyNone):
self._boardHomography=None
Wewillarbitrarilystipulatethatthebird’s-eyeviewoftheboardwillbenolargerthanthecapturedimageofthescene.Startingfromthisconstraint,wewillcalculatethesizeofasquareandtheboard:
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,wewillcalculateasetofcornercoordinatesforthebird’s-eyeview.Weneedthecoordinatesofonlythoseinternalcornerswherefoursquaresmeet.Later,intheDetectingtheboard’scornersandtrackingtheirmotionsubsection,wewillcomparetheseidealcoordinatestotheinternalcornersofthecheckerboardthataredetectedinthesceneinordertofindthehomography.HereisourcodeforcreatingalistandthenaNumPyarrayofevenlyspacedcorners:
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’sconsiderhowtheimagesofthesceneandboardwillchange,alongwiththeresultsofclassification.
UpdatingtheentiremodelofthegameOurCheckersModelclasswillprovidethefollowingupdatemethod,whichothermodulesmaycall:
defupdate(self,drawCorners=False,
drawSquareStatuses=True):
Specifically,theGUIapplication(intheCheckersmodule)willcallthisupdatemethod.Logically,theapplicationisresponsibleformanagingresourcesandensuringthateverythingremainsresponsive,soitisinabetterpositiontodecidewhenanupdateshouldoccur.Theupdatemethod’sdrawCornersargumentspecifieswhethertheresultsofcornerdetectionshouldbedisplayedinthescene.ThedrawSquareStatusesargumentspecifieswhethertheresultsofclassificationshouldbedisplayedinthebird’s-eyeviewoftheboard.
Theupdatemethodreliesonseveralhelpermethods.First,wewillcallahelperthattriestocaptureanewimageofthescene.Ifthisfails,wereturnFalsetoinformthecallerthatnoupdatehasoccurred:
ifnotself._updateScene():
returnFalse#Failure
Wewillproceedtosearchforthecheckerboardinthescene.Asuccessfulsearchwillproduceasetofcornercoordinatesfortheboard’ssquares.Moreover,thesecoordinateswillimplyahomography.Thefollowinglineofcodecallsahelpermethodthatisresponsibleforfindingthecornersandahomography:
self._updateBoardHomography()
Next,wewillcallahelpermethodthatisresponsibleforcreatingthebird’s-eyeviewoftheboardaswellasanalyzingandclassifyingeachsquare:
self._updateBoard()
Atthispoint,theboarddetectionandsquareclassificationarecomplete,butwemaystillneedtodisplayresultsdependingonthedrawCornersanddrawSquareStatusesarguments.Conveniently,OpenCVprovidesafunction,cv2.drawChessboardCorners(image,patternSize,corners,patternWasFound),todisplayasetofdetectedcornersinascenecontainingachessboardorcheckerboard.Hereisthewayweuseit:
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,andwereturnTruetoletthecallerknowthattherearenewresults:
returnTrue#Success
Let’sdelvedeeperintothehelpermethods’roles,startingwithimagecapture.
CapturingandconvertinganimageOpenCV’sVideoCaptureclasshasaread(image)method.Thismethodcapturesanimageandwritesittothegivendestinationarray(ifimageisNoneoritsformatiswrong,anewarrayiscreated).Themethodreturnsatuple,(retval,image),containingaboolean(Trueifthecapturehassucceeded)andthentheimage(eithertheoldarrayoranewlycreatedarray).Weusethismethodtotrytocaptureanewscenefromthecamera.Ifthissucceeds,weuseourextractChannelutilityfunctiontogettheredchannelasagrayscaleversionofthescene.Thefollowingmethodperformsthesestepsandreturnsabooleantoindicatesuccessorfailure:
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.TheleftmostimagerepresentstheoriginalsceneinBGRcolor(thoughitwillappearasgrayscaleinthisbook’sprintedition).Thisboard’scolorsareburntamberforthedarksquares,ochreforthelightsquares,andburntsiennafortheborder(thiscolorschemeisquitecommonincheckerboards).Thesecond,third,andfourthimages(fromlefttoright)representthescene’sblue,green,andredchannels,respectively:
Notethattheredchannelcapturesthecheckerboardbrightlyandwithgoodcontrastbetweenlighteranddarkersquares.Moreover,theboard’sborderappearsquitelight.Thisisgoodbecausewearesoongoingtouseadetectorthatisoptimizedforablack-and-whiteboardwithawhiteborder.
Detectingtheboard’scornersandtrackingtheirmotionWedonotwanttoupdatetheboard’scornersandhomographyineveryframe,butonlyinframeswheretheboardorcamerahasmoved.Thisrulereducesthecomputationalburdeninmostframesandallowsustokeepagooddetectionresultfromapreviousframesothatwedonotrequireanunobstructedviewoftheboardineveryframe.Particularly,wecanberelativelysureofgettinggooddetectionresultswhentheboardisempty,andwewouldwanttokeeptheseresultsforlaterframesinwhichtheboardisclutteredwithpiecesorwiththeplayers’movinghands.
Beforefindingnewcornersandthehomography,let’slookattrackingmotion.Supposewehavedetectedasetofcornersinapreviousframe.Ratherthansearchingtheentireimagefornewcorners,wecantrytofindthesamecornersatorneartheirpreviouslocations.Thisidea—mappingframe-to-framemotionofindividualpointsinanimage—iscalledopticalflow.OpenCVprovidesimplementationsofseveralopticalflowtechniques.WewilluseatechniquecalledpyramidalLukas-Kanade,whichisimplementedinthecv2.calcOpticalFlowPyrLK(prevImg,nextImg,prevPts)function.Thisfunctionreturnsatuple,(nextPts,status,error).Eachofthetuple’selementsisanarray.ThenextPtscontainsthepoints’estimatednewcoordinates.Thestatuscontainscodesforindicatingwhethereachestimateisvalid(1,meaningthepointwastracked)orinvalid(0,meaningitwaslost).Finally,errorcontainsameasurementofdissimilaritybetweenthepixelsintheoldandnewneighborhoodsforeachpoint.Ahighaverageerroracrossallpointsimpliesthateverythinglooksdifferentand,mostlikely,thattheboardorcamerahasmoved.Weapplythisreasoninginthefollowingcode,whichisthebeginningofthe_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
NoteThethreshold,meanError<4.0,hasbeenchosenexperimentally.Ifyouseethattheestimateoftheboard’scornerschangesevenwhentheboardisstill,tryraisingthethreshold.Conversely,ifyoufindthattheestimateoftheboard’scornersremainsunchangedevenwhentheboardismoved,youmayneedtolowerthethreshold.
Ifwehavenotyetreturned,itmeansthateithertherearenopreviouslyfoundcorners,orthepreviouslyfoundcornersarenolongervalid(probablybecausetheboardorthecamerahasmoved).Eitherway,wemustsearchfornewcorners.OpenCVprovidesmanygeneral-purposefunctionsforfindinganykindofcorner,butconveniently,italsoprovidesaspecializedfunctionforfindingthecornersofsquaresinachessboardoracheckerboard.Thisfunction,cv2.findChessboardCorners(image,patternSize),canfindaboardofanyspecifieddimensions,evenanon-squareboard.ThepatternSizeargumentspecifiesthenumberofinternalcorners,suchas(7,7)forastandardAmericancheckerboardwitheightrowsandeightcolumns.Thisfunctionreturnsatuple,(retval,corners),whereretvalisaBoolean(whichisTrueifallthecornerswerefound)andcornersisalistofcornercoordinates.Itexpectsacheckerboardwithpureblackandpurewhitesquares,butothercolorschemesmayworkdependingonthestrengthoftheircontrastinthegivengrayscaleimage.Moreover,thefunctionexpectstheboardtohavealightborder,whichmakesiteasiertofindthecornersoftheoutermostdarksquares.ThefollowingcodeshowsourusageoffindChessboardCorners:
#Findthecornersintheboard'sgrid.
cornersWereFound,corners=cv2.findChessboardCorners(
self._sceneGray,self._patternSize,
flags=cv2.CALIB_CB_ADAPTIVE_THRESH)
NotethatfindChessboardCornershasanoptionalflagsargument.Weusedaflagcalledcv2.CALIB_CB_ADAPTIVE_THRESH,whichstandsforadaptivethresholding.Whenthisflagisset,thefunctionattemptstocompensatefortheoverallbrightnessoftheimagesothatitdoesnotnecessarilyrequirethesquarestolookreallyblackorreallywhite.
ThesmallcirclesandlinesshowninthefollowingimageareavisualizationofthecornersfoundusingfindChessboardCorners.TheyaredrawnusingthedrawChessboardCornersfunction,whichwecoveredintheprevioussubsection.
Ifthecornersarefound,wecanconverttheircoordinatesfromalisttoaNumPyarray.Then,wecancomparethefoundcoordinateswiththereferencecoordinatesforabird’s-eyeview(rememberthatwealreadyinitializedthereferencecoordinatesintheInitializingtheentiremodelofthegamesubsection).Tocomparethetwosetsofcoordinates,wewilluseanOpenCVfunctioncalledcv2.findHomography(srcPoints,dstPoints,method).Theoptionalmethodargumentrepresentsastrategytorejectoutliers(incongruouspointsthatshouldnotbecountedtowardtheresult).Thefunctionreturnsahomographymatrix,whichisatransformationthatmapssrcPointstodstPointswithminimalerror.ThefollowingcodedemonstratesouruseoffindHomography:
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.Thenexttimethismethodiscalled,wewillcomparetheoldandnewgrayscalescenestotrackthecorners’motion.
Atthispoint,wemayhavefoundtheboard’shomography,butwehavenotyetcreatedanimagetoshowthebird’s-eyeview.Let’stacklethistasknext.
Creatingandanalyzingthebird’s-eyeviewoftheboardIftheboard’scornersandhomographyarefound,wecantransformthescene’sperspectivetoproduceabird’s-eyeviewoftheboard.TherelevantfunctioninOpenCViscv2.warpPerspective(src,M,dsize,dst),whereMisthehomographymatrixanddsizeisanarbitrarysizefortheoutputimage.Thisfunctionappliesthehomographymatrixtochangetheperspective,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),andtheresultingbird’s-eyeviewoftheboard(right)afterperspectivetransformation:
Atthisstage,thebird’s-eyeviewmightstillneedtoberotatedand/orflippedtomatchtheuser’ssubjectiveperceptionofdirections.Theuserisprobablysittingononesideoftheboardandperceivesthissideasthe“near”or“down”(positivey)side.Thecameramightbeonthesameside,anyotherside,oradiagonal!Moreover,dependingonitsdriversandconfiguration,thecameramayevencapturemirroredimages.ThefindChessboardCornersfunctiondoesnotlimititssearch.Asfarasitisconcerned,thescenecouldshowanupside-downandmirroredchessboard,andthissolutionisasgoodasanyothersolutionthatputsasetofcornersintherightplaces.Wecouldinspectandeditthevaluesinthe_boardHomographymatrixtoenforceourownassumptions,butinsteadofthis,wewilllettheuserinspecttheimageanddecideonanynecessarychanges.
Lookingcloselyattheprecedingpairofimages,notethattheboardhasadarkorblurryseamdownthemiddle(itcanfoldhere).Theseamisapproximatelyhorizontalinthecamera’sviewbutverticalinthebird’s-eyeview.Moreover,thetwoviewsdifferbyahorizontalflip.Ausermightfindthesedifferencesunintuitive.
OpenCVprovidesafunctioncalledcv2.flip(src,flipCode,dst)toflipanimageinthemannerspecifiedbyflipCode(-1orlesstoflipbothxandycoordinates,0toflipycoordinates,and1orgreatertoflipxcoordinates).Anotherfunction,cv2.transpose(src,dst),servestoswapxandycoordinateswitheachother.Arotationof90or270degreescanbeimplementedasacombinationofatransposeandaflip,whilearotationof180degreescanbeimplementedasaflipinbothdimensions.Let’susetheseapproachesinthefollowingcodetoapplyaspecifiedflipandrotation:
#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,andflipBoardYviatheGUI.Forexample,theusercanseetheprecedingpairofimagesandthenmakeadjustmentstoproducethefollowingpairofimagesinstead:
Oncethetransformationsarecomplete,wewilliterateoverallthesquaresandcallahelpermethodtogeneratedataabouteachsquare’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(sincewemaysearchforashadowtodistinguishakingfromapawn).Thisiswhyweanalyzethecolorsofallthesquaresinonestepandclassifythesquaresinanotherstep.Let’sconsidertheanalysisofcolorsnow.
AnalyzingthedominantcolorsinasquareOur_updateSquareDatamethodtakesasquare’sindicesasarguments,anditbeginsbycalculatingthesquare’stop-leftandbottom-rightpixelcoordinatesinthe_boardimage,asseeninthefollowingcode:
def_updateSquareData(self,i,j):
x0=i*self._squareWidth
x1=x0+self._squareWidth
y0=j*self._squareWidth
y1=y0+self._squareWidth
Rememberthatwehaveamembervariablecalled_clusterer.Itisaninstanceofthesklearn.cluster.MiniBatchKMeansclass.Thisclasshasamethod,calledfit(X),thatclassifiesthedatainXandstorestheresultsinthemembervariablesofMiniBatchKMeans.Xmustbea2DNumPyarray,sowemustreshapethesquare’simagedata.Forexample,ifasquarehas75x75pixelswiththreecolorchannels,wewillpassaviewofthesquareasanarrayofshape(75*75,3),not(75,75,3).Thefollowingcodeshowshowwesliceandreshapethesquareandpassittothefitmethod:
#Findthetwodominantcolorsinthesquare.
self._clusterer.fit(
self._board[y0:y1,x0:x1].reshape(
self._squareArea,3))
Theresultsofthecolorclusteringarestoredin_clusterer.centersand_clusterer.labels,whichareNumPyarrays.Theshapeofcentersis(2,3),anditrepresentsthetwodominantBGRcolorsinthesquare.Meanwhile,labelsisaone-dimensionalarraywhoselengthisequaltothenumberofpixelsinthesquare.Eachvalueinlabelsis0ifthepixelisclusteredwiththefirstdominantcolor,or1ifthepixelisclusteredwiththeseconddominantcolor.Thus,themeanoflabelsrepresentsthesecondcolor’sfrequency(theproportionofpixelsthatareclusteredwiththiscolor).Thefollowingcodeshowshowwecanfindthefrequencyofthelessdominantcolor,aswellasthenormalizeddistancebetweenthetwodominantcolors,basedontheformulainourColorUtilsmodule:
#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,“Thissquarecontainsabigobjectthatcontrastsstronglywiththebackground,”(highfrequencyandhighdistance)withoutneedingtosearchforanyspecificsquarecolororplayingpiececolor.Next,wewillusesuchobservationstoclassifythecontentsofasquare.
ClassifyingthecontentsofasquareOur_updateSquareStatusesmethodtakesasquare’sindicesasarguments(again),anditbeginsbylookingupthesquare’sfrequencyanddistancedata,asseeninthiscode:
def_updateSquareStatus(self,i,j):
freq=self._squareFreqs[j,i]
dist=self._squareDists[j,i]
Wearealsointerestedinthefrequencyanddistancedataofaneighboringsquarethatmaypotentiallycontainashadow.Asdiscussedearlier,theusermayconfigureashadow’sdirection.Thefollowingcodeshowshowwecanselectaneighborbasedontheshadow’sdirection:
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,wewilltestwhetherthefrequencyisbelowacertainthresholdandthatthedistanceisaboveanotherthreshold,asseeninthefollowingcode:
castsShadow=\
neighborFreqisnotNoneand\
neighborFreq<self.emptyFreqThresholdand\
neighborDistisnotNoneand\
neighborDist>self.shadowDistThreshold
Rememberthattheusermayconfigurethethresholdsinordertomanuallyadaptourapproachtodifferentlightingconditionsandcolorschemes.
NoteNotethatthesquaresononeedgeoftheboardwillnothaveanyneighborsintheshadow’sdirection,sowejustassumethatthereisnoshadowthere.Wecouldimproveonourapproachbyanalyzingtheborderareasjustpasttheboard’sedge.
Atthispoint,wehaveanideaofwhethertheneighbormightbeashadowornot,butwestillneedtoconsiderthecurrentsquare.Weexpectaplayingpiecetobealargeobject(highfrequency),andintheabsenceofsuchanobject,thesquaremustbeempty.Thislogicisreflectedinthefollowingcode,whichreliesonafrequencythreshold:
iffreq<self.emptyFreqThreshold:
squareStatus=SQUARE_STATUS_EMPTY
else:
Aplayingpiecemaybeeitherdarkorlight,andeitherapawnoraking.Weexpectalightplayingpiecetocontraststrongly(highdistance)withthebackgroundofadarksquare,whileadarkplayingpiecewillhaveweakercontrast.Thus,anotherdistancethresholdistested.Moreover,weexpectakingtohavealongshadowthatextendsintoaneighboringsquare,whileapawnshouldhaveashortershadow.Thefollowingcodereflectsthesecriteria:
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(andothermodules,viaapropertygetter)mayaccessit:
self._squareStatuses[j,i]=squareStatus
Next,wewillprovideaconvenientwaytovisualizetheclassificationsofallthesquares.
DrawingtextAsthelaststepofupdatingtheimageoftheboard,wewilldrawtextatopeachnonemptysquaretoshowthenumericcodeoftheclassificationresult.OpenCVprovidesafunctioncalledcv2.putText(img,text,org,fontFace,fontScale,color,thickness,lineType)fordrawingtextatthepositionspecifiedbytheorg(origin)argument.Theoriginreferstothetop-leftcornerofthetext.However,wewantthetexttobecenteredinthesquare.Tofindthetext’soriginrelativetothesquare’scenter,weneedtoknowthesizeofthetextinpixels.Fortunately,OpenCVprovidesanotherfunction,cv2.getTextSize(text,fontFace,fontScale,thickness),forthispurpose.Thefollowingcodeusesthesetwofunctionstoplacethetextinthecenterofagivensquare:
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_AAconstantstoselecttheHersheyPlainfontandanti-aliasing.
ThiscompletesthefunctionalityoftheCheckersModelclass.Next,wewriteautilityfunctiontoconvertanOpenCVimageforusewithwxPython.Afterthis,wewillimplementtheGUIapplication.ItconfiguresaninstanceofCheckersModelanddisplaystheresultingimagesofthesceneandboard.
ConvertingOpenCVimagesforwxPythonAswehaveseenearlier,OpenCVtreatsimagesasNumPyarrays—typically,3DarraysinBGRformator2Darraysingrayscaleformat.Conversely,wxPythonhasitsownclassesforrepresentingimages,typicallyinRGBformat(thereverseofBGR).Theseclassesincludewx.Image(aneditableimage),wx.Bitmap(adisplayableimage),andwx.StaticBitmap(aGUIelementthatdisplaysaBitmap).
OurwxUtilsmodulewillprovideafunctionthatconvertsaNumPyarrayfromeitherBGRorgrayscaletoanRGBBitmap,readyfordisplayinawxPythonGUI.ThisfunctionalitydependsonOpenCVandwxPython,asreflectedinthefollowingimportstatements:
fromCVBackwardCompatimportcv2
importwx
Conveniently,wxPythonprovidesafactoryfunctioncalledwx.BitmapFromBuffer(width,height,dataBuffer),whichreturnsanewBitmap.ThisfunctioncanacceptaNumPyarrayinRGBformatasthedataBufferargument.However,abugcausesBitmapFromBuffertofailonthefirst-generationRaspberryPi,apopularsingle-boardcomputer(SBC).Asaworkaround,wecanuseapairoffunctions:wx.ImageFromBuffer(width,height,dataBuffer)andwx.BitmapFromImage(image).Thelatterislessefficient,soweshouldpreferablyuseitonlyonhardwarethatisaffectedbythebug.
Tocheckwhetherwearerunningonthefirst-generationPi,wecaninspectthenameofthesystem’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,thenmakeaninformedguessaboutitsformat(BGRorgrayscale),andfinallyconvertittoanRGBarrayusinganOpenCVfunctioncalledcv2.cvtColor(src,code).Thecodeargumentspecifiesthesourceanddestinationformats,suchascv2.COLOR_BGR2RGB.Afterallofthis,weuseImageFromBufferandBitmapFromImagetoconverttheRGBarraytoanImageandtheImagetoaBitmap:
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,exceptthatweconverttheRGBarraydirectlytoaBitmapusingBitmapFromBuffer:
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.
BuildingtheGUIapplicationTheCheckersmodulewillcontainallofthecoderequiredfortheGUIapplication.ThismoduledependsonwxPythonaswellasPython’sstandardthreadingmoduletoallowustoputalloftheintensivecomputervisionworkontoabackgroundthread.Moreover,wewillrelyonourCheckersModelmoduleforthecapturingandanalysisofimages,andourWxUtilsmoduleforitsimageconversionutilityfunction.Herearetherelevantimportstatements:
importthreading
importwx
importCheckersModel
importWxUtils
Ourapplicationclass,Checkers,isasubclassofwx.Frame,whichrepresentsanormalwindow(notadialog).WeinitializeitwithaninstanceofCheckersModel,andawindowtitle(Checkersbydefault).Herearethedeclarationsoftheclassandthe__init__method:
classCheckers(wx.Frame):
def__init__(self,checkersModel,title='Checkers'):
WewillalsostoreCheckersModelinamembervariable,likethis:
self._checkersModel=checkersModel
Theimplementationoftheinitializercontinuesinthefollowingsubsections.WewillseehowtheapplicationlaysouttheGUI,handlesevents,andinteractswithCheckersModel.
CreatingawindowandbindingeventsWewillcreateawindowbyinitializingthewx.Framesuperclass.Ratherthanusethedefaultwindowstyle,wewillspecifyacustomstylethatdoesnotallowthewindowtoberesized.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-levelclasscalledEvtHandler.Themethodregistersagivencallbackfunction(handler)foragiventypeofGUIevent(event).Whentheobjectreceivesaneventofthegiventype,thecallbackisinvoked.Forexample,let’saddthefollowinglineofcodetoensurethatagivenmethodiscalledwhenthewindowisclosed:
self.Bind(wx.EVT_CLOSE,self._onCloseWindow)
TheprecedingcallbackisimportantbecauseourCheckersclassneedstodosomecustomcleanupasitcloses.
Wecanalsogiveeventbindingsanidentifierandconnectthesebindingstokeyboardshortcutsviathewx.AcceleratorTableclass.Forexample,let’saddthiscodetobindacallbacktotheEsckey:
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’sstartputtingGUIelementsinthere!
CreatingandlayingoutimagesintheGUIThewx.StaticBitmapclassisaGUIelementthatdisplaysawx.Bitmap.Usingthefollowingcode,let’screateapairofStaticBitmapforourimagesofthesceneandtheboard:
self._sceneStaticBitmap=wx.StaticBitmap(self)
self._boardStaticBitmap=wx.StaticBitmap(self)
Later,wewillimplementahelpermethodtoshowtheCheckersModel'slatestimages.WewillalsocallthishelpernowtoinitializetheBitmapsofStaticBitmap:
self._showImages()
TodefinethelayoutsoftheStaticBitmapsandotherwxPythonwidgets,wemustaddthemtoakindofcollectioncalledwx.Sizer.Ithasseveraldirectandindirectsubclasses,suchaswx.BoxSizer(asimplehorizontalorverticallayout)andwx.GridSizer.Forthisproject,wewillusewx.BoxSizeronly.ThefollowingcodeputsourtwoStaticBitmapsinahorizontallayout,withthesceneimagefirst(leftmost)andtheboardimagesecond:
videosSizer=wx.BoxSizer(wx.HORIZONTAL)
videosSizer.Add(self._sceneStaticBitmap)
videosSizer.Add(self._boardStaticBitmap)
TherestoftheGUIwillconsistofbuttons,aradiobox,andsliders,allforthepurposeofenablingtheusertoconfiguretheCheckersModel.
CreatingandlayingoutcontrolsBydesign,severalpropertiesofourCheckersModelclassmustbeconfiguredinteractively.Apersonmustadjusttheseparameterswhileviewingtheresultsofboarddetectionandsquareclassification.Adjustmentsshouldbenecessaryonlyaftertheboardisinitiallydetectedandafteranymajorchangesinthelighting.
Rememberthattheboarddetectordoesnotlimititssearchtoanyrangeofrotations,anditmaychoosearotationinanyquadrant.Theusermaywishtochangetherotationtomatchhisorhersubjectiveideaofwhichwaytheboardisfacing.Usingthefollowingcode,wewillcreateabuttonlabeledRotateboardCCW,andwewillbindittoacallbackthatwillberesponsibleforadjustingtherotationbyincrementsof90degreescounterclockwise:
rotateBoardButton=wx.Button(
self,label='RotateboardCCW')
rotateBoardButton.Bind(
wx.EVT_BUTTON,
self._onRotateBoardClicked)
Alsorememberthatthedetectormayarbitrarilyfliptheboard,sinceitmakesnoassumptionaboutwhetherornotthecameraismirrored.Let’screateandbindtheFlipboardXandFlipboardYbuttonstogivetheuseradditionalcontrolovertheperspective:
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’directiontodeterminewherethetallkingpieceslie,weenabletheusertospecifytheshadows’directionusingaradiobox(asetofradiobuttons).TheboxislabeledShadowdirectionandtheoptionsareup,left,down,andright,asspecifiedinthefollowingcode:
shadowDirectionRadioBox=wx.RadioBox(
self,label='Shadowdirection',
choices=['up','left','down','right'])
shadowDirectionRadioBox.Bind(
wx.EVT_RADIOBOX,
self._onShadowDirectionSelected)
Notethattheradiobuttonsintheboxarecollectivelyboundtoonecallback,whichisinvokedwheneveranybuttonispressed.
Lastamongthecontrols,wewillprovidesliderstoconfiguretheclassifier’sthresholdvalues.Thesedefineitsexpectationsaboutthecolorcontrastindifferentkindsofsquares.
Thecodeshownherecallsahelpermethodtomakeandbindthreesliders,labeledEmptythreshold,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)
NoteElsewhereintheCheckersclass,weimplementthefollowingmethodtohelpcreatesliders:
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.Theywillbecenteredverticallyintheirsizerandwillhave8pixelsofpaddingontheirright-handside(toseparateeachcontrolfromitsneighbor).Let’sdeclarethesepropertiesonce,likethis:
controlsStyle=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT
controlsBorder=8
Usingtheseproperties,let’saddallthecontrolstoahorizontalsizer,asseeninthefollowingcode:
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)
NestinglayoutsandsettingtherootlayoutSizerscanbenested.Let’screateaverticalsizerandaddourtwoprevioussizerstoitsothatourimageswillappearfirst(topmost)andourcontrolssecond:
rootSizer=wx.BoxSizer(wx.VERTICAL)
rootSizer.Add(videosSizer)
rootSizer.Add(controlsSizer,0,wx.EXPAND|wx.ALL,
border=controlsBorder)
Thewindowshouldadoptasizerastherootofthelayoutandshouldresizeitselftofitthislayout.Thenextlineofcodedoesthis:
self.SetSizerAndFit(rootSizer)
Now,wehaveaGUIwithalayoutandsomeeventbindings,buthowdowestartrunningupdatestothecheckersanalyzer?
StartingabackgroundthreadTheCheckersModelclassencapsulatesalloftheheavyprocessinginthisproject,particularlythecaptureandanalysisofimages.Ifweranitsupdate()methodonthemainthread(whichwxPythonusesforGUIevents),theGUIwouldbecomeunresponsive,becauseupdate()wouldhogtheprocessorallthetime.Thus,wemustcreateabackgroundthreadthatupdate()maysafelymonopolize.ThefollowingcodeusesPython’sstandardthreading.Threadclasstorunagivenfunctiononanewthread:
self._captureThread=threading.Thread(
target=self._runCaptureLoop)
self._running=True
self._captureThread.start()
Notethatweinitializedamembervariable,_running,beforewestartedthethread.Later,intheimplementationofthethread’sfunction,wewillusethe_runningvariabletocontroltheterminationofaloop.
Alittlelater,wewillseetheimplementationofthe_runCaptureLoop()method,whichwehaveassignedtorunonthebackgroundthread.First,let’slookattheimplementationsofthecallbackmethodsthatwehaveboundtovariousGUIevents.
ClosingawindowandstoppingabackgroundthreadWhenthewindowcloses,weneedtoensurethatthebackgroundthreadterminatesnormally,meaningthatthethread’sfunctionmustreturn.Thethreading.Threadclasshasamethodcalledjoin()thatblocksthecaller’sthreaduntilthecalleethreadreturns.Thus,itallowsustowaitforanotherthread’scompletion.Aswewillseelater,ourbackgroundthreadcontinuesuntil_runningisFalse,sowemustsetthisbeforecallingjoin().Finally,wemustcleanupthewx.FrameobjectbycallingitsDestroy()method.Hereisthecodefortherelevantcallback:
def_onCloseWindow(self,event):
self._running=False
self._captureThread.join()
self.Destroy()
NoteAlleventcallbacksinwxPythonrequireaneventargument.Dependingonthetypeofevent,theeventargumentmayhavepropertiesthatgivedetailsabouttheuser’sinputorothercircumstances.
Thewindowwillclose(andtheprecedingcallbackwillbecalled)whentheuserclicksonthestandardclosebutton(X),orwhenwecallthewx.FrameobjectsClose()method.RememberthatwewantthewindowtoclosewhentheuserpressestheEsckey.Thus,theEsckeyismappedtothefollowingcallback:
def_onQuitCommand(self,event):
self.Close()
Therestofourcallbacksrelatetocontrolsthatrepresenttheanalyzer’sconfiguration.
ConfiguringtheanalyzerbasedonuserinputRememberthattheCheckersModelclasshasaboardRotationproperty.Thisisanintegerintherangeof0to3,representingcounterclockwiserotationin90-degreeincrements.Theproperty’ssetterusesamodulusoperatortoensurethatavalueof4circlesbackto0,andsoon.Thus,whentheuserclicksontheGUI’sRotateboardCCWbutton,wecansimplyincrementboardRotation,likethis:
def_onRotateBoardClicked(self,event):
self._checkersModel.boardRotation+=1
Similarly,whentheuserclicksontheFlipboardXorFlipboardYbutton,wecannegatethevalueoftheflipBoardXorflipBoardYproperty(rememberthatthesepropertiesarebooleans).Herearetherelevantcallbacks:
def_onFlipBoardXClicked(self,event):
self._checkersModel.flipBoardX=\
notself._checkersModel.flipBoardX
def_onFlipBoardYClicked(self,event):
self._checkersModel.flipBoardY=\
notself._checkersModel.flipBoardY
LiketheboardRotationproperty,theshadowDirectionpropertyisanintegerthatrepresentsacounterclockwiserotationin90-degreeincrements.TheoptionsintheShadowdirectioncontrolboxarearrangedinthesameorder(up,right,down,andleft).Conveniently,wxPythongivestheselectedoption’sindextothecallbackinevent.Selection.WecanassignthisindextotheshadowDirectionproperty,asseeninthefollowingcode:
def_onShadowDirectionSelected(self,event):
self._checkersModel.shadowDirection=event.Selection
Thethresholdproperties(emptyFreqThreshold,playerDistThreshold,andshadowDistThreshold)arefloating-pointnumbersintherangeof0.0to1.0.Bydefault,wxPython’sslidersinterprettheuser’sinputasanintegerintherangeof0to100.Thus,whentheusermovesaslider,wechangethecorrespondingthresholdtoone-hundredthoftheslider’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,theeffectontheclassificationshouldbevisibleinrealtime,orwithonlyamomentarylag.Let’sconsiderhowtoupdateanddisplaytheresults.
UpdatingandshowingimagesOnthebackgroundthread,theapplicationwillcontinuallyaskitsCheckersModelinstancetoupdatetheimagesandtheanalysis.Wheneveranupdatesucceeds,theapplicationmustalteritsGUItoshowthenewimages.ThisGUIeventmustbeprocessedonthemain(GUI)thread;otherwise,theapplicationwillcrash,becausetwothreadscannotaccesstheGUIobjectsatonce.Conveniently,wxPythonprovidesafunctioncalledwx.CallAfter(callableObj)tocallatargetfunction(orsomeothercallableobject)onthemainthread.Thefollowingcodeimplementsourbackgroundthread’sloop,whichterminateswhen_runningisFalse:
def_runCaptureLoop(self):
whileself._running:
ifself._checkersModel.update():
wx.CallAfter(self._showImages)
Onthemainthread,wegettheCheckersModelimagesoftheunprocessedsceneandtheprocessedboard,convertthemtowxPython’sBitmapformatusingourutilityfunction,anddisplaythemintheapplication’sStaticBitmap.Thesetwomethodscarryoutthiswork:
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,butwestillneedonemorefunctionintheCheckersmodule.
RunningtheapplicationLet’swriteamain()functiontolaunchaninstanceoftheCheckersapplicationclass.Thisfunctionmustalsocreatetheapplication’sinstanceofCheckersModel.Wewillallowtheusertospecifythecameraindexasacommand-lineargument.Forexample,iftheuserrunsthefollowingcommand,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-worldconditionsAlthoughthisprojectshouldworkwithmanycheckerssets,cameraperspectives,andlightingsetups,itstillrequirestheusertosetthestagecarefully,inaccordancewiththefollowingguidelines:
1. Ensurethatthewebcamandtheboardarestationary.Thewebcamshouldbeonatripodorsomeothermount.Theboardshouldalsobesecuredinplace,oritshouldbesufficientlyheavysothatitdoesnotmovewhentheplayerstouchit.
2. Leavetheboardemptyuntiltheapplicationdetectsitanddisplaysthebird’s-eyeview.Ifthereareplayingpiecesontheboard,theywillprobablyinterferewiththeinitialdetection.
3. Forbestresults,useablack-and-whitecheckerboard.Othercolorschemes(suchasdarkwoodandlightwood)maywork.
4. Forbestresults,useaboardwithalightborderaroundtheplayingarea.5. Also,forbestresults,useaboardwithamattefinishornofinish.Reflectionsona
glossyboardwillprobablyinterferewiththeanalysisofthedominantcolorsinsomesquares.
6. Puttheplayingpiecesonthedarksquares.7. Useplayingpiecesthatarenotofthesamecolorasthedarksquares.Forexample,on
ablackandwhiteboard,useredandwhite(orredandgray)pieces.8. Useplayingpiecesthataresufficientlytall.Aking(apairofstackedpieces)must
castashadowonanadjacentsquare.9. Ensurethatthescenehasbrightlightcomingpredominantlyfromonedirection,such
asdaylightfromawindow.Alsoensurethattheboardisalignedsuchthatthelightisapproximatelyparalleltothexorygridlines.Thus,akings’shadowwillfallonanadjacentlightsquare.
Experimentwithvariousreal-worldconditionsandvarioussettingsintheapplicationtoseewhatworksandwhatfails.Everycomputervisionsystemhaslimits.Likeaperson,itdoesnotseeeverythingclearly,anditdoesnotunderstandallthatitsees.Aspartofourtestingprocess,weshouldalwaysstrivetofindthesystem’slimits.Thiswilldriveustoadoptorinventmorerobusttechniquesforourfuturework.
FurtherreadingonOpenCVOurcheckersapplicationgivesaglimpseoftheworldofcomputervision.Yet,thereismuchmoretosee!ForPythonprogrammers,thefollowingbooksfromPacktPublishingrevealabroadrangeofOpenCV’sfunctionality,withimpressiveapplications:
LearningOpenCV3ComputerVisionwithPython(October2015),byJoeMinichinoandJosephHowse:ThisisagrandtourofOpenCV’sPythoninterfaceandtheunderlyingtheoriesincomputervision,machinelearning,andartificialintelligence.Withagentlelearningcurve,itissuitableforeitherbeginnersorthosewhowanttoroundouttheirknowledgeofthelibraryanditsuses.RaspberryPiComputerVisionProgramming(May2015),byAshwinPajankar:Thisbeginner-friendlybookemphasizestechniquesthatarepracticalforlow-cost,low-poweredplatformssuchastheRaspberryPisingle-boardcomputers.OpenCVforSecretAgents(January,2015),byJosephHowse:Thisisacollectionofcreative,adventurousprojectsforintermediatetoadvanceddeveloperswhomaybenewtocomputervision.Ifyouwanttoperformabiometricrecognitionofacatorseepeople’sheartbeatsthroughamotion-amplifyingwebcam,thenthisisthe(only)bookforyou!
Besidestheseoptions,PacktPublishingoffersmorethanadozenotherbooksonOpenCVandcomputervisionforC++,Java,Python,iOS,andAndroiddevelopers.Wehopetomeetyouagaininanexplorationofthisexcitingandburgeoningtopic!
SummaryWhileanalyzingagameofcheckers,weencounteredseveralfundamentalaspectsofcomputervisioninthefollowingtasks:
CapturingimagesfromacameraPerformingcomputationsoncolorandgrayscaledataDetectingandrecognizingasetoffeatures(thecornersoftheboard’ssquares)TrackingmovementsofthefeaturesSimulatingadifferentperspective(abird’s-eyeviewoftheboard)Classifyingtheregionsoftheimage(thecontentsoftheboard’ssquares)
WealsosetupandusedOpenCVandotherlibraries.Havingbuiltacompleteapplicationusingtheselibraries,youareinabetterpositiontounderstandthepotentialofcomputervisionandplanfurtherstudiesandprojectsinthisfield.
ThischapteralsoconcludesourjourneytogetherinPythonGameProgrammingbyExample.Webuiltmodernversionsofclassicvideogamesaswellasananalyzerforaclassicboardgame.Alongtheway,yougainedpracticalexperienceinmanyofthemajorgameengines,graphicsAPIs,GUIframeworks,andscientificlibrariesforPython.Theseskillsaretransferabletootherlanguagesandlibraries,andwillserveasyourfoundationagainandagainasyoubuildyourowngamesandotherpiecesofvisualsoftware!
Gocreatesomemoregreatprojects,sharethefun,andstayintouchwithus(AlejandroRodasdePazat<[email protected]>andJosephHowseathttp://nummist.com/)!
IndexA
AARectShapeclass/ProcessingcollisionsActor/SpaceInvadersdesignadd(obj)method/Processingcollisionsadd_ballmethod/AddingtheBreakoutitemsadd_brickmethod/AddingtheBreakoutitemsanalyzer
building/Buildingtheanalyzeraccess,providingtoimages/Providingaccesstotheimagesandclassificationresultsaccess,providingtoclassificationresults/Providingaccesstotheimagesandclassificationresultsaccess,providingtoparametersforuserconfiguration/Providingaccesstoparametersfortheusertoconfigureentiregamemodel,initializing/Initializingtheentiremodelofthegameentiregamemodel,updating/Updatingtheentiremodelofthegameimage,capturing/Capturingandconvertinganimageimage,converting/Capturingandconvertinganimageboard’scorners,detecting/Detectingtheboard’scornersandtrackingtheirmotionboard’smotion,tracking/Detectingtheboard’scornersandtrackingtheirmotionbird’s-eyeview,creating/Creatingandanalyzingthebird’s-eyeviewoftheboarddominantcolors,analyzinginsquare/Analyzingthedominantcolorsinasquarecontentsofasquare,classifying/Classifyingthecontentsofasquaretext,drawing/Drawingtext
ApplePython/MacArbiter/Addingphysicsarrivalbehavior/Arrival
Bball
movement/Movementandcollisionscollisions/Movementandcollisions
Ballclass/TheBallclassbasicgameobjects
adding,togamelayer/ThegamelayerBreakoutgame
overview/AnoverviewofBreakoutplaying/PlayingBreakout
Breakoutitemsadding/AddingtheBreakoutitemsgameobjectinstantiation/AddingtheBreakoutitemskeyinputbinding/AddingtheBreakoutitems
Brickclass/TheBrickclass
Ccameras
configuring/ConfiguringcamerasCanvaswidget
about/DivingintotheCanvaswidgetcanvaswidget
canvas.coords()method/DivingintotheCanvaswidgetcanvas.move()method/DivingintotheCanvaswidgetcanvas.delete()method/DivingintotheCanvaswidgetcanvas.winfo_width()method/DivingintotheCanvaswidgetcanvas.itemconfig()method/DivingintotheCanvaswidgetcanvas.bind()method/DivingintotheCanvaswidgetcanvas.unbind()method/DivingintotheCanvaswidgetcanvas.create_text()method/DivingintotheCanvaswidgetcanvas.find_withtag()method/DivingintotheCanvaswidgetcanvas.find_overlapping()method/DivingintotheCanvaswidgetreferences,URL/DivingintotheCanvaswidget
Checkersapplicationplanning/PlanningtheCheckersapplication
check_collisionsmethod/StartingthegameChimpunk2D
URL/IntroducingPymunkCircleShapeclass/Processingcollisionsclear()method/ProcessingcollisionsCocos2d
actions/Cocos2dactionscocos2d
installing/Installingcocos2dabout/Gettingstartedwithcocos2dscene/Gettingstartedwithcocos2dlayer/Gettingstartedwithcocos2dsprite/Gettingstartedwithcocos2ddirector/Gettingstartedwithcocos2dclassmembers/Gettingstartedwithcocos2dfullscreenparameter/Gettingstartedwithcocos2dresizableparameter/Gettingstartedwithcocos2dvsyncparameter/Gettingstartedwithcocos2dwidthparameter/Gettingstartedwithcocos2dheightparameter/Gettingstartedwithcocos2dcaptionparameter/Gettingstartedwithcocos2dvisibleparameter/Gettingstartedwithcocos2duserinput,handling/Handlinguserinputscene,updating/Updatingthescene
collisions,processing/ProcessingcollisionsShootactor/Shoot’emup!HUD,adding/AddinganHUDmysteryship/Extrafeature–themysteryship
Cocos2dactionsabout/Cocos2dactionsinstantactions/Cocos2dactions,Instantactionsintervalactions/Intervalactionscombining/Combiningactionscustomactions/Customactions
CocosNode/Gettingstartedwithcocos2dcollisiondetectiongame
about/BasiccollisiondetectiongameCollisionManagerinterface
about/ProcessingcollisionsCollisionManagerBruteForce/ProcessingcollisionsCollisionManagerGrid/Processingcollisions
colorsworkingwith/Workingwithcolors
componentbaseclass/Component-basedgameenginesCshape/Processingcollisionscustomactions
about/Customactions
Ddistanceformulation
CentOS,settingupTopicnURL/WorkingwithcolorsDomain-specificLanguage(DSL)/Thescenariodefinitiondraw_textmethod/AddingtheBreakoutitems
Ffaceculling
about/TheCubeclassenabling/Enablingfaceculling
fleebehavior/Seekandfleeframespersecond(FPS)/Pygame101freeglut/Installingpackagesfutureposition/Pursuitandevade
Ggame
starting/Startingthegamegameassets
creating/CreatinggameassetsGameclass/TheGameclassgamedesign
about/Anintroductiontogamedesignleveldesign/Leveldesignplatformerskills/Platformerskillscomponent-basedgameengines/Component-basedgameengines
gameframeworkbuilding/Buildingagameframeworkstart()method/Buildingagameframeworkupdate(dt)method/Buildingagameframeworkon_collide(other,contacts)method/Buildingagameframeworkstop()method/Buildingagameframeworkphysics,adding/Addingphysicsrenderablecomponents/RenderablecomponentsInputManagermodule/TheInputManagermoduleGameclass/TheGameclass
gamelayerbasicgameobjects,addingto/Thegamelayer
GameLayerclass/ThePlayerCannonandGameLayerclassesgameobjects
about/BasicgameobjectsBallclass/TheBallclassPaddleclass/ThePaddleclassBrickclass/TheBrickclass
GameObjects/Component-basedgameenginesgamescene
about/GamesceneHUDclass/TheHUDclassassembling/Assemblingthescene
glClear()function/DrawingshapesglClearColor()function/InitializingthewindowglColorfunctions/InitializingthewindowglLightfv()function/DrawingshapesglLoadIdentity()function/DrawingshapesglMaterialfv()function/DrawingshapesglMatrixMode()function/InitializingthewindowglPopMatrix()function/DrawingshapesglPushMatrix()function/Drawingshapes
glTranslate()function/DrawingshapesgluLookAt()function/DrawingshapesgluPerspective()
function/InitializingthewindowglutDisplayFunc()function/InitializingthewindowGLUTlicensing/GettingstartedwithOpenGLglutMainLoop()function/InitializingthewindowglutSolidSphere()function/DrawingshapesglutSpecialFunc()function/Initializingthewindow,Processingtheuserinputgravitationgame
about/Gravitationgamebasicgameobjects/Basicgameobjectsplanets/Planetsandpickupspickups/Planetsandpickupsplayer/Playerandenemiesenemies/Playerandenemiesexplosions/Explosions
GUIapplicationbuilding/BuildingtheGUIapplicationwindow,creating/Creatingawindowandbindingeventsevents,binding/Creatingawindowandbindingeventsimages,layingout/CreatingandlayingoutimagesintheGUIimages,creating/CreatingandlayingoutimagesintheGUIcontrols,layingout/Creatingandlayingoutcontrolscontrols,creating/Creatingandlayingoutcontrolslayouts,nesting/Nestinglayoutsandsettingtherootlayoutrootlayout,setting/Nestinglayoutsandsettingtherootlayoutbackgroundthread,starting/Startingabackgroundthreadwindow,closing/Closingawindowandstoppingabackgroundthreadbackgroundthread,stopping/Closingawindowandstoppingabackgroundthreadanalyzerbasedonuserinput,configuring/Configuringtheanalyzerbasedonuserinputimages,updating/Updatingandshowingimagesimages,displaying/Updatingandshowingimagesrunning/Runningtheapplication
GUIlayout/ThebasicGUIlayout
Iinstallation
Python/InstallingPythoninstallation,NumPy/NumPyinstallationinstantactions
about/Cocos2dactions,InstantactionsPlace/InstantactionsCallFunc/InstantactionsCallFuncS/InstantactionsHide/InstantactionsShow/InstantactionsToggleVisibility/Instantactions
IntegratedDevelopmentEnvironments(IDE)about/Aquickdemonstration
intervalactionsabout/IntervalactionsLerp/IntervalactionsMoveTo/IntervalactionsMoveBy/IntervalactionsJumpTo/IntervalactionsJumpBy/IntervalactionsBezier/IntervalactionsBlink/IntervalactionsRotateTo/IntervalactionsRotateBy/IntervalactionsScaleTo/IntervalactionsScaleBy/IntervalactionsFadeOut/IntervalactionsFadeIn/IntervalactionsFadeTo/IntervalactionsDelay/IntervalactionsRandomDelay/Intervalactions
invadersabout/Invaders!Alienclass/Invaders!AlienColumnclass/Invaders!AlienGroup.class/Invaders!
iter_colliding(obj)method/Processingcollisions
Kk-meansclustering/Initializingtheentiremodelofthegameknownentities/Processingcollisionsknows(obj)method/Processingcollisions
MMac
settingup/MacMacPorts
URL/Macmainmenu
adding/Addingamainmenumathmodule
about/IntervalactionsURL/Intervalactions
Menu/Addingamainmenumove()method/AddingtheBreakoutitems
NNumPy
installing/NumPyinstallationURL,forofficialbinaries/NumPyinstallationURL,forunofficialcompiledbinaries/NumPyinstallation
Oobstacleavoidancebehavior
about/ObstacleavoidanceOpenCV
settingup/SettingupOpenCVandotherdependenciesdependencies,settingup/SettingupOpenCVandotherdependenciesWindows,settingup/WindowsURL/WindowsMac,settingup/MacDebian,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintRaspbian,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintUbuntu,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintLinuxMint,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintFedora,settingup/Fedoraanditsderivatives,includingRHELandCentOSFedora,derivates/Fedoraanditsderivatives,includingRHELandCentOSRHEL,settingup/Fedoraanditsderivatives,includingRHELandCentOSCentOS,settingup/Fedoraanditsderivatives,includingRHELandCentOSOpenSUSE/OpenSUSEanditsderivativesOpenSUSE,derivates/OpenSUSEanditsderivativesmultipleversions,supporting/SupportingmultipleversionsofOpenCVreferences/FurtherreadingonOpenCV
OpenCVimagesconverting,forwxPython/ConvertingOpenCVimagesforwxPython
OpenGLabout/GettingstartedwithOpenGLwindow,initializing/Initializingthewindowshapes,drawing/Drawingshapesdemo,running/Runningthedemoprogram,refactoring/RefactoringourOpenGLprogramuserinput,processing/Processingtheuserinputused,fordrawing/DrawingwithOpenGLCubeclass/TheCubeclassfaceculling,enabling/Enablingfaceculling
OpenGLUtilityLibrary/InitializingthewindowOpenGLUtilityToolkit/GettingstartedwithOpenGLopticalflow/Detectingtheboard’scornersandtrackingtheirmotion
Ppackageinstallation
PyOpenGL/Installingpackagesfreeglut/InstallingpackagesPygame/Installingpackages
packagesinstalling/Installingpackages
Paddleclass/ThePaddleclassParticleSystemclass
about/TheParticleSystemclassdemonstrating/Aquickdemonstration
PlayerCannonclass/ThePlayerCannonandGameLayerclassesPlayerclass
about/ThePlayerclassanditscomponentsrespawn/ThePlayerclassanditscomponentsPlayerMovement/ThePlayerclassanditscomponents
projecttroubleshooting/Troubleshootingtheprojectinreal-worldconditions
pursuitbehavior/PursuitandevadePygame
about/InstallingpackagesURL/Installingpackages,Pygame101Macintosh,URL/Installingpackagesadding/AddingthePygamelibrary101/Pygame101documentation/Pygame101integration/Pygameintegration
Pygletabout/Installingcocos2deventframework/Handlinguserinput
pyglet.window.keymoduleURL/Handlinguserinput
Pymunkabout/IntroducingPymunk
pymunkpackage,classesspace/IntroducingPymunkbody/IntroducingPymunkshape/IntroducingPymunkarbiter/IntroducingPymunk
PyOpenGLabout/InstallingpackagesURL/Installingpackages,Initializingthewindow
PyPlatformer
developing/DevelopingPyPlatformerplatforms,creating/Creatingtheplatformspickups,adding/Addingpickupsshootingaction/Shooting!Playerclass/ThePlayerclassanditscomponentsclass/ThePyPlatformerclass
PyPlatformerclass/ThePyPlatformerclasspyramidalLukas-Kanade/Detectingtheboard’scornersandtrackingtheirmotionPython
installing/InstallingPythonURL/InstallingPythonpackagingecosystem/Installingcocos2dspecialmethods/Combiningactions
Python2andTkinter/InstallingPython
PythonExtensionPackagesURL/Installingpackages
Pythonmodules,CheckersapplicationCheckers.py/PlanningtheCheckersapplicationCheckersModel.py/PlanningtheCheckersapplicationWxUtils.py/PlanningtheCheckersapplicationResizeUtils.py/PlanningtheCheckersapplicationColorUtils.py/PlanningtheCheckersapplicationCVBackwardCompat.py/PlanningtheCheckersapplication
PythonPackageIndex(PyPi)/Installingcocos2d
Rrender()method/Buildingagameframeworkrenderablecomponents
about/Renderablecomponentscameracomponent/TheCameracomponent
Sscenario
definition/Thescenariodefinitionscenarioclass/Thescenarioclassscenariomodule/Thescenarioclassscenes
about/Cocos2dactionstransitionsbetween/Transitionsbetweenscenesgameovercutscene/Gameovercutscene
seekbehavior/SeekandfleeSimpleDirectMediaLayer(SDL)/PygameintegrationSingle-boardComputer(SBC)/ConvertingOpenCVimagesforwxPythonslowingarea/ArrivalSpaceInvadersdesign
about/SpaceInvadersdesignPlayerCannonclass/SpaceInvadersdesign,ThePlayerCannonandGameLayerclassesAlienclass/SpaceInvadersdesignAlienColumnclass/SpaceInvadersdesignAlienGroupclass/SpaceInvadersdesignShootclass/SpaceInvadersdesignPlayerShootclass/SpaceInvadersdesignGameLayerclass/ThePlayerCannonandGameLayerclasses
sprite/Cocos2dactionsstartmethod
glutInit()/InitializingthewindowglutInitWindowPosition()/InitializingthewindowglutInitWindowsSize()/InitializingthewindowglutCreateWindow()/InitializingthewindowglEnable()/Initializingthewindow
start_gamemethod/Startingthegamesteeringbehaviors
implementing/Implementingsteeringbehaviorsseek/Seekandfleeflee/Seekandfleearrival/Arrivalpursuit/Pursuitandevadeevade/Pursuitandevadewander/Wanderobstacleavoidance/Obstacleavoidance
steeringbehaviors,forautonomouscharactersreferencelink/Implementingsteeringbehaviors
subclasses,MenuItem
ToggleMenuItem/AddingamainmenuMultipleMenuItem/AddingamainmenuEntryMenuItem/AddingamainmenuImageMenuItem/AddingamainmenuColorMenuItem/Addingamainmenu
super(MyClass,self).__init__(arguments)/ThebasicGUIlayoutsyntacticsugar/Combiningactions
TThreadBuildingBlocks(TBB)/MacTiledMapEditor
URL/TiledMapEditorabout/TiledMapEditor
tilemapsabout/TilemapsTiledMapEditor/TiledMapEditortiles,loading/Loadingtiles
TkinterURL/InstallingPythonandPython2/InstallingPython
TMXformat/TiledMapEditortowerdefenseactors
about/Thetowerdefenseactorsturret/Turretsandslotsslots/Turretsandslotsenemies/Enemiesbunker/Bunker
towerdefensegameplayabout/Thetowerdefensegameplay
Uupdate()method/Buildingagameframeworkupdatemethod/Movementandcollisionsupdate_lives_textmethod/AddingtheBreakoutitems
Vvalues,wanderbehavior
wander_angle/Wandercircle_distance/Wandercircle_radius/Wanderangle_change/Wander
Wwanderbehavior
about/Wandervalues/Wander
WindowsURL/Windowssettingup/Windows
wxPythonURL/WindowsOpenCVimages,converting/ConvertingOpenCVimagesforwxPython
wxPythonPhoenixURL/Windows,Mac