Download Python Game Programming By Example

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

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