Python Game Programming By Example - Programmer Books · Benjamin Johnson is an experienced Python...

351

Transcript of Python Game Programming By Example - Programmer Books · Benjamin Johnson is an experienced Python...

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

PythonGameProgrammingByExampleCopyright©2015PacktPublishing

Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.

Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthors,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.

PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.

Firstpublished:September2015

Productionreference:1230915

PublishedbyPacktPublishingLtd.

LiveryPlace

35LiveryStreet

BirminghamB32PB,UK.

ISBN978-1-78528-153-2

www.packtpub.com

CreditsAuthors

AlejandroRodasdePaz

JosephHowse

Reviewers

BenjaminJohnson

DennisO’Brien

AcquisitionEditors

OwenRoberts

SonaliVernekar

ContentDevelopmentEditor

DharmeshParmar

TechnicalEditor

RyanKochery

CopyEditor

VikrantPhadke

ProjectCoordinator

HarshalVed

Proofreader

SafisEditing

Indexer

RekhaNair

Graphics

JasonMonteiro

ProductionCoordinator

ManuJoseph

CoverWork

ManuJoseph

AbouttheAuthorsAlejandroRodasdePazisacomputerengineerandgamedeveloperfromSeville,Spain.

HecameacrossPythonbackin2009,whilehewasstudyingattheUniversityofSeville.AlejandrodevelopedseveralacademicprojectswithPython,fromwebcrawlerstoartificialintelligencealgorithms.Inhissparetime,hestartedbuildinghisowngamesinPython.HedidaminoringamedesignatHogeschoolvanAmsterdam,wherehecreatedasmall3Dgameenginebasedontheideashelearnedduringthisminor.

Hehasalsodevelopedsomeopensourceprojects,suchasaPythonAPIforthePhilipsHuepersonallightingsystem.YoucanfindtheseprojectsinhisGitHubaccountathttps://github.com/aleroddepaz.

Priortothispublication,AlejandrocollaboratedwithPacktPublishingasatechnicalrevieweronthebookTkinterGUIApplicationDevelopmentHotshot.

Iwouldliketothankmyparents,FelicianoandMaríaTeresa,fortheirabsolutetrustandsupport.Theyhavebeenaninspirationtomeandanexampleofhardwork.

Iwouldalsoliketothankmygirlfriend,Lucía,forherloveandforputtingupwithmewhileIworkedonthisbook.

JosephHowseisawriter,softwaredeveloper,andbusinessownerfromHalifax,NovaScotia,Canada.Computergamesandcodeareimbibedinhisearliestmemories,ashelearnedtoreadandtypebyplayingtextadventureswithhisolderbrother,Sam,andwatchinghimwritegraphicsdemosinBASIC.

Joseph’sotherbooksincludeOpenCVforSecretAgents,OpenCVBlueprints,AndroidApplicationProgrammingwithOpenCV3,andLearningOpenCV3ComputerVisionwithPython.Heworkswithhiscatstomakecomputervisionsystemsforhumans,felines,andotherusers.Visithttp://nummist.comtoreadaboutsomeofhislatestprojectsdoneatNummistMediaCorporationLimited.

IdedicatemyworktoSam,Jan,Bob,Bunny,andmycats,whohavebeenmylifelongguidesandcompanions.

Icongratulatemycoauthorforproducinganexcellentcompendiumofclassicexamplesofgamedevelopment.Iamgratefulfortheopportunitytoaddmychapteroncheckers(draughts)andcomputervision.

Iamalsoindebtedtothemanyeditorsandtechnicalreviewerswhohavecontributedtoplanning,polishing,andmarketingthisbook.IhavecometoexpectanoutstandingteamwhenworkingwithPacktPublishing,andonceagain,allofthemhaveguidedmewiththeirexperienceandsavedmefromsundryerrorsandomissions.Pleasemeetthetechnicalreviewersbyreadingtheirbiographieshere.

Finally,Iwanttothankmyreadersandeverybodyintheopensourcecommunity.Weareunitedinoureffortstobuildandshareallkindsofprojectsandknowledge,pavingthewayforbookssuchasthistosucceed.

AbouttheReviewersBenjaminJohnsonisanexperiencedPythonprogrammerwithapassionforgameprogramming,softwaredevelopment,andwebdesign.HeiscurrentlystudyingcomputerscienceatTheUniversityofTexasatAustinandplanstospecializeinsoftwareengineering.HismostpopularPythonprojectsincludeanadventuregameengineandaparticlesimulator,bothdevelopedusingPygame.YoucancheckoutBenjamin’slatestPygameprojectsandarticlesonhiswebsiteatwww.learnpygame.com.

IwouldliketothankPacktPublishingforgivingmetheopportunitytoreadandreviewthisexcellentbook!

DennisO’BrienisthedirectorofdatascienceatGameShowNetworkGames.HestudiedphysicsattheUniversityofChicagoasanundergraduateandcompletedhisgraduatestudiesincomputersciencefromtheUniversityofIllinois,Chicago.HewastheprincipalsoftwareengineeratElectronicArts,aseniorsoftwareengineeratLeapfrogEnterprises,andaleadgamedeveloperatJellyvisionGames.

www.PacktPub.com

Supportfiles,eBooks,discountoffers,andmoreForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.

DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>formoredetails.

Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.

https://www2.packtpub.com/books/subscription/packtlib

DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks.

Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser

FreeaccessforPacktaccountholdersIfyouhaveanaccountwithPacktatwww.PacktPub.com,youcanusethistoaccessPacktLibtodayandview9entirelyfreebooks.Simplyuseyourlogincredentialsforimmediateaccess.

PrefaceWelcometoPythonGameProgrammingByExample.Ashobbyistprogrammersorprofessionaldevelopers,wemaybuildawidevarietyofapplications,fromlargeenterprisesystemstowebapplicationsmadewithstate-of-the-artframeworks.However,gamedevelopmenthasalwaysbeenanappealingtopic,maybesimplyforcreatingcasualgamesandnotjustforhigh-budgetAAAtitles.

IfyouwanttoexplorethedifferentwaysofdevelopinggamesinPython,alanguagewithclearandsimplesyntax,thenthisisthebookforyou.Ineachchapter,wewillbuildanewgamefromscratch,usingseveralpopularlibrariesandutilities.Bytheendofthisbook,youwillbeabletoquicklycreateyourown2Dand3Dgames,andhaveahandfulofPythonlibrariesinyourtoolbelttochoosefrom.

WhatthisbookcoversChapter1,Hello,Pong!,detailstherequiredsoftware,itsinstallation,andthebasicsyntaxofPython:datastructures,controlflowstatements,objectorientation,andsoon.Italsoincludesthefirstgameofthebook,theclassic“Hello,world”game.

Chapter2,CocosInvaders,introducesthecocos2dgameengineandexplainshowtobuildagamesimilartoSpaceInvaderstoputthisknowledgeintopractice.Here,youlearnthebasicsofcollisions,inputhandling,andscenesetup.

Chapter3,BuildingaTowerDefenseGame,iswhereyoulearntodevelopafull-fledgedgamewithcocos2d.Thisgameincludessomeinterestingcomponents,suchasaHUDandamainmenu.

Chapter4,SteeringBehaviors,coversseeminglyintelligentmovementsforautonomouscharacters.Youwillbeaddingthesestrategiesgradually,indifferentlevelsofabasicgamebuiltwithparticlesystems.

Chapter5,Pygameand3D,presentsthefoundationsof3DandguidesyouthroughthebasicstructureofanOpenGLprogram.

Chapter6,PyPlatformer,iswhereyoudevelopa3Dplatformergamewithallthetechniqueslearnedinthepreviouschapter.

Chapter7,AugmentingaBoardGamewithComputerVision,introducesthetopicofcomputervision,whichallowssoftwaretolearnabouttherealworldviaacamera.Inthischapter,youbuildasystemtoanalyzeagameofcheckers(draughts)inrealtimeasplayersmovepiecesonaphysicalboard.

WhatyouneedforthisbookTheprojectscoveredinthisbookassumethatyouhaveinstalledPython3.4onacomputerwithWindows,MacOSX,orLinux.Wealsoassumethatyouhaveincludedpipduringtheinstallationprocess,sinceitwillbethepackagemanagerusedtoinstalltherequiredthird-partypackages.

WhothisbookisforIfyouhaveeverwantedtocreatecasualgamesinPythonandyouwishtoexplorethevariousGUItechnologiesthatthislanguageoffers,thenthisisthebookforyou.ThistitleisintendedforbeginnersinPythonwithlittleornoknowledgeofgamedevelopment,anditcoversstepbystephowtobuildsevendifferentgames,fromthewell-knownSpaceInvaderstoaclassical3Dplatformer.

ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.

Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“Forinstance,onUbuntu,youneedtoinstallthepython3-tkpackage.”

Ablockofcodeissetasfollows:

new_list=[]

forelemincollection:

ifelemisnotNone:

new_list.append(elem)

Anycommand-lineinputoroutputiswrittenasfollows:

$python–-version

Python3.4.3

Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:“MakesurethatyouchecktheTcl/Tkoptiontoincludethelibrary.”

NoteWarningsorimportantnotesappearinaboxlikethis.

TipTipsandtricksappearlikethis.

ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.

Tosendusgeneralfeedback,simplye-mail<[email protected]>,andmentionthebook’stitleinthesubjectofyourmessage.

Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.

CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.

DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.comforallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.

Additionally,up-to-dateexamplecodeforChapter7,AugmentingaBoardGamewithComputerVision,ispostedathttp://nummist.com/opencv.

DownloadingthecolorimagesofthisbookWealsoprovideyouwithaPDFfilethathascolorimagesofthescreenshots/diagramsusedinthisbook.Thecolorimageswillhelpyoubetterunderstandthechangesintheoutput.Youcandownloadthisfilefromhttps://www.packtpub.com/sites/default/files/downloads/B04505_Graphics.pdf.

ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.

Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.

Additionally,anyerrataforChapter7,AugmentingaBoardGamewithComputerVision,willbepostedathttp://nummist.com/opencv.

PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.

Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.

Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.

QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<[email protected]>,andwewilldoourbesttoaddresstheproblem.

Youcanalsocontacttheauthorsdirectly.AlejandraRodasdePaz,authorofChapters1to6,canbereachedat<[email protected]>.JosephHowse,authorofChapter7,canbereachedat<[email protected]>,andanswerstocommonquestionscanbefoundonhiswebsite,http://nummist.com/opencv.

Chapter1.Hello,Pong!Gamedevelopmentisahighlyevolvingsoftwaredevelopmentprocess,andithasimprovedcontinuouslysincetheappearanceofthefirstvideogamesinthe1950s.Nowadays,thereareawidevarietyofplatformsandengines,andthisprocesshasbeenfacilitatedwiththearrivalofopensourcetools.

Pythonisafreehigh-levelprogramminglanguagewithadesignintendedtowritereadableandconciseprograms.Thankstoitsphilosophy,wecancreateourowngamesfromscratchwithjustafewlinesofcode.ThereareaplentyofgameframeworksforPython,butforourfirstgame,wewillseehowwecandevelopitwithoutanythird-partydependency.

Inthischapter,wewillcoverthefollowingtopics:

InstallationoftherequiredsoftwareAnoverviewofTkinter,aGUIlibraryincludedinthePythonstandardlibraryApplyingobject-orientedprogrammingtoencapsulatethelogicofourgameBasiccollisionandinputdetectionDrawinggameobjectswithoutexternalassetsDevelopingasimplifiedversionofBreakout,apong-basedgame

InstallingPythonYouwillneedPython3.4withTcl/Tk8.6installedonyourcomputer.ThelatestbranchofthisversionisPython3.4.3,whichcanbedownloadedfromhttps://www.python.org/downloads/.Here,youcanfindtheofficialbinariesforthemostpopularplatforms,suchasWindowsandMacOS.Duringtheinstallationprocess,makesurethatyouchecktheTcl/Tkoptiontoincludethelibrary.

ThecodeexamplesincludedinthebookhavebeentestedagainstWindows8andMac,butcanberunonLinuxwithoutanymodification.NotethatsomedistributionsmayrequireyoutoinstalltheappropriatepackageforPython3.Forinstance,onUbuntu,youneedtoinstallthepython3-tkpackage.

OnceyouhavePythoninstalled,youcanverifytheversionbyopeningCommandPromptoraterminalandexecutingtheselines:

$python--version

Python3.4.3

Afterthischeck,youshouldbeabletostartasimpleGUIprogram:

$python

>>>fromtkinterimportTk

>>>root=Tk()

>>>root.title('Hello,world!')

>>>root.mainloop()

Thesestatementscreateawindow,changeitstitle,andrunindefinitelyuntilthewindowisclosed.Donotclosethenewwindowthatisdisplayedwhenthesecondstatementisexecuted.Otherwise,itwillraiseanerrorbecausetheapplicationhasbeendestroyed.

Wewillusethislibraryinourfirstgame,andthecompletedocumentationofthemodulecanbefoundathttps://docs.python.org/3/library/tkinter.html.

TipTkinterandPython2

TheTkintermodulewasrenamedtotkinterinPython3.IfyouhavePython2installed,simplychangetheimportstatementwithTkinterinuppercase,andtheprogramshouldrunasexpected.

AnoverviewofBreakoutTheBreakoutgamestartswithapaddleandaballatthebottomofthescreenandsomerowsofbricksatthetop.Theplayermusteliminateallthebricksbyhittingthemwiththeball,whichreboundsagainstthebordersofthescreen,thebricks,andthebottompaddle.AsinPong,theplayercontrolsthehorizontalmovementofthepaddle.

Theplayerstartsthegamewiththreelives,andiftheymisstheball’sreboundanditreachesthebottomborderofthescreen,onelifeislost.Thegameisoverwhenallthebricksaredestroyed,orwhentheplayerlosesalltheirlives.

Thisisascreenshotofthefinalversionofourgame:

ThebasicGUIlayoutWewillstartoutgamebycreatingatop-levelwindowasinthesimpleprogramweranpreviously.However,thistime,wewillusetwonestedwidgets:acontainerframeandthecanvaswherethegameobjectswillbedrawn,asshownhere:

WithTkinter,thiscaneasilybeachievedusingthefollowingcode:

importtkinterastk

lives=3

root=tk.Tk()

frame=tk.Frame(root)

canvas=tk.Canvas(frame,width=600,height=400,bg='#aaaaff')

frame.pack()

canvas.pack()

root.title('Hello,Pong!')

root.mainloop()

TipDownloadingtheexamplecode

Youcandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.comforallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.

Throughthetkalias,weaccesstheclassesdefinedinthetkintermodule,suchasTk,Frame,andCanvas.

Noticethefirstargumentofeachconstructorcallwhichindicatesthewidget(thechildcontainer),andtherequiredpack()callsfordisplayingthewidgetsontheirparentcontainer.ThisisnotnecessaryfortheTkinstance,sinceitistherootwindow.

However,thisapproachisnotexactlyobject-oriented,sinceweuseglobalvariablesanddonotdefineanynewclassestorepresentournewdatastructures.Ifthecodebasegrows,thiscanleadtopoorlyorganizedprojectsandhighlycoupledcode.

Wecanstartencapsulatingthepiecesofourgameinthisway:

importtkinterastk

classGame(tk.Frame):

def__init__(self,master):

super(Game,self).__init__(master)

self.lives=3

self.width=610

self.height=400

self.canvas=tk.Canvas(self,bg='#aaaaff',

width=self.width,

height=self.height)

self.canvas.pack()

self.pack()

if__name__=='__main__':

root=tk.Tk()

root.title('Hello,Pong!')

game=Game(root)

game.mainloop()

Ournewtype,calledGame,inheritsfromtheFrameTkinterclass.TheclassGame(tk.Frame):definitionspecifiesthenameoftheclassandthesuperclassbetweenparentheses.

Ifyouarenewtoobject-orientedprogrammingwithPython,thissyntaxmaynotsoundfamiliar.Inourfirstlookatclasses,themostimportantconceptsarethe__init__methodandtheselfvariable:

The__init__methodisaspecialmethodthatisinvokedwhenanewclassinstanceiscreated.Here,wesettheobjectattributes,suchasthewidth,theheight,andthecanvaswidget.Wealsocalltheparentclassinitializationwiththesuper(Game,self).__init__(master)statement,sotheinitialstateoftheFrameisproperlyinitialized.Theselfvariablereferstotheobject,anditshouldbethefirstargumentofamethodifyouwanttoaccesstheobjectinstance.Itisnotstrictlyalanguagekeyword,butthePythonconventionistocallitselfsothatotherPythonprogrammerswon’tbeconfusedaboutthemeaningofthevariable.

Intheprecedingsnippet,weintroducedtheif__name__=='__main__'condition,whichispresentinmanyPythonscripts.Thissnippetchecksthenameofthecurrentmodulethatisbeingexecuted,andwillpreventstartingthemainloopwherethismodulewasbeingimportedfromanotherscript.Thisblockisplacedattheendofthescript,sinceitrequiresthattheGameclassbedefined.

Tip

New-andold-styleclasses

YoumayseetheMySuperClass.__init__(self,arguments)syntaxinsomePython2examples,insteadofthesupercall.Thisistheold-stylesyntax,theonlyflavoravailableuptoPython2.1,andismaintainedinPython2forbackwardcompatibility.

Thesuper(MyClass,self).__init__(arguments)isthenew-classstyleintroducedinPython2.2.Itisthepreferredapproach,andwewilluseitthroughoutthisbook.

Seethechapter1_01.pyscript,whichcontainsthiscode.Sincenoexternalassetsareneeded,youcanplaceitinanydirectoryandexecuteitfromthePythoncommandlinebyrunningchapter1_01.py.Themainloopwillrunindefinitelyuntilyouclickontheclosebuttonofthewindow,oryoukilltheprocessfromthecommandline.

Thisisthestartingpointofourgame,solet’sstartdivingintotheCanvaswidgetandseehowwecandrawandanimateitemsinit.

DivingintotheCanvaswidgetSofar,wehavethewindowsetupandnowwecanstartdrawingitemsonthecanvas.TheCanvaswidgetistwo-dimensionalandusestheCartesiancoordinatesystem.Theorigin—the(0,0)orderedpair—isplacedinthetop-leftcorner,andtheaxiscanberepresentedasshowninthefollowingscreenshot:

Keepingthislayoutinmind,wecanusetwomethodsoftheCanvaswidgettodrawthepaddle,thebricks,andtheball:

canvas.create_rectangle(x0,y0,x1,y1,**options)

canvas.create_oval(x0,y0,x1,y1,**options)

Eachofthesecallsreturnsaninteger,whichidentifiestheitemhandle.Thisreferencewillbeusedlatertomanipulatethepositionoftheitemanditsoptions.The**optionssyntaxrepresentsakey/valuepairofadditionalargumentsthatcanbepassedtothemethodcall.Inourcase,wewillusethefillandthetagsoption.

Thex0andy0coordinatesindicatethetop-leftcornerofthepreviousscreenshot,andx1andy1areindicatedinthebottom-rightcorner.

Forinstance,wecancallcanvas.create_rectangle(250,300,330,320,fill='blue',tags='paddle')tocreateaplayer’spaddle,where:

Thetop-leftcornerisatthecoordinates(250,300).Thebottom-rightcornerisatthecoordinates(300,320).

Thefill='blue'meansthatthebackgroundcoloroftheitemisblue.Thetags='paddle'meansthattheitemistaggedasapaddle.Thisstringwillbeusefullatertofinditemsinthecanvaswithspecifictags.

WewillinvokeotherCanvasmethodstomanipulatetheitemsandretrievewidgetinformation.ThistablegivesthereferencestotheCanvaswidgetthatwillbeusedinthischapter:

Method Description

canvas.coords(item) Returnsthecoordinatesoftheboundingboxofanitem.

canvas.move(item,x,y) Movesanitembyahorizontalandaverticaloffset.

canvas.delete(item) Deletesanitemfromthecanvas.

canvas.winfo_width() Retrievesthecanvaswidth.

canvas.itemconfig(item,**options) Changestheoptionsofanitem,suchasthefillcolororitstags.

canvas.bind(event,callback)Bindsaninputeventwiththeexecutionofafunction.ThecallbackhandlerreceivesoneparameterofthetypeTkinterevent.

canvas.unbind(event)Unbindstheinputeventsothatthereisnocallbackfunctionexecutedwhentheeventoccurs.

canvas.create_text(*position,

**opts)

Drawstextonthecanvas.Thepositionandtheoptionsargumentsaresimilartotheonespassedincanvas.create_rectangleandcanvas.create_oval.

canvas.find_withtag(tag) Returnstheitemswithaspecifictag.

canvas.find_overlapping(*position)Returnstheitemsthatoverlaporarecompletelyenclosedbyagivenrectangle.

Youcancheckoutacompletereferenceoftheeventsyntaxaswellassomepracticalexamplesathttp://effbot.org/tkinterbook/tkinter-events-and-bindings.htm#events.

BasicgameobjectsBeforewestartdrawingallourgameitems,let’sdefineabaseclasswiththefunctionalitythattheywillhaveincommon—storingareferencetothecanvasanditsunderlyingcanvasitem,gettinginformationaboutitsposition,anddeletingtheitemfromthecanvas:

classGameObject(object):

def__init__(self,canvas,item):

self.canvas=canvas

self.item=item

defget_position(self):

returnself.canvas.coords(self.item)

defmove(self,x,y):

self.canvas.move(self.item,x,y)

defdelete(self):

self.canvas.delete(self.item)

AssumingthatwehavecreatedaCanvaswidgetasshowninourpreviouscodesamples,abasicusageofthisclassanditsattributeswouldbelikethis:

item=canvas.create_rectangle(10,10,100,80,fill='green')

game_object=GameObject(canvas,item)#createnewinstance

print(game_object.get_position())

#[10,10,100,80]

game_object.move(20,-10)

print(game_object.get_position())

#[30,0,120,70]

game_object.delete()

Inthisexample,wecreatedagreenrectangleandaGameObjectinstancewiththeresultingitem.Thenweretrievedthepositionoftheitemwithinthecanvas,movedit,andcalculatedthepositionagain.Finally,wedeletedtheunderlyingitem.

ThemethodsthattheGameObjectclassofferswillbereusedinthesubclassesthatwewillseelater,sothisabstractionavoidsunnecessarycodeduplication.Nowthatyouhavelearnedhowtoworkwiththisbasicclass,wecandefineseparatechildclassesfortheball,thepaddle,andthebricks.

TheBallclassTheBallclasswillstoreinformationaboutthespeed,direction,andradiusoftheball.Wewillsimplifytheball’smovement,sincethedirectionvectorwillalwaysbeoneofthefollowing:

[1,1]iftheballismovingtowardsthebottom-rightcorner[-1,-1]iftheballismovingtowardsthetop-leftcorner[1,-1]iftheballismovingtowardsthetop-rightcorner[-1,1]iftheballismovingtowardsthebottom-leftcorner

Arepresentationofthepossibledirectionvectors

Therefore,bychangingthesignofoneofthevectorcomponents,wewillchangetheball’sdirectionby90degrees.Thiswillhappenwhentheballbouncesagainstthecanvasborder,whenithitsabrick,ortheplayer’spaddle:

classBall(GameObject):

def__init__(self,canvas,x,y):

self.radius=10

self.direction=[1,-1]

self.speed=10

item=canvas.create_oval(x-self.radius,y-self.radius,

x+self.radius,y+self.radius,

fill='white')

super(Ball,self).__init__(canvas,item)

Fornow,theobjectinitializationisenoughtounderstandtheattributesthattheclasshas.Wewillcovertheballreboundlogiclater,whentheothergameobjectshavebeendefinedandplacedinthegamecanvas.

ThePaddleclassThePaddleclassrepresentstheplayer’spaddleandhastwoattributestostorethewidthandheightofthepaddle.Aset_ballmethodwillbeusedtostoreareferencetotheball,whichcanbemovedwiththeballbeforethegamestarts:

classPaddle(GameObject):

def__init__(self,canvas,x,y):

self.width=80

self.height=10

self.ball=None

item=canvas.create_rectangle(x-self.width/2,

y-self.height/2,

x+self.width/2,

y+self.height/2,

fill='blue')

super(Paddle,self).__init__(canvas,item)

defset_ball(self,ball):

self.ball=ball

defmove(self,offset):

coords=self.get_position()

width=self.canvas.winfo_width()

ifcoords[0]+offset>=0and\

coords[2]+offset<=width:

super(Paddle,self).move(offset,0)

ifself.ballisnotNone:

self.ball.move(offset,0)

Themovemethodisresponsibleforthehorizontalmovementofthepaddle.Stepbystep,thefollowingisthelogicbehindthismethod:

Theself.get_position()calculatesthecurrentcoordinatesofthepaddleTheself.canvas.winfo_width()retrievesthecanvaswidthIfboththeminimumandmaximumx-axiscoordinates,plustheoffsetproducedbythemovement,areinsidetheboundariesofthecanvas,thisiswhathappens:

Thesuper(Paddle,self).move(offset,0)callsthemethodwithsamenameinthePaddleclass’sparentclass,whichmovestheunderlyingcanvasitemIfthepaddlestillhasareferencetotheball(thishappenswhenthegamehasnotbeenstarted),theballismovedaswell

Thismethodwillbeboundtotheinputkeyssothattheplayercanusethemtocontrolthepaddle’smovement.WewillseelaterhowwecanuseTkintertoprocesstheinputkeyevents.Fornow,let’smoveontotheimplementationofthelastoneofourgame’scomponents.

TheBrickclassEachbrickinourgamewillbeaninstanceoftheBrickclass.Thisclasscontainsthelogicthatisexecutedwhenthebricksarehitanddestroyed:

classBrick(GameObject):

COLORS={1:'#999999',2:'#555555',3:'#222222'}

def__init__(self,canvas,x,y,hits):

self.width=75

self.height=20

self.hits=hits

color=Brick.COLORS[hits]

item=canvas.create_rectangle(x-self.width/2,

y-self.height/2,

x+self.width/2,

y+self.height/2,

fill=color,tags='brick')

super(Brick,self).__init__(canvas,item)

defhit(self):

self.hits-=1

ifself.hits==0:

self.delete()

else:

self.canvas.itemconfig(self.item,

fill=Brick.COLORS[self.hits])

Asyoumayhavenoticed,the__init__methodisverysimilartotheoneinthePaddleclass,sinceitdrawsarectangleandstoresthewidthandtheheightoftheshape.Inthiscase,thevalueofthetagsoptionpassedasakeywordargumentis'brick'.Withthistag,wecancheckwhetherthegameisoverwhenthenumberofremainingitemswiththistagiszero.

AnotherdifferencefromthePaddleclassisthehitmethodandtheattributesituses.TheclassvariablecalledCOLORSisadictionary—adatastructurethatcontainskey/valuepairswiththenumberofhitsthatthebrickhasleft,andthecorrespondingcolor.Whenabrickishit,themethodexecutionoccursasfollows:

Thenumberofhitsofthebrickinstanceisdecreasedby1Ifthenumberofhitsremainingis0,self.delete()deletesthebrickfromthecanvasOtherwise,self.canvas.itemconfig()changesthecolorofthebrick

Forinstance,ifwecallthismethodforabrickwithtwohitsleft,wewilldecreasethecounterby1andthenewcolorwillbe#999999,whichisthevalueofBrick.COLORS[1].Ifthesamebrickishitagain,thenumberofremaininghitswillbecomezeroandtheitemwillbedeleted.

AddingtheBreakoutitemsNowthattheorganizationofouritemsisseparatedintothesetop-levelclasses,wecanextendthe__init__methodofourGameclass:

classGame(tk.Frame):

def__init__(self,master):

super(Game,self).__init__(master)

self.lives=3

self.width=610

self.height=400

self.canvas=tk.Canvas(self,bg='#aaaaff',

width=self.width,

height=self.height)

self.canvas.pack()

self.pack()

self.items={}

self.ball=None

self.paddle=Paddle(self.canvas,self.width/2,326)

self.items[self.paddle.item]=self.paddle

forxinrange(5,self.width-5,75):

self.add_brick(x+37.5,50,2)

self.add_brick(x+37.5,70,1)

self.add_brick(x+37.5,90,1)

self.hud=None

self.setup_game()

self.canvas.focus_set()

self.canvas.bind('<Left>',

lambda_:self.paddle.move(-10))

self.canvas.bind('<Right>',

lambda_:self.paddle.move(10))

defsetup_game(self):

self.add_ball()

self.update_lives_text()

self.text=self.draw_text(300,200,

'PressSpacetostart')

self.canvas.bind('<space>',

lambda_:self.start_game())

Thisinitializationismorecomplexthatwhatwehadatthebeginningofthechapter.Wecandivideitintotwosections:

Gameobjectinstantiation,andtheirinsertionintotheself.itemsdictionary.Thisattributecontainsallthecanvasitemsthatcancollidewiththeball,soweaddonlythebricksandtheplayer’spaddletoit.Thekeysarethereferencestothecanvasitems,andthevaluesarethecorrespondinggameobjects.Wewillusethisattributelaterinthecollisioncheck,whenwewillhavethecollidingitemsandwillneedtofetchthegameobject.Keyinputbinding,viatheCanvaswidget.Thecanvas.focus_set()callsetsthefocusonthecanvas,sotheinputeventsaredirectlyboundtothiswidget.Thenwe

bindtheleftandrightkeystothepaddle’smove()methodandthespacebartotriggerthegamestart.Thankstothelambdaconstruct,wecandefineanonymousfunctionsaseventhandlers.SincethecallbackargumentofthebindmethodisafunctionthatreceivesaTkintereventasanargument,wedefinealambdathatignoresthefirstparameter—lambda_:<expression>.

Ournewadd_ballandadd_brickmethodsareusedtocreategameobjectsandperformabasicinitialization.Whilethefirstonecreatesanewballontopoftheplayer’spaddle,thesecondoneisashorthandwayofaddingaBrickinstance:

defadd_ball(self):

ifself.ballisnotNone:

self.ball.delete()

paddle_coords=self.paddle.get_position()

x=(paddle_coords[0]+paddle_coords[2])*0.5

self.ball=Ball(self.canvas,x,310)

self.paddle.set_ball(self.ball)

defadd_brick(self,x,y,hits):

brick=Brick(self.canvas,x,y,hits)

self.items[brick.item]=brick

Thedraw_textmethodwillbeusedtodisplaytextmessagesinthecanvas.Theunderlyingitemcreatedwithcanvas.create_text()isreturned,anditcanbeusedtomodifytheinformation:

defdraw_text(self,x,y,text,size='40'):

font=('Helvetica',size)

returnself.canvas.create_text(x,y,text=text,

font=font)

Theupdate_lives_textmethoddisplaysthenumberoflivesleftandchangesitstextifthemessageisalreadydisplayed.Itiscalledwhenthegameisinitialized—thisiswhenthetextisdrawnforthefirsttime—anditisalsoinvokedwhentheplayermissesaballrebound:

defupdate_lives_text(self):

text='Lives:%s'%self.lives

ifself.hudisNone:

self.hud=self.draw_text(50,20,text,15)

else:

self.canvas.itemconfig(self.hud,text=text)

Weleavestart_gameunimplementedfornow,sinceittriggersthegameloop,andthislogicwillbeaddedinthenextsection.SincePythonrequiresacodeblockforeachmethod,weusethepassstatement.Thisdoesnotexecuteanyoperation,anditcanbeusedasaplaceholderwhenastatementisrequiredsyntactically:

defstart_game(self):

pass

Seethechapter1_02.pymodule,ascriptwiththesamplecodewehavesofar.Ifyouexecutethisscript,itwilldisplayaTkinterwindowliketheoneshowninthefollowing

figure.Atthispoint,wecanmovethepaddlehorizontally,sowearereadytostartthegameandhitsomebricks:

MovementandcollisionsNowthatwehaveplacedallofourgameobjects,wecandefinethemethodsthatwillbeexecutedinthegameloop.Thislooprunsindefinitelyuntilthegameends,andeachiterationupdatesthepositionoftheballandchecksthecollisionthatoccurs.

WiththeCanvaswidget,wecancalculatewhattheitemsthatoverlapwiththegivencoordinatesare,sofornow,wewillimplementthemethodsthatareresponsibleformovingtheballandchangingitsdirection.

Let’sstartwiththemovementoftheballandtheconditionsforrecreatingthebouncingeffectwhenitreachesthecanvasborders:

defupdate(self):

coords=self.get_position()

width=self.canvas.winfo_width()

ifcoords[0]<=0orcoords[2]>=width:

self.direction[0]*=-1

ifcoords[1]<=0:

self.direction[1]*=-1

x=self.direction[0]*self.speed

y=self.direction[1]*self.speed

self.move(x,y)

Theupdatemethoddoesthefollowing:

Itgetsthecurrentpositionandthewidthofthecanvas.Itstoresthevaluesinthecoordsandwidthlocalvariables,respectively.Ifthepositioncollideswiththeleftorrightborderofthecanvas,thehorizontalcomponentofthedirectionvectorchangesitssignIfthepositioncollideswiththeupperborderofthecanvas,theverticalcomponentofthedirectionvectorchangesitssignWescalethedirectionvectorbytheball’sspeedTheself.move(x,y)movestheball

Forinstance,iftheballhitstheleftborder,thecoords[0]<=0conditionevaluatestotrue,sothex-axiscomponentofthedirectionchangesitssign,asshowninthisdiagram:

Iftheballhitsthetop-rightcorner,bothcoords[2]>=widthandcoords[1]<=0evaluatetotrue.Thischangesthesignofboththecomponentsofthedirectionvector,likethis:

Thelogicofthecollisionwithabrickisabitmorecomplex,sincethedirectionoftherebounddependsonthesidewherethecollisionoccurs.

Wewillcalculatethex-axiscomponentoftheball’scenterandcheckwhetheritisbetweenthelowermostanduppermostx-axiscoordinatesofthecollidingbrick.Totranslatethisintoaquickimplementation,thefollowingsnippetshowsthepossiblechangesinthedirectionvectoraspertheballandbrickcoordinates:

coords=self.get_position()

x=(coords[0]+coords[2])*0.5

brick_coords=brick.get_position()

ifx>brick_coords[2]:

self.direction[0]=1

elifx<brick_coords[0]:

self.direction[0]=-1

else:

self.direction[1]*=-1

Forinstance,thiscollisioncausesahorizontalrebound,sincethebrickisbeinghitfromabove,asshownhere:

Ontheotherhand,acollisionfromtheright-handsideofthebrickwouldbeasfollows:

Thisisvalidwhentheballhitsthepaddleorasinglebrick.However,theballcanhittwobricksatthesametime.Inthissituation,wecannotexecutethepreviousstatementsforeachbrick;ifthey-axisdirectionismultipliedby-1twice,thevalueinthenextiteration

ofthegameloopwillbethesame.

Wecouldcheckwhetherthecollisionoccurredfromaboveorbehind,buttheproblemwithmultiplebricksisthattheballmayoverlapthelateralofoneofthebricksand,therefore,changethex-axisdirectionaswell.Thishappensbecauseoftheball’sspeedandtherateatwhichitspositionisupdated.

Wewillsimplifythisbyassumingthatacollisionwithmultiplebricksatthesametimeoccursonlyfromaboveorbelow.Thatmeansthatitchangesthey-axiscomponentofthedirectionwithoutcalculatingthepositionofthecollidingbricks:

iflen(game_objects)>1:

self.direction[1]*=-1

Withthesetwoconditions,wecandefinethecollidemethod.Aswewillseelater,anothermethodwillberesponsiblefordeterminingthelistofcollidingbricks,sothismethodonlyhandlestheoutcomeofacollisionwithoneormorebricks:

defcollide(self,game_objects):

coords=self.get_position()

x=(coords[0]+coords[2])*0.5

iflen(game_objects)>1:

self.direction[1]*=-1

eliflen(game_objects)==1:

game_object=game_objects[0]

coords=game_object.get_position()

ifx>coords[2]:

self.direction[0]=1

elifx<coords[0]:

self.direction[0]=-1

else:

self.direction[1]*=-1

forgame_objectingame_objects:

ifisinstance(game_object,Brick):

game_object.hit()

Notethatthismethodhitseverybrickinstancethatiscollidingwiththeball,sothehitcountersaredecreasedandthebricksareremovediftheyreachzerohits.

StartingthegameFinally,wehavebuiltthefunctionalityneededtorunthegameloop—thelogicrequiredtoupdatetheball’spositionaccordingtotherebounds,andrestartthegameiftheplayerlosesonelife.

NowwecanaddthefollowingmethodstoourGameclasstocompletethedevelopmentofourgame:

defstart_game(self):

self.canvas.unbind('<space>')

self.canvas.delete(self.text)

self.paddle.ball=None

self.game_loop()

defgame_loop(self):

self.check_collisions()

num_bricks=len(self.canvas.find_withtag('brick'))

ifnum_bricks==0:

self.ball.speed=None

self.draw_text(300,200,'Youwin!')

elifself.ball.get_position()[3]>=self.height:

self.ball.speed=None

self.lives-=1

ifself.lives<0:

self.draw_text(300,200,'GameOver')

else:

self.after(1000,self.setup_game)

else:

self.ball.update()

self.after(50,self.game_loop)

Thestart_gamemethod,whichweleftunimplementedinaprevioussection,isresponsibleforunbindingtheSpacebarinputkeysothattheplayercannotstartthegametwice,detachingtheballfromthepaddle,andstartingthegameloop.

Stepbystep,thegame_loopmethoddoesthefollowing:

Itcallsself.check_collisions()toprocesstheball’scollisions.Wewillseeitsimplementationinthenextcodesnippet.Ifthenumberofbricksleftiszero,itmeansthattheplayerhaswon,andacongratulationstextisdisplayed.Supposetheballhasreachedthebottomofthecanvas:

Then,theplayerlosesonelife.Ifthenumberoflivesleftiszero,itmeansthattheplayerhaslost,andtheGameOvertextisshown.Otherwise,thegameisreset

Otherwise,thisiswhathappens:

Thepositionoftheballisupdatedaccordingtoitsspeedanddirection,andthegameloopiscalledagain.The.after(delay,callback)methodonaTkinterwidgetsetsatimeouttoinvokeafunctionafteradelayinmilliseconds.Since

thisstatementwillbeexecutedwhenthegameisnotoveryet,thiscreatestheloopnecessarytoexecutethislogiccontinuously:

defcheck_collisions(self):

ball_coords=self.ball.get_position()

items=self.canvas.find_overlapping(*ball_coords)

objects=[self.items[x]forxinitems\

ifxinself.items]

self.ball.collide(objects)

Thecheck_collisionsmethodlinksthegameloopwiththeballcollisionmethod.SinceBall.collidereceivesalistofgameobjectsandcanvas.find_overlappingreturnsalistofcollidingitemswithagivenposition,weusethedictionaryofitemstotransformeachcanvasitemintoitscorrespondinggameobject.

RememberthattheitemsattributeoftheGameclasscontainsonlythosecanvasitemsthatcancollidewiththeball.Therefore,weneedtopassonlytheitemscontainedinthisdictionary.Oncewehavefilteredthecanvasitemsthatcannotcollidewiththeball,suchasthetextdisplayedinthetop-leftcorner,weretrieveeachgameobjectbyitskey.

Withlistcomprehensions,wecancreatetherequiredlistinonesimplestatement:

objects=[self.items[x]forxinitemsifxinself.items]

Thebasicsyntaxoflistcomprehensionsisthefollowing:

new_list=[expr(elem)forelemincollection]

Thismeansthatthenew_listvariablewillbealistwhoseelementsaretheresultofapplyingtheexprfunctiontoeacheleminthelistcollection.

Wecanfiltertheelementstowhichtheexpressionwillbeappliedbyaddinganifclause:

new_list=[expr(elem)forelemincollectionifelemisnotNone]

Thissyntaxisequivalenttothefollowingloop:

new_list=[]

forelemincollection:

ifelemisnotNone:

new_list.append(elem)

Inourcase,theinitiallististhelistofcollidingitems,theifclausefilterstheitemsthatarenotcontainedinthedictionary,andtheexpressionappliedtoeachelementretrievesthegameobjectassociatedwiththecanvasitem.Thecollidemethodiscalledwiththislistasaparameter,andthelogicforthegameloopiscompleted.

PlayingBreakoutOpenthechapter1_complete.pyscripttoseethefinalversionofthegame,andrunitbyexecutingchapter1_complete.py,asyoudidwiththepreviouscodesamples.

Whenyoupressthespacebar,thegamestartsandtheplayercontrolsthepaddlewiththerightandleftarrowkeys.Eachtimetheplayermissestheball,thelivescounterwilldecrease,andthegamewillbeoveriftheballreboundismissedagainandtherearenolivesleft:

Inourfirstgame,alltheclasseshavebeendefinedinasinglescript.However,asthenumberoflinesofcodeincreases,itbecomesnecessarytodefineseparatescriptsforeachpart.Inthenextchapters,wewillseehowitispossibletoorganizeourcodebymodules.

SummaryInthischapter,webuiltoutfirstgamewithvanillaPython.Wecoveredthebasicsofthecontrolflowandtheclasssyntax.WeusedTkinterwidgets,especiallytheCanvaswidgetanditsmethods,toachievethefunctionalityneededtodevelopagamebasedoncollisionsandsimpleinputdetection.

OurBreakoutgamecanbecustomizedaswewant.Feelfreetochangethecolordefaults,thespeedoftheball,orthenumberofrowsofbricks.

However,GUIlibrariesareverylimited,andmorecomplexframeworksarerequiredtoachieveawiderrangeofcapabilities.Inthenextchapter,wewillintroduceCocos2d,agameframeworkthathelpsuswiththedevelopmentofournextgame.

Chapter2.CocosInvadersInthepreviouschapter,webuiltagamewithaGraphicalUserInterface(GUI)packagecalledTkinter.SinceitispartofthePython’sstandardlibraries,itwaseasytosetuptheprojectandcreatetherequiredwidgetswithafewlinesofcode.However,thiskindofmodulefallsshortofprovidingthecorefunctionalityofgamedevelopmentbeyondthe2Dgraphics.

Theprojectcoveredinthischapterisdevelopedwithcocos2d.ThisframeworkhasforksfordifferentprogramminglanguagesbesidesPython,suchasC++,Objective-C,andJavaScript.

Thegamethatwewilldevelopisavariantofaclassicalgame,SpaceInvaders.Thisarcadewasasuccessofitstime,andithasbecomepartofthecultureofvideogaming.

Inthistwo-dimensionalgame,theplayermustdefeatwavesofdescendingaliensbyshootingthemwithalasercannon,whichcanbemovedhorizontallyandisprotectedbysomedefensebunkers.Theenemyfireattemptstodestroytheplayer’scannon,anditgraduallydamagesthebunkers.

Inourversion,wewillleaveoutthedefensebunkers,andthegamewilllookasfollows:

Withthisproject,youwilllearnthefollowing:

Thefoundationsofcocos2dHowtoworkwithspritesProcessinginputeventsHandlingmovementsandcollisionsComplementingcocos2dwiththePygletAPI

Installingcocos2dInourpreviousgame,wedidnotuseanythird-partypackagesinceTkinterwaspartofthePythonstandardlibrary.Thisisnotthecasewithournewgameframework,whichmustbedownloadedandinstalled.

ThePythonPackageIndex(alsocalledPyPI)isanofficialsoftwarerepositoryofthird-partypackages,andthankstopackagemanagersystems,thisprocessbecomesverystraightforward.Thepipisthepackagingtoolrecommendation,anditisalreadyincludedinthelatestPythoninstallations.

Youcancheckwhetherpipisinstalledbyrunningpip--version.Theoutputindicatestheversionofpipandwhereitislocated.ThisinformationmayvarydependingonyourPythonversionanditsinstallationdirectory:

$pip--version

pip6.0.8fromC:\Python34\lib\site-packages(python3.4)

Sincecocos2disavailableonthePyPI,youcaninstallitbyrunningthefollowingcommand:

$pipinstallcocos2d

Thiscommanddownloadsandinstallsnotonlythecocos2dpackagebutalsoitsdependencies(Pygletandsix).Oncerun,theconsoleoutputshouldprinttheprogressandindicatethattheinstallationwasexecutedsuccessfully.Toverifythatcocos2discorrectlyinstalled,youcanrunthiscommand:

$python-c"importcocos;print(cocos.version)"

0.6.0

Atthetimeofwritingthisbook,thelatestversionofcocos2dis0.6.0,sotheoutputmightbeagreaterversionthanthisone.

TipThePythonpackagingecosystem

Apartfrompip,thereareotherpackagingutilities,suchaseasy_installandconda.Eachtoolhasdifferentfeaturesandlimitations,butinthisbook,wewillstickwithpipduetoitseaseofuseandthefactthatitisincludedinthelatestPythoninstallationsbydefault.

Gettingstartedwithcocos2dAswithanyotherframework,cocos2discomposedofseveralmodulesandclassesthatimplementdifferentfeatures.InordertodevelopapplicationswiththisAPI,weneedtounderstandthefollowingbasicconcepts:

Scene:Eachofthestagesofyourapplicationisascene.Whileyourgamemayhavemanyscenes,onlyoneisactiveatanygivenpointintime.Thetransitionbetweenscenesdefinesyourgame’sworkflow.Layer:Everysheetcontainedinascene,whoseoverlaycreatesthefinalappearanceofthescene,iscalledalayer.Forinstance,yourgame’smainscenemayhaveabackgroundlayer,anHUDlayerwithplayerinformationandscores,andananimationlayer,whereeventsandcollisionsbetweenspritesarebeingprocessed.Sprite:Thisisa2Dimagethatcanbemanipulatedthoughcocos2dactions,suchasmove,scale,orrotate.Inourgames,spriteswillrepresenttheplayer’scharacter,enemies,andvisualinformation,suchasthenumberoflivesleft.Director:Thisisasharedobjectthatinitializesthemainwindowandcontrolsthecurrentsceneaswellastherestofthescenesthatareonhold.

AlloftheseclassesexceptDirectorinheritfromCocosNode.Thisclassrepresentselementsthatare—orcontain—nodesthatgetdrawn,anditispartofthecoreofthelibrary.

Thefunctionalityofthisclasscoversparent-childrelations,specialplacements,rendering,andscheduledactions.CocosNodesarealsonotifiedwhentheyareaddedasachildofanothernodeandwhentheyareremovedfromtheirparentnode.

Thistablehasasummaryofthemostrelevantmembersofthisclass:

Member Description

add(child,z=0,

name=None)Thisaddsachild.Itraisesanexceptionifthenamealreadyexists.

remove(child)Thisremovesachildgivenitsnameortheobject.Itraisesanexceptionifthechildisnotonthelistofchildren.

kill() Removesitselffromitsparent.

get_children() Returnsalistofthechildrenofthenode,orderedbytheirzindex.

parent Theparentnode.

position Thepositioncoordinatesrelativetotheparentnode.

x Thex-axispositioncoordinate.

y They-axispositioncoordinate.

schedule(callback) Thisschedulesafunctiontobecalledeveryframe.

on_enter()Thisiscalledwhenaddedasachildwhiletheparentisintheactivescene,orifthescenegoesactive.

on_exit()Thisiscalledwhenremovedfromitsparentwhiletheparentisintheactivescene,orifthescenegoesactive.

Withthesedefinitionsinmind,wecanbuildourfirstcocos2dapplication.Itwillbeasimpleapplicationinwhichtheplayermustpickupfourobjectsplacedaroundthescene.Theplayercharactercanbemovedwiththearrowkeys,andthereareneithermenusnordisplayinformation.

Theapplicationwilllooklikethis:

Ascanbeseen,thefunctionalitywillbequitebasic,sincethepurposeofthisexampleistofamiliarizeyourselfwiththecocos2dmodules.

Remembertosavethescriptinthesamedirectoryasthe'ball.png'image,sinceitisloadedbyyourgame.Thefirstiterationofourappcontainsthefollowingcode:

importcocos

classMainLayer(cocos.layer.Layer):

def__init__(self):

super(MainLayer,self).__init__()

self.player=cocos.sprite.Sprite('ball.png')

self.player.color=(0,0,255)

self.player.position=(320,240)

self.add(self.player)

if__name__=='__main__':

cocos.director.director.init(caption='Hello,Cocos')

layer=MainLayer()

scene=cocos.scene.Scene(layer)

cocos.director.director.run(scene)

Firstofall,weimportthecocosmodule.Wecanimportonlytherequiredmodulesseparately(cocos.sprite,cocos.layer,cocos.scene,andcocos.director),whichhastheadvantageofaquickerinitialization.However,forthisbasicapp,wewillfollowasimplerapproach.

Wehavedefinedacustomlayerbyinheritingfromthecocos.layer.Layerclass.Inthisexample,wecallitsparent__init__methodonly,butaswewillseelater,thereareotherLayersubclassesthatcanbeusedforspecializedfunctionality,suchasColorLayerandMenu.

Thelayercontainsasinglecocos.sprite.Spriteinstancewiththe'ball.png'image,whichwillrepresenttheplayercharacter.ThecolorattributeindicatestheRGBcolorappliedtothespriteimage,representedasatuplewhosevaluesrangefrom0to255.Thepositionattribute,declaredintheCocosNodeclass,setsthesprite’scentercoordinates.Itisalsopossibletopassthesevaluesaskeywordargumentstothe__init__method.

Themaincodeblockperformsthefollowingactions:

InitializesthemainwindowwiththeDirectorinstanceInstantiatesourLayersubclasswiththespriteCreatesanemptyscenethatcontainsthelayerRunstheDirectormainloopwiththescene

Sincedirector.initwrapsthecreationofaPygletwindow,thecompletelistofkeywordparametersthatcanbepassedtothismethodisavailableatthePygletWindowAPIReference.

Forourcocos2dgames,onlyasubsetoftheseparameterswillbeused,andthemostrelevantkeywordparametersarethefollowing:

Parametername Description Defaultvalue

fullscreen Aflagforcreatingthewindowthatfillstheentirescreen False

resizable Indicateswhetherthewindowisresizable False

vsync Syncwiththeverticalretrace True

width Windowwidthinpixels 640

height Windowheightinpixels 480

caption Windowtitle(string)

visible Indicateswhetherthewindowisvisibleornot True

Oncewehavesetuptheapplication,weextenditsfunctionalitybyhandlinginputevents.

HandlinguserinputEachLayerisresponsibleforhandlinginputeventsthroughtheis_event_handlerflag,whichissettoFalsebydefault.IfthisflagissettoTrue,theLayerwillreceiveinputeventsandcallthecorrespondinghandlermethods.

TheseareeventlistenersfromtheLayerclass:

on_mouse_motion(x,y,dx,dy):The(x,y)arethephysicalcoordinatesofthemouse,and(dx,dy)isthedistancevectorcoveredbythemousesincethelastcallon_mouse_press(x,y,buttons,modifiers):Justasinon_mouse_motion,(x,y)arethephysicalcoordinatesofthemouse,butthisiscalledwhenamousebuttonispressedon_mouse_drag(x,y,dx,dy,buttons,modifiers):Acombinationofon_mouse_pressandon_mouse_motion,thisiscalledwhenthemousemovesoverwithoneormorebuttonspressedon_key_press(key,modifiers):Thisiscalledwhenakeyispressedon_key_release(key,modifiers):Thisiscalledwhenakeyisreleased

ThebuttonsparameterisabitwiseORofPygletbuttonconstantsthataredefinedinthepyglet.window.mouseandpyglet.window.keymodulesfortheon_mouseandon_keyevents,respectively.ThemodifiersparameterisabitwiseORofthepyglet.window.keyconstants,suchasShift,Option,andAlt.Youcanfindalltheconstantsdefinedinthismoduleathttps://pythonhosted.org/pyglet/api/pyglet.window.key-module.html.

WhenyoudefineoneofthesemethodsinyourLayersubclassandthecorrespondinginputeventoccurs,themethodiscalled.

Inthisiterationofoursamplegame,wewillprintamessagewhenakeyispressedorreleased:

TipNotethatsincePygletisusedinternallyforeventhandling,weneedthesymbol_stringfunctionfromthepyglet.window.keymoduletodecodethenameofthekey.

importcocos

frompyglet.windowimportkey

classMainLayer(cocos.layer.Layer):

is_event_handler=True

def__init__(self):

super(MainLayer,self).__init__()

self.player=cocos.sprite.Sprite('ball.png')

self.player.color=(0,0,255)

self.player.position=(320,240)

self.add(self.player)

defon_key_press(self,k,m):

print('Pressed',key.symbol_string(k))

defon_key_release(self,k,m):

print('Released',key.symbol_string(k))

TipWehaveomittedtheif__name__=='__main__'block,butremembertoincludeitinyourscriptsinordertoexecutetheapplication.

Whenyoulaunchit,thekeyboardeventsmustbeprintedontheconsole.Ournextstepwillbetoreflecttheseeventsinthegamestatebymodifyingthesprite’sposition.

TipThePygleteventframework

Eventhandlingisimplementedincocos2dwiththePygleteventframework.Thiseventframeworkallowsyoutodefineemitterswithagiveneventname(suchason_key_press)andregisterlistenersovertheseevents.

RememberthatPygletisthemultimedialibraryonwhichcocos2drelies,sowewillseemoreusagesofthisAPIsincemajorcocos2dapplicationsusePygletmodules.

UpdatingthesceneSofar,ourgameonlydisplaysthesamecontent,asobjectsarebeingmodified.InawaysimilartotheTkintercallbackthatwasloopingduringtheexecutionofourlastgame,weneedtorefreshourcocos2dapplicationperiodically.

ThisisachievedwiththeschedulemethodfromtheCocosNodeclass.Itschedulesafunctionthatiscalledoneveryframe,andthefirstargumentthatthisfunctionreceivesistheelapsedtimeinsecondssincethelastclocktick.

Whileitispossibletopassadditionalargumentstothiscallbackfunction,wewilldefineanewmethodintheMainLayerclasswiththeelapsedtimeparameteronly:

importcocos

fromcollectionsimportdefaultdict

frompyglet.windowimportkey

classMainLayer(cocos.layer.Layer):

is_event_handler=True

def__init__(self):

super(MainLayer,self).__init__()

self.player=cocos.sprite.Sprite('ball.png')

self.player.color=(0,0,255)

self.player.position=(320,240)

self.add(self.player)

self.speed=100.0

self.pressed=defaultdict(int)

self.schedule(self.update)

defon_key_press(self,k,m):

self.pressed[k]=1

defon_key_release(self,k,m):

self.pressed[k]=0

defupdate(self,dt):

x=self.pressed[key.RIGHT]-self.pressed[key.LEFT]

y=self.pressed[key.UP]-self.pressed[key.DOWN]

ifx!=0ory!=0:

pos=self.player.position

new_x=pos[0]+self.speed*x*dt

new_y=pos[1]+self.speed*y*dt

self.player.position=(new_x,new_y)

Wehavemodifiedon_key_pressandon_key_releasesothattheysavethekeystrokeindefaultdict,aspecialdictionaryfromthecollectionsmodule.Thisdatastructurereturnsthecorrespondingvalueifthekeyispresentorthedefaultvalueofthetypeifthekeydoesnotexist.Inourcase,sinceitisdefaultdict(int),thedefaultvaluefortheabsentkeysis0.

Wehavescheduledtheupdatemethod,andforeachframe,itchecksthevaluesofthe

pressedarrowkeys.Ifthedifferencebetweenthesevaluesisdistincttozero,thespriteismoved.

ProcessingcollisionsIncocos2d,proximityandcollisionsamongactorsarecalculatedwithaninterfaceofthecocos.collision_modelpackagecalledCollisionManager.Thisinterfacecanbeusedtodeterminewhethertwoobjectsareoverlapping,orwhichobjectsarecloserthanacertaindistancefromagivenobject.

Actorsshouldbeaddedfirsttothesetofobjectsthataremanagedbythecollisionmanager,alsocalledknownentities.Thesetofmanagedobjectsisinitiallyempty.

TheCollisionManagerinterfaceisimplementedbytwoclasses:

CollisionManagerBruteForce:Thisisastraightforwardimplementation.Init,allknownentitiesareusedtocalculateproximityandcollisions.Itisintendedfordebuggingpurposes,anditdoesnotscaleifthesetofknownentitiesgrowsinsize.Itdoesnotneedanyargumentsinitsinitialization.CollisionManagerGrid:Thisdividesthespaceintorectangularcellswithagivenwidthandheight.Whencalculatingthespatialrelationsbetweenobjects,itonlyconsiderstheobjectsfromtheknownentitiesthatoverlapthesamecell.Itsperformanceisbettersuitedforalargenumberofknownentities.The__init__methodrequirestheminimumandmaximumcoordinatesofthespace,thecellwidth,andthecellsize.Therecommendedcellsizeisthemaximumobjectwidthandheightmultipliedby1.25.Notethatasthisfactorincreasesitsvalue,moreobjectsoverlapthesamecellandtheperformancestartstodegrade.

ThereferenceoftheCollisionManagermethodsthatwearegoingtouseinthischapteristhefollowing:

Method Description

add(obj) Makesobjaknownobject

clear() Emptiesthesetofknownobjects

iter_colliding(obj) Returnsaniteratorofobjectscollidingwithobj

knows(obj) Thisistrueifobjisaknownobject

AnobjectcanbecollidedwithifithasamembercalledCshapeanditsvalueisaninstanceofcocos.collision_model.Cshape.Therearetwoclassesincocos2dthatinheritfromCshape:

CircleShape:Thisusesacircleasthegeometricspace.Whenaninstanceiscreated,thecenterofthecircleanditsradiusmustbespecified.TwocirclescollideiftheEuclideandistancebetweentheircentersislessthanthesumoftheirradius.AARectShape:Thisusesarectangleasthegeometricspace.Therectangleisaxis-aligned,whichmeansthatitssidesareparalleltothexandyaxes.Thiscanbewellsuitediftheactorsdonotrotate.InsteadoftheEuclideandistance,itusestheManhattandistancetocalculateacollisionbetweentworectangles.

Inourexample,wewillusecircularshapesforouractors.Inordertotestthecollisionmanager,fourspritesareaddedtotheLayer,andtheywillactaspickups—theywillbedestroyedwhentheycollidewiththeplayer’ssprite.

Besides,sinceCshaperequiresacocos.euclid.Vector2forthecentercoordinates,wewillreplacethetuplesfortheVector2instances.

Beforeaddingthecollisionmanager,wedefineanewclasstoreplaceoursprites.Theactorsofourgamesharetheseattributes,sowewillavoidcodeduplication:

importcocos

importcocos.collision_modelascm

importcocos.euclidaseu

fromcollectionsimportdefaultdict

frompyglet.windowimportkey

classActor(cocos.sprite.Sprite):

def__init__(self,x,y,color):

super(Actor,self).__init__('ball.png',color=color)

self.position=pos=eu.Vector2(x,y)

self.cshape=cm.CircleShape(pos,self.width/2)

Nowthatwehavethisbaseclass,wecanaddtheseobjectswiththerequiredcshapemember.ThecollisionmanagerimplementationthatwewilluseinallourapplicationsisCollisionManagerGrid,sincethebrute-forceimplementationisonlyintendedforreferenceanddebugging:

classMainLayer(cocos.layer.Layer):

is_event_handler=True

def__init__(self):

super(MainLayer,self).__init__()

self.player=Actor(320,240,(0,0,255))

self.add(self.player)

forposin[(100,100),(540,380),\

(540,100),(100,380)]:

self.add(Actor(pos[0],pos[1],(255,0,0)))

cell=self.player.width*1.25

self.collman=cm.CollisionManagerGrid(0,640,0,480,

cell,cell)

self.speed=100.0

self.pressed=defaultdict(int)

self.schedule(self.update)

defon_key_press(self,k,m):

self.pressed[k]=1

defon_key_release(self,k,m):

self.pressed[k]=0

Nowtheupdatemethodmustperformthefollowingstepsinordertobeabletodetectcollisionsofmovingobjects:clearthecollisionmanager,addtheactorstothesetof

knownentities,anditerateovertheobjects’collisions:

defupdate(self,dt):

self.collman.clear()

for_,nodeinself.children:

self.collman.add(node)

forotherinself.collman.iter_colliding(self.player):

self.remove(other)

x=self.pressed[key.RIGHT]-self.pressed[key.LEFT]

y=self.pressed[key.UP]-self.pressed[key.DOWN]

ifx!=0ory!=0:

pos=self.player.position

new_x=pos[0]+self.speed*x*dt

new_y=pos[1]+self.speed*y*dt

self.player.position=(new_x,new_y)

self.player.cshape.center=self.player.position

Notethestatementinwhichthecshapecenteroftheplayerspriteisupdatedwiththenewposition.Withoutthisline,thecollisionmanagerwillnotdetectanycollisionbetweentheplayerandthepickups,sincetheentitycenterwillalwaysbeintheinitialposition.

Inthechapter2_01.pyscript,youcanfindthecompletecode.Forthisapplication,weonlyneededasinglesprite.However,mostgamesuseseveralimagestorepresentdifferentgamecharactersandtheiranimations.

Inthenextsection,wewillseethespritesthatourSpaceInvadersgamewillhave.

CreatinggameassetsWewillneedafewbasicimagesforourcocos2dsprites.Theywillbethevisualrepresentationsoftheplayer’scannonandthedifferenttypesofaliens.

Thefollowingimageshowshowthesespritescanbedrawnwithanimageeditorprogram,withthehelpofagrid:

Unfortunately,theusageofimagemanipulationtoolsisbeyondthescopeofthisbook.Thereisawidevarietyintheseprograms,andforbasicsprites(suchastheonesshowninthepreviousimage),youcanuseMSPaintonWindowssystems.

Undertheimgfolder,youcanfindthedifferentimagesthatwewilluseinourgame.Feelfreetoeditthemandchangetheircolors.However,donotmodifytheirsizeasitisusedtodeterminethewidthandtheheightoftheSpriteanditsCshapemember.

TipCreatingyourown“pixelart”

Therearemoreadvancedapplicationsthatcanberunonanyplatform.ThisisthecaseofGIMP,anopensourceprogramthatispartoftheGNUproject.ThespritesusedinthischapterhavebeendrawnwithGIMP2,thecurrentversion.

Youcandownloaditforfreefromhttp://www.gimp.org/downloads/.

SpaceInvadersdesignBeforewritingourgamewithcocos2d,weshouldconsiderwhichactorsweneedtomodelinordertorepresentourgameentities.InourSpaceInvadersversion,wewillusethefollowingclasses:

PlayerCannon:Thisisthecharactercontrolledbytheplayer.Thespacebarisusedtoshoot,andthehorizontalmovementiscontrolledwiththerightandleftarrowkeys.Alien:Eachoneofthedescendingenemies,withdifferentlooksandscoresdependingonthealientype.AlienColumn:Therearecolumnsoffivealiensintowhicheverygroupofaliensisdivided.AlienGroup:Awholegroupofenemies.Itmoveshorizontallyordescendsuniformly.Shoot:Asmallprojectilelaunchedbytheenemiestowardstheplayercharacter.PlayerShoot:AShootsubclass.Itislaunchedbytheplayerratherthanthealiengroup.

Apartfromtheseclasses,weneedacustomLayerforthegamelogic,whichwillbenamedGameLayer,andabaseclassforthelogicthatissharedamongtheactors—similartotheGameObjectclassfromourpreviousgame.

WewillcallitActor,aswedidinourpreviouscocos2dapplication.Infact,itisquitesimilartothatone,exceptthatthisimplementationaddsthedefinitionoftwomethods:updateandcollide.Theywillbecalledwhenourgameloopupdatesthepositionofthenodesandwhenaknownentityhitstheactor,respectively.

Thismoduleisnew,apartfromthecodewesawintheprevioussection,anditwillcontainthefollowingcode:

importcocos.sprite

importcocos.collision_modelascm

importcocos.euclidaseu

classActor(cocos.sprite.Sprite):

def__init__(self,image,x,y):

super(Actor,self).__init__(image)

self.position=eu.Vector2(x,y)

self.cshape=cm.AARectShape(self.position,

self.width*0.5,

self.height*0.5)

defmove(self,offset):

self.position+=offset

self.cshape.center+=offset

defupdate(self,elapsed):

pass

defcollide(self,other):

pass

ThePlayerCannon,Alien,Shoot,andPlayerShootclasseswillextendfromthisclass.Therefore,withthislittlepieceofcode,wehavesetuptheinterfaceforcollisiondetectionandthemovementsofallourgameobjects.

ThePlayerCannonandGameLayerclassesAswesawearlier,PlayerCannonisoneofourgameactors.Itmustrespondtotheleftandrightkeystrokestocontrolthehorizontalmovementofthesprite,andtheobjectisdestroyedifanenemy’sshothitsit.

Wewillimplementthisbyoverridingbothupdateandcollide:

fromcollectionsimportdefaultdict

frompyglet.windowimportkey

classPlayerCannon(Actor):

KEYS_PRESSED=defaultdict(int)

def__init__(self,x,y):

super(PlayerCannon,self).__init__('img/cannon.png',x,y)

self.speed=eu.Vector2(200,0)

defupdate(self,elapsed):

pressed=PlayerCannon.KEYS_PRESSED

movement=pressed[key.RIGHT]-pressed[key.LEFT]

w=self.width*0.5

ifmovement!=0andw<=self.x<=self.parent.width-w:

self.move(self.speed*movement*elapsed)

defcollide(self,other):

other.kill()

self.kill()

Todeterminewhetherakeyispressedornot,wehavedefineddefaultdict,asinourbasiccocos2dapplication.Inthiscase,itisaclassattributeofPlayerCannon.Thehorizontalmovementislimitedsothatthecharactercannotleavethecollisionmanagergrid.

TheGameLayerclasswillbethemainlayerofourgame,anditwillberesponsiblefordoingthefollowing:

KeepingtrackofthenumberofplayerlivesleftandthecurrentscoreHandlinginputkeyeventsbysettingitsis_event_handlerflagtotrueCreatingthegameactorsandaddingthemaschildnodesRunningthegameloopbyexecutingascheduledfunctionforeachframewherethecollisionsareprocessedandtheobjectpositionsareupdated

Itsinitialimplementationisthefollowing:

importcocos.layer

classGameLayer(cocos.layer.Layer):

is_event_handler=True

defon_key_press(self,k,_):

PlayerCannon.KEYS_PRESSED[k]=1

defon_key_release(self,k,_):

PlayerCannon.KEYS_PRESSED[k]=0

def__init__(self):

super(GameLayer,self).__init__()

w,h=cocos.director.director.get_window_size()

self.width=w

self.height=h

self.lives=3

self.score=0

self.update_score()

self.create_player()

self.create_alien_group(100,300)

cell=1.25*50

self.collman=cm.CollisionManagerGrid(0,w,0,h,

cell,cell)

self.schedule(self.update)

Thecreate_playermethodaddsaPlayerCannoninthecenterofthescreen.Itwillbecalledeachtimetheplayercharacterneedstoberespawned,whileupdate_scoreincrementsthescorebyaddingthepointsforeachalienthatisdestroyed.Thedefaultvalueofthescore=0argumentinthemethodsignaturemeansthatifnoargumentsarepassed,theargument’svaluewillbe0:

defcreate_player(self):

self.player=PlayerCannon(self.width*0.5,50)

self.add(self.player)

defupdate_score(self,score=0):

self.score+=score

Thecreate_alien_groupmethodinitializestherowsofdescendingaliens.Sincetherequiredclasseshavenotbeenimplementedyet,wewillleaveitwiththepassstatementfornow:

defcreate_alien_group(self,x,y):

pass

Theupdatemethodisacallbackthatwillbescheduledtobeexecutedforeachframe:

defupdate(self,dt):

self.collman.clear()

for_,nodeinself.children:

self.collman.add(node)

ifnotself.collman.knows(node):

self.remove(node)

for_,nodeinself.children:

node.update(dt)

Wedidasmalltrickhere.AswesawintheCollisionManagerreference,theknowsmethodcheckswhetheranobjectisinthesetofknownentities—whichshouldbealwaystruesinceallofthelayer’schildrenhavebeenadded.

However,whenanobjectisoutsideofthesurfacecoveredbythecollisionmanager,itdoesnotoverlapwithanycellofthegridanditisconsideredtobe“notknown.”Inthis

way,objectsthatmoveoutofthisareaareautomaticallyremoved:

defcollide(self,node):

ifnodeisnotNone:

forotherinself.collman.iter_colliding(node):

node.collide(other)

returnTrue

returnFalse

Finally,thecollidemethodencapsulatesthecalltoiter_collidingandcheckswhetherthenodeisareferencetoavalidobject—thePlayerShootinstancecanbeNoneifthereisnocurrentshoot.

Asusual,wewritethemainblockforthemomentwhenourscriptisrunasthemainmodule:

if__name__=='__main__':

cocos.director.director.init(caption='CocosInvaders',

width=800,height=650)

game_layer=GameLayer()

main_scene=cocos.scene.Scene(game_layer)

cocos.director.director.run(main_scene)

Invaders!Therearethreeclassesusedtorepresentourinvaders:Alien,AlienColumn,andAlienGroup.

Outoftheseclasses,onlyAlieninheritsfromActorbecauseitistheonlyentitythatisdrawnandcollideswithotherobjects.Insteadofastaticimage,thespritewillbeabasicanimationwhereineachimagewillbeshownfor0.5seconds.ThisisachievedbyloadinganImageGridandcreatinganAnimationfromthisspritegrid.Sincetheseclassesbelongtothepyglet.imagemodule,weneedtoimportthemfirst.

Anotherfunctionofouralienswillbenotifyingitscolumnthattheobjecthasbeenremoved.Thankstothis,thecolumnofaliensknowswhatthebottomoneisandstartsshootingfromitsposition.

YoulearnedfromtheCocosNodereferencethattheon_exitmethodiscalledwhenanodeisremoved,soyouwillbeoverridingittoinformitscorrespondingcolumn.Notethatareferencetothecolumnispassedtothe__init__method.WecouldhaveimplementedthesamefunctionalitywithaPygleteventhandlermechanism,butthatwouldrequirepushingthehandlerstoeveryAlieninstance:

frompyglet.imageimportload,ImageGrid,Animation

classAlien(Actor):

defload_animation(imgage):

seq=ImageGrid(load(imgage),2,1)

returnAnimation.from_image_sequence(seq,0.5)

TYPES={

'1':(load_animation('img/alien1.png'),40),

'2':(load_animation('img/alien2.png'),20),

'3':(load_animation('img/alien3.png'),10)

}

deffrom_type(x,y,alien_type,column):

animation,score=Alien.TYPES[alien_type]

returnAlien(animation,x,y,score,column)

def__init__(self,img,x,y,score,column=None):

super(Alien,self).__init__(img,x,y)

self.score=score

self.column=column

defon_exit(self):

super(Alien,self).on_exit()

ifself.column:

self.column.remove(self)

TheTYPESclassattributehelpusloadtheanimationsonlyonce,atthebeginningofthegame.Thescoreisthenumberofpointsearnedwhentheenemyisdestroyed,whilethecolumnattributecontainsareferencetothecolumntowhichthealienbelongs.

TheAlienColumnclassisresponsibleforinstantiatingthecolumnsofalienswithagivenpattern,likethis:

SincewehavedefinedtheAlien.from_typeutilitymethod,wecallitintheproperordertoplaceeachtypeofalien,asseeninthepreviousfigure:

classAlienColumn(object):

def__init__(self,x,y):

alien_types=enumerate(['3','3','2','2','1'])

self.aliens=[Alien.from_type(x,y+i*60,alien,self)

fori,alieninalien_types]

defremove(self,alien):

self.aliens.remove(alien)

defshoot(self):pass

Theshould_turnmethodcheckswhether,giventhecurrentdirection,thecolumnhasreachedthesideofthescreenornot.ItwillreturnFalseiftherearenoaliensleftinthecolumn:

defshould_turn(self,d):

iflen(self.aliens)==0:

returnFalse

alien=self.aliens[0]

x,width=alien.x,alien.parent.width

returnx>=width-50andd==1orx<=50andd==-1

AlltheAlienColumninstancesformtheAlienGroup.Itismoveduniformly,anditdelegatestheshootinglogictoeachcolumn.Thismovementisimplementedwiththespeedanddirectionobjectattributes.

Whilethealiengroupisbeingupdated,itsumstheelapsedtimesbetweenframes.Whenacertainperiodhaselapsed,thewholegroupismoveddownorlaterally.Thedirectionwilldependonwhetheranycolumnhasreachedthelateralsidesornot:

classAlienGroup(object):

def__init__(self,x,y):

self.columns=[AlienColumn(x+i*60,y)

foriinrange(10)]

self.speed=eu.Vector2(10,0)

self.direction=1

self.elapsed=0.0

self.period=1.0

defupdate(self,elapsed):

self.elapsed+=elapsed

whileself.elapsed>=self.period:

self.elapsed-=self.period

offset=self.direction*self.speed

ifself.side_reached():

self.direction*=-1

offset=eu.Vector2(0,-10)

foralieninself:

alien.move(offset)

defside_reached(self):

returnany(map(lambdac:c.should_turn(self.direction),

self.columns))

def__iter__(self):

forcolumninself.columns:

foralienincolumn.aliens:

yieldalien

Here,wealsodefinea__iter__method,whichisaspecialmethodthatisinvokedwhenyouiterateoveranobject.Inthisway,wecancallforalieninalien_groupintherestofourcode.

Nowthatwehaveimplementedthelogicofourenemiesandthemovementofthewholegroup,wecanadditsinstantiationtotheGameLayerclass:

defcreate_alien_group(self,x,y):

self.alien_group=AlienGroup(x,y)

foralieninself.alien_group:

self.add(alien)

Thiscreatesanewaliengroupandaddsalltheenemiestothelayeraschildnodes.Thechapter2_02.pyscriptcontainsthecodethatwehavewrittensofar,anditsexecutionwilllooklikethis:

Shoot’emup!TheShootactor,byitself,isquitebasic;itonlyrequiresaspeedattributeandoverridestheupdatemethodsothattheobjectismovedbythedistancedeterminedbythisspeedandtheelapsedtimebetweenframes:

classShoot(Actor):

def__init__(self,x,y,img='img/shoot.png'):

super(Shoot,self).__init__(img,x,y)

self.speed=eu.Vector2(0,-400)

defupdate(self,elapsed):

self.move(self.speed*elapsed)

ThePlayerShootclassrequiresabitmorelogic,sincetheplayercannotshootuntilthepreviousbeamhashitanenemyorreachedtheendofthescreen.

Aswewanttoavoidglobalvariables,wewilluseaclassattributetoholdthereferencetothecurrentshot.Whentheshotleavesthescene,thisreferencewillbesettoNoneagain.

WewilloverridethecollidemethodfromtheActorclasssothatboththebeamandthealienaredestroyedwhenacollisionoccurs.ThisisdonebycallingthekillmethoddefinedintheSpriteclass.ItinternallyremovestheCocosNodefromitsparent:

classPlayerShoot(Shoot):

INSTANCE=None

def__init__(self,x,y):

super(PlayerShoot,self).__init__(x,y,'img/laser.png')

self.speed*=-1

PlayerShoot.INSTANCE=self

defcollide(self,other):

ifisinstance(other,Alien):

self.parent.update_score(other.score)

other.kill()

self.kill()

defon_exit(self):

super(PlayerShoot,self).on_exit()

PlayerShoot.INSTANCE=None

Withthisclass,wecanaddthisfunctionalitytotheupdatemethodofPlayerCannon:

defupdate(self,elapsed):

pressed=PlayerCannon.KEYS_PRESSED

space_pressed=pressed[key.SPACE]==1

ifPlayerShoot.INSTANCEisNoneandspace_pressed:

self.parent.add(PlayerShoot(self.x,self.y+50))

movement=pressed[key.RIGHT]-pressed[key.LEFT]

ifmovement!=0:

self.move(self.speed*movement*elapsed)

TheshootmethodinAlienColumn—whichweleftunimplemented—canbemodified

withournewShootclass.

Torandomlyshootanewbeam,wewillcallrandom.random(),whichreturnsarandomfloatbetweenthesemi-openrangeof(0.0,1.0).Wewillsetalowprobability.Thisisbecausethisfunctionwillbecalledseveraltimespersecond.TherandommoduleispartofthePythonstandardlibrary,soyoucanstartplayingaroundwithitbyaddingtheimportrandomstatementatthebeginningofthescript:

defshoot(self):

ifrandom.random()<0.001andlen(self.aliens)>0:

pos=self.aliens[0].position

returnShoot(pos[0],pos[1]-50)

returnNone

NowtheupdatemethodoftheGameLayerclasscancalculatewhichobjectscollidewiththecannonandtheplayer’scurrentshot,aswellasrandomlyshootfromthealiencolumns:

defupdate(self,dt):

self.collman.clear()

for_,nodeinself.children:

self.collman.add(node)

ifnotself.collman.knows(node):

self.remove(node)

self.collide(PlayerShoot.INSTANCE)

ifself.collide(self.player):

self.respawn_player()

forcolumninself.alien_group.columns:

shoot=column.shoot()

ifshootisnotNone:

self.add(shoot)

for_,nodeinself.children:

node.update(dt)

self.alien_group.update(dt)

Here,respawn_player()decrementsthenumberoflives,anditunschedulesupdateiftherearenolivesleft.Thisstopsthemainloopandrepresentsthegameoversituation:

defrespawn_player(self):

self.lives-=1

ifself.lives<0:

self.unschedule(self.update)

else:

self.create_player()

Inthisiteration,thegamewehavebuiltisverysimilartothefinalversion.Theplayablecharactercanbemovedwiththearrowkeysandisabletoshoot.Theenemiesmoveuniformly,andtheloweralienofeachrowcanshootaswell.Thecodeforthisimplementationcanbefoundinthechapter2_03.pyscript.

Now,theonlydetailleftisdisplayingthegameinformation:thecurrentscoreandthenumberoflivesleft.

AddinganHUDOur“heads-updisplay”willbeanewLayerthatwillbedrawnoverourGameLayer:

classHUD(cocos.layer.Layer):

def__init__(self):

super(HUD,self).__init__()

w,h=cocos.director.director.get_window_size()

self.score_text=cocos.text.Label('',font_size=18)

self.score_text.position=(20,h-40)

self.lives_text=cocos.text.Label('',font_size=18)

self.lives_text.position=(w-100,h-40)

self.add(self.score_text)

self.add(self.lives_text)

defupdate_score(self,score):

self.score_text.element.text='Score:%s'%score

defupdate_lives(self,lives):

self.lives_text.element.text='Lives:%s'%lives

defshow_game_over(self):

w,h=cocos.director.director.get_window_size()

game_over=cocos.text.Label('GameOver',font_size=50,

anchor_x='center',

anchor_y='center')

game_over.position=w*0.5,h*0.5

self.add(game_over)

OurGameLayerwillholdareferencetotheHUDlayer,anditsmethodswillbemodifiedsothattheycancalltheHUDdirectly:

def__init__(self,hud):

super(GameLayer,self).__init__()

w,h=cocos.director.director.get_window_size()

self.hud=hud

self.width=w

self.height=h

#...

Thehudattributewillbeusedwhentheplayerneedstoberespawnedandwhenthescoreisupdated:

defcreate_player(self):

self.player=PlayerCannon(self.width*0.5,50)

self.add(self.player)

self.hud.update_lives(self.lives)

defrespawn_player(self):

self.lives-=1

ifself.lives<0:

self.unschedule(self.update)

self.hud.show_game_over()

else:

self.create_player()

defupdate_score(self,score=0):

self.score+=score

self.hud.update_score(self.score)

Themainblockshouldalsobemodifiedtocreatethescenewiththetwolayers.Here,wepassthezindextoindicatetheorderingofthechildrenlayers:

if__name__=='__main__':

cocos.director.director.init(caption='CocosInvaders',

width=800,height=650)

main_scene=cocos.scene.Scene()

hud_layer=HUD()

main_scene.add(hud_layer,z=1)

game_layer=GameLayer(hud_layer)

main_scene.add(game_layer,z=0)

cocos.director.director.run(main_scene)

Extrafeature–themysteryshipTocompleteourgame,wewilladdafinalingredient:themysteryshipthatrandomlyappearsatthetopofthescreen:

classMysteryShip(Alien):

SCORES=[10,50,100,200]

def__init__(self,x,y):

score=random.choice(MysteryShip.SCORES)

super(MysteryShip,self).__init__('img/alien4.png',x,y,

score)

self.speed=eu.Vector2(150,0)

defupdate(self,elapsed):

self.move(self.speed*elapsed)

Inanapproachsimilartotheshootinglogicofourenemies,wewillrandomlyaddthisalientoourscheduledfunction:

defupdate(self,dt):

self.collman.clear()

#...

self.alien_group.update(dt)

ifrandom.random()<0.001:

self.add(MysteryShip(50,self.height-50))

Youcanfindthecompleteimplementationofthisgameinthechapter2_04.pyscript.Enjoyyourfirstcocos2dgame!

SummaryInthischapter,weintroducedcocos2danditsmostrelevantmodules.Wedevelopedourfirstapplicationtogetstartedwiththelibrary,andlaterwebuiltasimplifiedversionofSpaceInvaders.

Thisversioncanbeextendedbyaddingdefensebunkersormorerandomvaluestothepossiblescoresofthemysteryship.Besides,ifyouwanttochangethevisualappearanceoftheinvaders,youcaneditthespritesandcreateyourownenemies!

Inthenextchapter,wewilldevelopacompletetowerdefensegame,withtransitionsbetweenscenes,enhanceddisplayinformation,andcomplexmenus.

Chapter3.BuildingaTowerDefenseGameInthepreviouschapter,youlearnedthefundamentalsofcocos2d,andnowyouareabletodevelopabasicgamewiththislibrary.However,mostgamesusemorethanasinglescene,andtheircomplexityitisnotlimitedtocollisionsandinputdetection.

Withournextgame,wewilldiveintothecocos2dmodulesthatprovideustheadvancedfunctionalitywearelookingfor:menus,transitions,scheduledactions,andanefficientwayofstoringlevelgraphics.

Inthischapter,wewillcoverthesetopics:

Howtomanipulatespriteswithcocos2dactionsUsingtilemapsasbackgroundlayersAnimatedtransitionsbetweengamescenesHowtocreatescenesthatactasmenusandcutscenesBuildingafull-fledgedcocos2dapplicationwithalltheingredientsyouhavelearnedsofar

ThetowerdefensegameplayThegenreoftowerdefensechallengestheplayertostopenemycharactersfromreachingacertainpositionbyplacingstrategicallydifferenttowerssothattheycandefeattheenemiesbeforetheyarriveatthatpoint.Thetowersshootautonomouslytowardstheenemiesthatarewithintheirfiringrange.Thegameisoverwhenaconcretenumberofenemiesreachtheendpoint.

Inourversion,thescenariowillbeameanderingroadinthedesert,andwehavetoprotectabunkerthatisplacedattheendofthisroad,asshowninthefollowingscreenshot.Atthefarend,enemytankswillbespawningrandomly,andwemustplaceturretsthatdestroythembeforetheyreachourbunker.Theturretscanbeplacedinspecificslots,andeachturretspendspartofourlimitedresources.

Cocos2dactionsInourpreviousgame,wemanipulatedourspritesdirectlythroughtheirmembers,especiallythepositionattribute.Thegameloopupdatedeachactorwiththeelapsedtimefromthepreviousframe.

However,ourtowerdefensegamewillbebasedmainlyoncocos2dactions,whichareorderstomodifyobjectattributessuchastheposition,rotation,orscale.Theyareexecutedbycallingthedo()methodoftheCocosNodeclass.Therefore,anysprite,layer,orscenecanbeavalidtargetofanaction.

Theactionsthatwewillcoverinthissectioncanbedividedintotwomaingroups:intervalactionsandinstantactions.

IntervalactionsTheseactionshaveaduration,andtheirexecutionendsafterthatcertainduration.Forinstance,ifwewanttomoveaspritetoacertainposition,wedonotwantittohappenimmediatelybutlastafixedamountoftime,givingtheimpressionthatitmoveswithadeterminedspeed.ThiscanbeachievedwiththeMoveToaction:

importcocos

importcocos.actionsasac

if__name__=='__main__':

cocos.director.director.init(caption='Actions101')

layer=cocos.layer.Layer()

sprite=cocos.sprite.Sprite('tank.png',position=(200,200))

sprite.do(ac.MoveTo((250,300),3))

layer.add(sprite)

scene=cocos.scene.Scene(layer)

cocos.director.director.run(scene)

Inthissample,wemoveourtanksprite,initiallyplacedatposition(200,200),toposition(250,300).ThesecondargumentofMoveToindicatestheduration,whichis3seconds.Ifwewantedtousearelativeoffsetinsteadoftheabsolutecoordinates,wecouldhaveusedtheMoveByaction.TheequivalentinthisexamplewouldbeMoveBy((50,100),3).

Anothercommonactionisrotatinganode,andforthispurpose,cocos2dofferstheRotateByandRotateToactions,whichtaketheangleindegreesandthedurationinseconds:

#...

sprite=cocos.sprite.Sprite('tank.png',position=(200,200))

sprite.do(ac.RotateBy(180,5))#Rotate180degreesin5sec

LikeMoveToandMoveBy,thedifferencebetweenRotateToandRotateByistheuseofabsoluteandrelativerotations.

TipThemathmodule

ThePythonstandardlibrarycontainsamodulewithcommonlyusedmathematicalfunctions.Sincecocos2dworkswithdegreesforrotationactions,themath.degrees(radians)andmath.radians(degrees)conversionutilitiesincludedinthemathmodulecanbeextremelyuseful.

Seetheentirefunctionalityprovidedbythismoduleathttps://docs.python.org/3.4/library/math.html.

Thecompletereferenceofinstantactionsincludedinthecocos.actions.interval_actionsmoduleisthefollowing:

Intervalaction Description

Lerp Interpolatesbetweenvaluesforaspecifiedattribute

MoveTo Movesthetargettotheposition(x,y)

MoveBy Movesthetargetbyanoffsetof(x,y)

JumpTo Movesthetargettoapositionsimulatingajumpmovement

JumpBy Movesthetargetsimulatingajumpmovement

Bezier MovesthetargetthroughaBézierpath

Blink Blinksthetargetbyhidingandshowingitanumberoftimes

RotateTo Rotatesthetargettoacertainangle

RotateBy Rotatesatargetclockwisebyanumberofdegrees

ScaleTo Scalesthetargettoazoomfactor

ScaleBy Scalesthetargetbyazoomfactor

FadeOut Fadesoutthetargetbymodifyingitsopacityattribute

FadeIn Fadesinthetargetbymodifyingitsopacityattribute

FadeTo Fadesthetargettoaspecificalphavalue

Delay Delaystheactionbyacertainnumberofseconds

RandomDelay Delaystheactionrandomlybetweenaminimumvalueandamaximumvalueofseconds

InstantactionsWehavejustintroducedintervalactions.Theseareappliedoveradurationoftime.Nowwewillcoverinstantactions,whichareappliedimmediatelytotheCocosNodeinstance.Actually,theyareinternallyimplementedasintervalactionswithzeroduration.OneexampleofaninstantactionisCallFunc,whichinvokesafunctionwhentheactionisexecuted:

importcocos.actionsasac

defupdate_score():

print('Updatingthescore…')

sprite=cocos.sprite.Sprite('tank.png',position=(200,200))

sprite.do(ac.CallFunc(update_score))

Aswewillseelater,thisactionwillbehandywhenwewanttocallaspecificfunctionduringasequenceofactions.

Notethattheupdate_scoreargumentinthelastlineisnotfollowedby().Thisisasubtlebutimportantdifference;itmeansthatwearecreatingCallFuncwithareferencetotheupdate_scorefunction,insteadofactuallyinvokingit.

Thefollowingtablecontainsalltheactionsdefinedinthecocos.actions.instant_actionsmodule:

Instantaction Description

Place Placesthetargetintheposition(x,y)

CallFunc Callsafunction

CallFuncS Callsafunctionwiththetargetasthefirstargument

Hide HidesthetargetbysettingitsvisibilitytoFalse

Show ShowsthetargetbysettingitsvisibilitytoTrue

ToggleVisibility Togglesthevisibilityofthetarget

CombiningactionsBynow,youknowhowtoapplyactionsseparately,butyouneedtocombinetheminordertoachieveamorecomplexbehavior.

TheActionclass,definedinthecocos.actions.base_actionsmodule,isthebaseclassthatInstantActionandIntervalActioninheritfrom.Itimplementsthe__add__specialmethod,whichiscalledinternallywhenweusethe+operator.

Thisoperatorcreatesasequenceofactionsthatareappliedseriallytothetarget:

sprite=cocos.sprite.Sprite('tank.png',position=(200,200))

sprite.do(ac.MoveBy((80,0),3)+ac.Delay(1)+\

ac.CallFunc(sprite.kill))

Thissnippetmovesourtank80pixelstotherightin3seconds.Itstandsfor1second,andfinally,itisremovedbycallingthekill()method.

Apartfrom__add__,the__or__specialmethodisalsooverridden.Itisinvokedwhenthe|operatorisusedandrunstheactionsinparallel:

sprite=cocos.sprite.Sprite('tank.png',position=(200,200))

sprite.do(ac.MoveTo((500,150),3)|ac.RotateBy(90,2))

Thismovesthespritetothe(500,150)positionin3seconds,andduringthefirst2seconds,itrotates90degreesclockwise.

The__add__and__or__specialmethodscanbecombinedtoproduceasequenceofactionsthatrunbothinparallelandsequentially:

sprite=cocos.sprite.Sprite('tank.png',position=(200,200))

sprite.do((ac.MoveTo((500,150),3)|ac.RotateBy(90,2))+\

ac.CallFunc(sprite.kill))

Thisexampleperformscombinedmovementandrotationasthepreviousone,butwhentheparallelactionsaredone,itremovesthesprite.

TipPython’sspecialmethods

Cocos2dimplementsactionsequencesbyemulatingnumericoperators,butthesecouldhavebeenimplementedwithnormalmethods.Inprograming,thiskindofshortcutofferedbyalanguageiscalledsyntacticsugar,anditsuccinctlyexpressesafunctionalitythatcanalsobeimplementedinamoreverbosemanner.

CustomactionsWehaveafulllistofactionsincludedinthelibrary,butwhatifnoneoftheseactionsperformthevisualeffectthatwedesireandwewanttodefineanewaction?

Wecancreateinstantorintervalactionsbyextendingthecorrespondingclass.Forourgame,wewantanactionthatindicatesvisuallythatanenemytankhasbeenhitbyapplyingaredfiltertothetank,anditshouldgraduallyreturntotheoriginalcolor.

Inthiscase,wedefineanIntervalActionsubclasscalledHit,keepinginmindthatthefollowingstepswillbeperformedinternallybycocos2d:

Theinit(*args,**kwargs)iscalled.Oneofthesekeywordargumentsshouldbethedurationoftheaction,soyoucansetitasyourdurationattribute.Donotconfusethiswiththe__init__specialmethod.Acopyoftheinstanceismade;usually,thisshouldnothaveanysideeffect.Thestart()iscalled.Fromhere,theself.targetattributecanbeused.Theupdate(t)iscalledseveraltimes,wheretisthetimeinthe(0,1)range.Then,update(1)iscalled.Thestop()iscalled.

Itisnotnecessarytooverrideallofthesemethods,butonlythosethatarerequiredtoimplementyouraction.Forinstance,weonlyneedtostorethedurationandupdatethecolorofthespritedependingontheelapsedtime,t:

classHit(ac.IntervalAction):

definit(self,duration=0.5):

self.duration=duration

defupdate(self,t):

self.target.color=(255,255*t,255*t)

Whenupdate(0)isinvoked,thesprite’scolorwillbe(255,0,0),whichlooksasifaredfilterisbeingappliedtotheimage.Theredtonewilldecreaseastheelapsedtime,t,risesmonotonically.

Sinceweknowthatupdate(1)willbecalled,thefinalcolorofthetargetwillbe(255,255,255),andthereisnoneedtoresettheinitialcolorofthesprite.

Thefollowingsnippetshowshowthisnewactioncanbeused:

sprite=cocos.sprite.Sprite('tank.png',position=(200,200))

sprite.do(ac.MoveBy((100,0),3)+Hit()+ac.MoveBy((50,0),2))

IntheChapter3_01.pyscript,youcanseethissnippetwithalloftheusualcoderequiredtorunthescene.

AddingamainmenuTheSpaceInvadersversionthatwedevelopedinthepreviouschapterstartsanewgameassoonastheapplicationisloaded.Inmostgames,aninitialscreenisdisplayedandtheplayercanchoosebetweendifferentoptionsapartfromstartinganewgame,suchaschangingthedefaultcontrolsortakingalookatthehighscores.

Thecocos.menucocos2dmoduleoffersaLayersubclassnamedMenu,whichservesexactlythispurpose.Byextendingit,youcanoverrideits__init__methodandsetthestyleofthetitle,themenuitems,andtheselectedmenuitem.

TheseitemsarerepresentedasalistofMenuIteminstances.Oncethislistisinstantiated,youcancallthecreate_menumethod,whichbuildsthefinalmenuwiththeactionsthatareexecutedwhenamenuitemisselected.

WhilethebasicMenuItemonlydisplaysastaticlabel,thereareseveralMenuItemsubclassesfordistinctinputmodes:

ToggleMenuItem:TogglesaBooleanoptionMultipleMenuItem:SwitchesbetweenmultiplevaluesEntryMenuItem:ThisisthemenuitemforenteringatextinputImageMenuItem:ShowsaselectableimageinsteadofatextlabelColorMenuItem:Thisisthemenuitemforselectingacolor

AlloftheseclassesexceptImageMenuIteminvokeacallbackfunctionwhenitsvaluechangestothisnewvalueasthefirstargument.

Atypicalusagecouldbethefollowing:

importcocos

fromcocos.menuimport*

importpyglet.app

classMainMenu(Menu):

def__init__(self):

super(MainMenu,self).__init__('Samplemenu')

self.font_title['font_name']='TimesNewRoman'

self.font_title['font_size']=60

self.font_title['bold']=True

self.font_item['font_name']='TimesNewRoman'

self.font_item_selected['font_name']=\

'TimesNewRoman'

self.difficulty=['Easy','Normal','Hard']

m1=MenuItem('NewGame',self.start_game)

m2=EntryMenuItem('Playername:',self.set_player_name,

'JohnDoe',max_length=10)

m3=MultipleMenuItem('Difficulty:',self.set_difficulty,

self.difficulty)

m4=ToggleMenuItem('ShowFPS:',self.show_fps,False)

m5=MenuItem('Quit',pyglet.app.exit)

self.create_menu([m1,m2,m3,m4,m5],

shake(),shake_back())

Whenself.create_menuiscalled,wecanpassacocos2dactionthatisappliedwhenamenuitemisactivatedandwhenitisdeactivated.Thecocos.menumoduleincludesafewactions,andweimportedtheshake()andshake_back()actions,whichperformalittleshakemovementatthecurrentmenuitemandreturntotheoriginalstylewhentheoptionisdeselected,respectively.

Therestofthecodehasbeenomittedforbrevity;youcanfindthecompletescriptinChapter3_02.pywiththeclassmethodsthatarecalledwhenamenuitemisactivated.Notethereferencetothepyglet.app.exitfunction,whichfinalizesthecocos2dapplication.

Withjustafewlinesofcode,wehavebuiltamenuwheretheplayercaninputtheirname,setthegamedifficulty,andtoggletheFPSdisplay.Themenuitemscanbeselectedwiththearrowkeysorthemousepointer.

Thisisthetypeofmainmenuwearelookingforforourtowerdefensegame.Sincethisisonlyonelayer,wecanlateraddabackgroundlayerthatimprovesthevisualappearanceofthemenu.

NowthatyouhaveunderstoodthemenuAPI,let’smoveontothedesignofthemainscene.

TilemapsThetechniqueoftilemapsbecameasuccessfulapproachtostoringlargeamountsofinformationaboutgameworldswithsmall,reusablepiecesofgraphics.In2Dgames,tilemapsarerepresentedbyatwo-dimensionalmatrixthatreferencestoatileobject.Thisobjectcontainstherequireddataabouteachcelloftheterrain.

Theinitialsheetusedbythetilemapcontainsthe“buildingblocks”ofourscenario,anditlookslikewhatisshowninthefollowingscreenshot:

Startingfromthissimpleimage,wecanbuildagridmapinwhicheachcellisoneofthesquaresthesheetisdividedinto.

TiledMapEditorWewilluseTiledMapEditor,ausefultoolformanipulatingtiledmaps.Itcanbedownloadedforfreefromhttp://www.mapeditor.org/,anditcanberunonmostoperatingsystems,includingWindowsandMacOS.

Thissoftwareisalsowell-suitedforleveldesign,sinceyoucaneditandvisualizetheresultingworldinasimpleway.

Oncewehaveinstalledandlaunchedtheprogram,wecanloadtheimagethatwewilluseforourtilemap.Forthisgame,wehaveusedaPNGimagethatisalreadybundledwiththeMapEditorinstallation.

Inthemenubar,gotoFile|New…tocreateanewmap.Whenanewmapiscreated,youwillbepromptedforsomebasicinformation,suchasthemaporientation,mapsize,sizeofthepattern,andspacingbetweenthecells.Inourgame,wewilluseanorthogonalmapof640x480pixelsand32x32pixelsforeachcell,asshowninthefollowingscreenshot:

NowgotoMap|NewTileset…,enterthenamemap0,andloadthetmw_desert_spacing.pngimagefromtheexamplesfolder,asshowninthisscreenshot:

YoucanusethetilesshownintheTilesetviewtodrawamaplikethisone:

Oncethemapisdrawn,wecansaveitinseveralfileformats,suchasCSV,JSON,orTXT.WewillchoosetheTMXformat,whichisanXMLfilethatcanbeloadedbycocos2d.GotoFile|Saveas…tostorethetiledmapwiththisformat.

LoadingtilesThankstothecocos.tilesmodule,wecanloadourtilemapsaslayersandmanipulatethemlikeanyotherCocosNodeinstance.IfourTMXfileandthecorrespondingPNGimagearestoredintheassetsfolder,andthenameofthemapwewanttoloadis'map0',thiswouldbethecodenecessarytoloadit:

importcocos

tmx_file=cocos.tiles.load('assets/tower_defense.tmx')

my_map=tmx_file('map0')

my_map.set_view(0,0,my_map.px_width,my_map.px_height)

scene=cocos.scene.Scene(my_map)

ThescenariodefinitionOncewehaveloadedthetilemap,weneedtolinktheresultingimagewiththegameinformation.Ourscenarioclassshouldcontainthefollowing:

ThepositionswheretheturretscanbeplacedThepositionofthebunkerTheinitialpositionforenemyspawningThepaththattheenemiesmustfollowtoreachthebunker

Inthefollowingscreenshot,wecanseethisdataoverlaidontopofourTMXmap:

Therectanglesrepresenttheslotsinwhichtheplayercanplacetheturrets.Thescenariostoresonlythecentersofthesesquares,becausethegamelayerwilltranslatethesepositionsintoclickablesquares.

Thelinesovertheroadrepresentthepaththattheenemytanksmustfollow.ThismovementwillbeimplementedbychainingtheMoveByandRotateByactions.Wewilldefinetwoconstantsforrotationtowardtheleftortheright,andanauxiliaryfunctionthatreturnsaMoveByactionwhosedurationmakestheenemiesmoveuniformly:

importcocos.actionsasac

RIGHT=ac.RotateBy(90,1)

LEFT=ac.RotateBy(-90,1)

defmove(x,y):

dur=abs(x+y)/100.0

returnac.MoveBy((x,y),duration=dur)

Withtheseconstantsandtheauxiliaryfunction,wecaneasilycreatealistofactionsthat

willbechainedtorepresentthecompletelistofstepsrequiredtofollowthegreenpathshowninthepreviousscreenshot.Forinstance,thiscouldbeahypotheticalusageoftheseutilitiestocomposeachainofactions:

steps=[move(610,0),LEFT,move(0,160),LEFT,move(-415,0),

RIGHT,move(0,160),RIGHT,move(420,0)]

forstepinsteps:

actions+=step

sprite.do(actions)

Thissolutionnotonlyavoidstheneedtowriteallthestepsonebyone,butalsomakesourcodemoreexpressiveandreadable.Nowlet’sencapsulatethislogicinaclassthatwillholdthisdata.

TipDomain-specificlanguages

Thisuseofabstractionstorepresenthigh-levelconceptsiswidelyfoundinprogramming,andgamedevelopmentisnotanexception.Whenthissyntaxreachesawholedomainofspecializedfeatures,theresultinglanguageiscalledaDomain-specificLanguage(DSL).

WhilethissmalldomainonlycoversspritemovementsandcannotbeconsideredaDSLbyitself,advancedgameenginesincludespecificscriptinglanguagesthatarefocusedongamedevelopment.

ThescenarioclassLet’sstartthedefinitionofthescenariomodulebycreatinganemptyfilecalledscenario.pyandopeningitwithourfavoritetexteditor.ThismodulewillcontainthedefinitionoftheScenarioclass,whichgroupsthepreviouslydiscussedinformation,suchasthebunker’sposition,thechainofactionsthattheenemieswillfollow,andsoon:

classScenario(object):

def__init__(self,tmx_map,turrets,bunker,enemy_start):

self.tmx_map=tmx_map

self.turret_slots=turrets

self.bunker_position=bunker

self.enemy_start=enemy_start

self._actions=None

defget_background(self):

tmx_map=cocos.tiles.load('assets/tower_defense.tmx')

bg=tmx_map[self.tmx_map]

bg.set_view(0,0,bg.px_width,bg.px_height)

returnbg

Toretrievethesequenceofactions,wewillusethe@propertydecorator.Itallowsustoaccesstoanattributebyinvokingfunctionsthatwillactasgettersandsetters:

@property

defactions(self):

returnself._actions

@actions.setter

defactions(self,actions):

self._actions=ac.RotateBy(90,0.5)

forstepinactions:

self._actions+=step

Thisdecoratorwrapstheaccesstotheinternal_actionsmember.Givenaninstanceofthisclassnamedscenario,theretrievalofthescenario.actionsmemberwouldtriggerthegetterfunction,whileanassignmenttoscenario.actionswouldtriggerthesetterfunction.

Nowwecandefineafunctionthatinstantiatesascenarioandsetitsmembersbasedonthedispositionofour'map0'.Ifwehaddesignedmorelevels,wecouldhaveaddedmorefunctionsthatcreatethesenewscenarios:

defget_scenario():

turret_slots=[(192,352),(320,352),(448,352),

(192,192),(320,192),(448,192),

(96,32),(224,32),(352,32),(480,32)]

bunker_position=(528,430)

enemy_start=(-80,110)

sc=Scenario('map0',turret_slots,

bunker_position,enemy_start)

sc.actions=[move(610,0),LEFT,move(0,160),

LEFT,move(-415,0),RIGHT,

move(0,160),RIGHT,move(420,0)]

returnsc

Tousethismodulefromanotherone,wewillimportitwiththisstatement:fromscenarioimportget_scenario.Thenextscriptthatwewillwriteisresponsiblefordefiningourgame’smainmenu.

TransitionsbetweenscenesInaprevioussection,youlearnedhowtocreateamenuwithcocos2d’sbuilt-inclasses.Themenuofourtowerdefensegamewillbeverysimilar,sinceitfollowsthesamesteps.Itwillbeimplementedinaseparatemodule,namedmainmenu:

classMainMenu(cocos.menu.Menu):

def__init__(self):

super(MainMenu,self).__init__('TowerDefense')

self.font_title['font_name']='Oswald'

self.font_item['font_name']='Oswald'

self.font_item_selected['font_name']='Oswald'

self.menu_anchor_y='center'

self.menu_anchor_x='center'

items=list()

items.append(MenuItem('NewGame',self.on_new_game))

items.append(ToggleMenuItem('ShowFPS:',self.show_fps,

director.show_FPS))

items.append(MenuItem('Quit',pyglet.app.exit))

self.create_menu(items,ac.ScaleTo(1.25,duration=0.25),

ac.ScaleTo(1.0,duration=0.25))

Thismenudisplaysthreemenuitems:

AnoptionforstartinganewgameAtoggleitemforshowingtheFPSlabelAquitoption

Tostartanewgame,wewillneedtheinstanceofthemainscene.Itwillbereturnedbycallingthenew_gamefunctionofmainscene.py,whichhasnotbeendevelopedyet.Besides,wewilladdatransitionwiththeFadeTRTransitionclassfromthecocos.scenes.transitionsmodule.

Atransitionisascenethatperformsavisualeffectbeforesettingthecontrolofanewscene.Itreceivesthissceneasitsfirstargumentandsomeoptions,suchasthetransitiondurationinseconds:

fromcocos.scenes.transitionsimportFadeTRTransition

frommainsceneimportnew_game

game_scene=new_game()#Instanceofcocos.scene.Scene

transition=FadeTRTransition(game_scene,duration=2)

Now,toreplacethecurrentscenewiththenewonedecoratedwithatransition,wecalldirector.push.Inthisway,theon_start_menumethodofourMainMenuclassisimplemented:

defon_new_game(self):

director.push(FadeTRTransition(new_game(),duration=2))

Thevisualeffectproducedbythistransitionisthatthecurrentscene’stilesfadefromtheleft-bottomcornertothetop-rightcorner,asshownhere:

Youcanfindthecompletecodeofthismoduleinthemainmenu.pyscript.

GameovercutsceneTocreateacutscenewhenthegameisover,wewillneedasimplelayerandatextlabel.SinceasceneisaCocosNode,wecanapplyactionstoit.Toholdthisscreenforamoment,itwillperformaDelayactionthatlasts3seconds,andthenitwilltriggeraFadeTransitiontothemainmenu.

Wewillwrapthesestepsinaseparatefunction,whichwillbepartofanothernewmodule,namedgamelayer.py:

defgame_over():

w,h=director.get_window_size()

layer=cocos.layer.Layer()

text=cocos.text.Label('GameOver',position=(w*0.5,h*0.5),

font_name='Oswald',font_size=72,

anchor_x='center',anchor_y='center')

layer.add(text)

scene=cocos.scene.Scene(layer)

new_scene=FadeTransition(mainmenu.new_menu())

func=lambda:director.replace(new_scene)

scene.do(ac.Delay(3)+ac.CallFunc(func))

returnscene

Thisfunctionwillbecalledfromthegamelayerwhenthebunker’shealthpointsdecreasetozero.

ThetowerdefenseactorsWewilldefineseveralclassesintheactors.pymoduletorepresentthegameobjects.

Aswithourpreviousgame,wewillincludeabaseclass,fromwhichtherestoftheactorclasseswillinherit:

importcocos.sprite

importcocos.euclidaseu

importcocos.collision_modelascm

classActor(cocos.sprite.Sprite):

def__init__(self,img,x,y):

super(Actor,self).__init__(img,position=(x,y))

self._cshape=cm.CircleShape(self.position,

self.width*0.5)

@property

defcshape(self):

self._cshape.center=eu.Vector2(self.x,self.y)

returnself._cshape

IntheActorimplementationinthepreviouschapter,whenthespritewasdisplacedbycallingthemove()method,bothcshapeandpositionwereupdatedatthesametime.However,actionssuchasMoveByonlymodifythespriteposition,andtheCShapecenterisnotupdated.

Tosolvethisissue,wewraptheaccesstotheCShapememberthroughthecshapeproperty.Withthisconstruct,whentheactor.cshapevalueisread,theinternal_cshapeisupdatedbysettingitscenterwiththecurrentspriteposition.

ThissolutionispossiblebecauseanobjectonlyneedsacshapememberinordertobeavalidentityforaCollisionManager,andapropertyisexposedinthesamewayasanyotherattributethatisdirectlyaccessed.

Nowthatouractorbaseclasshasbeendefined,wecanstartimplementingtheclassesfortheturretsandtheenemytanks.

TurretsandslotsTurretsaregameobjectsthatactautonomously;thatis,thereisnoneedtobindinputeventswiththeiractions.Thecollisionshapeisnotthespritesizebutthefiringrange.Therefore,acollisionbetweenaturretandatankmeansthattheenemyisinsidethisrangeandcanbeconsideredavalidtarget:

classTurret(Actor):

def__init__(self,x,y):

super(Turret,self).__init__('turret.png',x,y)

self.add(cocos.sprite.Sprite('range.png',opacity=50,

scale=5))

self.cshape.r=125.0

self.target=None

self.period=2.0

self.reload=0.0

self.schedule(self._shoot)

Theshootinglogicisimplementedbyschedulingafunctionthatincrementsthereloadcounter.Whenthesumofelapsedsecondsreachestheperiodvalue,thecounterisdecreasedandtheturretcreatesashootspritewhoseaimisatthecurrenttarget:

def_shoot(self,dt):

ifself.reload<self.period:

self.reload+=dt

elifself.targetisnotNone:

self.reload-=self.period

offset=eu.Vector2(self.target.x-self.x,

self.target.y-self.y)

pos=self.cshape.center+offset.normalized()*20

self.parent.add(Shoot(pos,offset,self.target))

Apartfromsettingthetarget,acollisionwiththecircularshapethatrepresentsthefiringrangealsorotatestheturret,givingtheimpressionthatitisaimingatthetarget.

Tocalculatetheanglebywhichthespritemustrotate,wecalculatethevectorthatrunsfromtheturrettothetarget.Withtheatan2functionfromthemathmodule,wecalculatetheanglebetweentheπand-πradiansformedbythepositivexaxisandthisvector.Finally,wechangethesignoftheangleandconvertitfromradianstodegrees:

defcollide(self,other):

self.target=other

ifself.targetisnotNone:

x,y=other.x-self.x,other.y-self.y

angle=-math.atan2(y,x)

self.rotation=math.degrees(angle)

Ashootisnotanactor,sinceitisnotrequiredtobeabletocollide.Throughasequenceofactions,itwillmovefromtheturrettothetank’spositionandhitthetargetinstance:

classShoot(cocos.sprite.Sprite):

def__init__(self,pos,offset,target):

super(Shoot,self).__init__('shoot.png',position=pos)

self.do(ac.MoveBy(offset,0.1)+

ac.CallFunc(self.kill)+

ac.CallFunc(target.hit))

ApartfromTurretandShoot,wewillneedaclasstorepresenttheslotsinwhichtheturretscanbeplaced.SincewewilltakeadvantageoftheCollisionManagerfunctionalitytodetectwhetheraCShapehasbeenclickedon,thisclassonlyneedsacshapemember:

classTurretSlot(object):

def__init__(self,pos,side):

self.cshape=cm.AARectShape(eu.Vector2(*pos),side*0.5,side*0.5)

InstancesofthisclasswillbeaddedtoadifferentCollisionManagersothattheydonotconflictwiththerestofthegameobjects.

EnemiesAnenemytankisanactorthatfollowsthescenariopathuntilitreachesthebunkerattheendoftheroad.Itisdestroyediftheturretshotsreduceitshealthpointstozero.

Theplayerincrementstheirscoreeachtimeatankisdestroyed,soapartfromhealth,wewillneedascoreattributetoindicatehowmanypointstheplayerearns:

classEnemy(Actor):

def__init__(self,x,y,actions):

super(Enemy,self).__init__('tank.png',x,y)

self.health=100

self.score=20

self.destroyed=False

self.do(actions)

Sincewemustdifferentiatebetweenwhetherthetankhasexplodedbecauseithasbeendefeatedbytheturretsorbecauseithasreachedtheendpoint,wewilluseadestroyedflag.Thisflagwillbesettotrueonlyifthetankisdestroyedbyaturret.

WealsochecktheCocosNodeflagcalledis_running,sinceitissettofalsewhenthenodeisremoved.Thus,wecanpreventthetankfrombeingremovedwhenithasalreadybeenkilled:

defhit(self):

self.health-=25

self.do(Hit())

ifself.health<=0andself.is_running:

self.destroyed=True

self.explode()

defexplode(self):

self.parent.add(Explosion(self.position))

self.kill()

LiketheanimationsofourSpaceInvadersgame,anexplosionwillbesimulatedwithafastsequenceofspritesthatlastsfor0.07seconds.Toavoidhavingtoomanyspriteinstancesinourlayer,thekill()methodiscalled1secondaftertheobjectinstantiation:

raw=pyglet.image.load('explosion.png')

seq=pyglet.image.ImageGrid(raw,1,8)

explosion_img=Animation.from_image_sequence(seq,0.07,False)

classExplosion(cocos.sprite.Sprite):

def__init__(self,pos):

super(Explosion,self).__init__(explosion_img,pos)

self.do(ac.Delay(1)+ac.CallFunc(self.kill))

BunkerYoumightrememberthedescriptionofthegameplay.Weneedtokeeptheenemiesawayfromaspecificarea.Inourversion,itisrepresentedbyabunkerthatisplacedattheendoftheroad.

Thebunkerinstanceonlyneedstoprocesstheenemycollisionsanddecreaseitshealthpoints.Theinitialnumberofhealthpointsis100,andeachcollisionsubtracts10pointsfromthistotal:

classBunker(Actor):

def__init__(self,x,y):

super(Bunker,self).__init__('bunker.png',x,y)

self.hp=100

defcollide(self,other):

ifisinstance(other,Enemy):

self.hp-=10

other.explode()

ifself.hp<=0andself.is_running:

self.kill()

AswedidwiththeTankclass,wecheckwhethertheCocosNodeflagis_runningissettotruetoavoidcallingkill()whenthebunkerhasalreadybeenremoved.

GamesceneThegamelayercontainsseveralattributesforholdingthereferencetotheHUDlayer,thescenario,andthegameinformation,suchasthescoreorthenumberofpointsthatcanbespenttobuildnewturrets.

Theseclassesareaddedtothegamelayermodule,whichhascontainedonlythegameovertransitionsofar:

classGameLayer(cocos.layer.Layer):

def__init__(self,hud,scenario):

super(GameLayer,self).__init__()

self.hud=hud

self.scenario=scenario

self.score=self._score=0

self.points=self._points=40

self.turrets=[]

w,h=director.get_window_size()

cell_size=32

self.coll_man=cm.CollisionManagerGrid(0,w,0,h,

cell_size,

cell_size)

self.coll_man_slots=cm.CollisionManagerGrid(0,w,0,h,

cell_size,

cell_size)

forslotinscenario.turret_slots:

self.coll_man_slots.add(actors.TurretSlot(slot,

cell_size))

self.bunker=actors.Bunker(*scenario.bunker_position)

self.add(self.bunker)

self.schedule(self.game_loop)

Anotherdifferencefromourpreviouscocos2dgameistheusageoftwocollisionmanagers:coll_manforthegameactorsandcoll_man_slotsfortheturretslots.Whilethefirstoneisupdatedduringeachiterationofthegameloop,thesecondonecontainsstaticshapesthatdonotconflictwiththeactors’collisions.Asaresult,weavoidunnecessaryadditionsandremovalsfromtheturretslotshapes;thus,improvingthecollisioncheckingperformance.

Bothpointsandscorearepropertiesthat,apartfromaccessingthe_pointsand_scoreinternalattributes,updatetheHUDwiththenewnumericvalue.Here,wewillshowthepointsproperty,butscorehasasimilarimplementation:

@property

defpoints(self):

returnself._points

@points.setter

defpoints(self,val):

self._points=val

self.hud.update_points(val)

Thegameloopisquitesimplesincethelogicofthegameismainlyimplementedwithactions:

defgame_loop(self,_):

self.coll_man.clear()

forobjinself.get_children():

ifisinstance(obj,actors.Enemy):

self.coll_man.add(obj)

forturretinself.turrets:

obj=next(self.coll_man.iter_colliding(turret),None)

turret.collide(obj)

forobjinself.coll_man.iter_colliding(self.bunker):

self.bunker.collide(obj)

ifrandom.random()<0.005:

self.create_enemy()

Thesearethestatementsperformedforeachframe:

Itupdatesthecollisionmanagerwiththepositionsoftheenemytanks.Foreachturret,wecheckwhetherthereisanyenemywithinthefiringrange.Ifso,wecallthecollide()methodoftheTurretclass.Wealsocheckwhetheracollisionwiththebunkerhasoccurred.Sincetheonlyentitiesmanagedbyself.coll_manaretheenemytanks,wedonothavetoworryaboutverifyingthattheotherobjectisatankandnotaturret.Enemiesarerandomlyspawnedwithagivenprobability.Weleftastaticvalue,butitcouldhavebeencalculateddependingonthenumberofenemiesdefeatedsothatthegamebecomesincreasinglymoredifficult.

Thecreate_enemy()methodplacesanewtankattheinitialposition.Topreventthemfromalwaysspawningatthesamecoordinates,wewillapplyarandomoffsetof+/-10pixelsinboththexandycomponents:

defcreate_enemy(self):

enemy_start=self.scenario.enemy_start

x=enemy_start[0]+random.uniform(-10,10)

y=enemy_start[1]+random.uniform(-10,10)

self.add(actors.Enemy(x,y,self.scenario.actions))

Thelayerwillprocessuserinputasusual,andwewillregisteronlyonemethodtoprocessmouseevents.Withtheobjs_touching_point()method,wewillknowwhetheranyslothasbeenclickedon.Iftheplayerhasenoughpoints,anewturretinstanceisplacedatthecenteroftheslot’sposition:

is_event_handler=True

defon_mouse_press(self,x,y,buttons,mod):

slots=self.coll_man_slots.objs_touching_point(x,y)

iflen(slots)andself.points>=20:

self.points-=20

slot=next(iter(slots))

turret=actors.Turret(*slot.cshape.center)

self.turrets.append(turret)

self.add(turret)

Finally,weoverridetheremove()methodtodetectwhichtypeofobjecthasbeenremoved:

defremove(self,obj):

ifobjisself.bunker:

director.replace(SplitColsTransition(game_over()))

elifisinstance(obj,actors.Enemy)andobj.destroyed:

self.score+=obj.score

self.points+=5

super(GameLayer,self).remove(obj)

Ifthenodeisthebunker,itmeansthattheplayerhaslostthegame,andthedirectorreplacesthecurrentscenewiththeGameOvercutscene.Ifthenodeisanenemytank,thescoreandthepointsareupdatedbeforeactuallyremovingtheobject.

TheHUDclassThislayerisresponsiblefordisplayingthegameinformation,afunctionalitysimilartotheHUDwebuiltinourpreviousgame:

classHUD(cocos.layer.Layer):

def__init__(self):

super(HUD,self).__init__()

w,h=director.get_window_size()

self.score_text=self._create_text(60,h-40)

self.score_points=self._create_text(w-60,h-40)

def_create_text(self,x,y):

text=cocos.text.Label(font_size=18,font_name='Oswald',

anchor_x='center',anchor_y='center')

text.position=(x,y)

self.add(text)

returntext

defupdate_score(self,score):

self.score_text.element.text='Score:%s'%score

defupdate_points(self,points):

self.score_points.element.text='Points:%s'%points

AssemblingthesceneToconcludeourmainscene.pymodule,wewilldefinethenew_game()function.Thisfunctionisusedbythemainmenuwhenanewgameisstarted.Itreturnsthescenewiththetilemap,theHUDlayer,andthegamelayerinitializedanddisplayedinthecorrectorder:

defnew_game():

scenario=get_scenario()

background=scenario.get_background()

hud=HUD()

game_layer=GameLayer(hud,scenario)

returncocos.scene.Scene(background,game_layer,hud)

Thegamewillbestartedwiththeconditionalblockthatwesawinourpreviousgames.Apartfrominitializingthedirectorandrunningthescene,wewillloadtheOswaldfontandaddtheimagestotheresourcepathwiththePygletAPI:

fromcocos.directorimportdirector

importpyglet.font

importpyglet.resource

frommainmenuimportnew_menu

if__name__=='__main__':

pyglet.resource.path.append('assets')

pyglet.resource.reindex()

pyglet.font.add_file('assets/Oswald-Regular.ttf')

director.init(caption='TowerDefense')

director.run(new_menu())

Thisissavedinthetowerdefense.pyscript.Thefollowingscreenshotshowsthefinalprojectlayout:

SummaryWiththisproject,wesawhowtoincludemenus,transitions,andactionsinourcocos2dapplications.Ourcodeissplitintomultiplemodules,enhancingabetterorganizationofourgames.

Thisgamecanbethebaseforamorecomplextowerdefensegame.Youcancustomizethenumberofpointsrequiredtocreateanewturret,orchangetheprobabilityofspawningenemies.Asanexercise,trytocreateanewTMXmapwithMapEditoranddefineacustomscenariobasedonthisbackground.Yourimaginationisthelimit!

Chapter4.SteeringBehaviorsOurpreviouscocos2dgamewasbasedonactions,somovementsandrotationswerepredefinedandthecharacterswerenotinfluencedbythecurrentstateofthegame.However,arecurringproblemingamedevelopmentishowtorecreatelife-likeanimations,suchaspursuingatargetoravoidingmovingobstacles.

Now,youwilllearnhowtoapplysteeringbehaviors,atechniqueusedtocreateseeminglyintelligentmovementsforautonomouscharacters.Theimplementationofthesestrategiesachievestheabilitytonavigatethroughthegameworldwithimprovisedpatterns.

Finally,wewillputthesestrategiesinpracticewithparticlesystems,aCocos2dmodulethatwehavenotworkedwithsofar.Sincewewillusesimpleshapes,theseparticlesystemswillrepresentourcharacters,withtheadvantageofusnotrequiringexternalassetsforourapplications.

Inthischapter,wewillcoverthesetopics:

BasicconceptsofsteeringbehaviorsBehaviorsforindividualsandgroupsMixingthesestrategiesHowtorenderparticlesystemswithcocos2d

NumPyinstallationTousetheparticlesystemsupportofcocos2d,itisnecessarytoinstallNumPy,aPythonpackageusedtooperatewithlargearraysandmatricesinanefficientway.SinceitcontainsseveralCmodules,itmightbedifficulttoinstallitonWindowssystemsbecauseyoumightnothavetheappropriatecompiler.

YoucandownloadtheofficialbinariesforWindowsandMacOSXfromtheNumPysiteathttp://sourceforge.net/projects/numpy/files/NumPy/1.9.2/.

AnotheroptionistodownloadtheunofficialcompiledbinariesfromChristophGohlke’swebsiteathttp://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy.Here,thepackagesareuploadedas.whlfiles.Thisistheextensionofthewheelformatandcanbeinstalledwithpip:

$pipinstallnumpy‑1.9.2+mkl‑cp34‑none‑win32.whl

Inbothcases,remembertoinstallthebinariesforPython3.4,sincetheversionsforPython2.7and3.3areavailablefordownloadaswell.

Youcancheckwhethertheinstallationwassuccessfulbyrunningthiscommand:

$python-c"importnumpy;print(numpy.version.version)"

1.9.2

OnceNumPyisinstalled,youhavealltherequirementsforsupportingparticlesystemsincocos2d.

TheParticleSystemclassThebaseclassforcocos2dparticlesystemsisParticleSystem,asdefinedinthecocos.particlemodule.Thefollowingtablelistssomeofthemostrelevantclassmembers.Notethattheonesendingin_varindicatethatrandomvariancecanbeappliedtothebasevalue.

Classmember Description

Active Indicateswhethertheparticlesystemisspawningnewparticlesornot.

DurationThedurationofthesysteminseconds.Thisvalueis-1forinfiniteduration.

Gravity Gravityoftheparticles.

angle,angle_var Theangulardirectionoftheparticlesmeasuredindegrees.

speed,speed_var Thespeedoftheparticles.

tangential_accel,

tangential_accel_valTangentialacceleration.

radial_accel,radial_accel_var Radialacceleration.

size,size_var Thesizeoftheparticles.

life,life_var Thetimeinsecondsforwhicheachparticlewilllive.

start_color,start_color_var Thestartcoloroftheparticles.

end_color,end_color_val Theendcoloroftheparticles.

total_particles Themaximumnumberofparticles.

Youcancreateyourownparticlesystemsbyextendingthisclassandredefiningthevaluesoftheseclassmembers.

Cocos2dincludesanothermodule,cocos.particles_systems,withsomepredefinedParticleSystemsubclasses,eachproducingadifferentvisualeffect.ThenamesoftheseclassesareFireworks,Spiral,Meteor,Sun,Fire,Galaxy,Flower,Explosion,andSmoke.

AquickdemonstrationThiscodeisabasicexamplethatshowshowtoaddastaticParticleSystemtoourlayerwithoutprocessinganyuserinputoractions:

importcocos

importcocos.particle_systemsasps

classMainLayer(cocos.layer.Layer):

def__init__(self):

super(MainLayer,self).__init__()

particles=ps.Spiral()

particles.position=(320,240)

self.add(particles)

if__name__=='__main__':

cocos.director.director.init(caption='Particlesexample')

scene=cocos.scene.Scene(MainLayer())

cocos.director.director.run(scene)

ParticleSysteminheritsfromCocosNode.Therefore,theinstancehastheusualmembers’position,rotation,andscale,aswellasthedo()andkill()methods.

Runningthisscriptshowsthefollowingpredefinedparticlesystem:

Youcanreplaceps.Spiral()withotherbuilt-inparticlesystems,suchasps.Galaxy()orps.Fireworks().ManyIntegratedDevelopmentEnvironments(IDE),suchasPyCharmorPyDevm,offercodecompletionandwilllistallthenamesthataredefinedinamoduleaftertypingitsname.

ImplementingsteeringbehaviorsThestrategiesthatwewillcoverherehavebeentakenfromCraigReynolds’spaperSteeringBehaviorsforAutonomousCharacters,writtenin1999.Ithasbecomeawell-knownreferenceforimplementingautonomousmotioninaneasymannerfornon-playablecharacters.

Youcancheckouttheonlineversionathttp://www.red3d.com/cwr/steer/gdc99/.Inthissection,youwilllearnhowtoimplementthefollowingkindsofbehavior:

SeekandfleeArrivalPursuitandevadeWanderObstacleavoidance

SeekandfleeTheseekbehaviormovesthecharactertowardsaspecificpositioninthespace.Thisbehaviorisbasedonthecombinationoftwoforces:thecharacter’svelocityandthesteeringforce.Thisforceiscalculatedasthedifferencebetweenthedesiredvelocity(thedirectionfromthecharactertothetarget)andthecharacter’scurrentvelocity.

Notethatthisisnotaforceinthestrictphysicsdefinition;itisjustanothervelocityvector.Youcanthinkofitasacorrectionofthecharacter’svelocity,andtheresultingpathwillbeasmoothcurvethatadjuststhevelocityuntilitisalignedtowardsthetarget.

Fleeistheinverseofseek,anditmakesthecharactermoveawayfromthetarget.Itmeansthatthesteeringforcepushesthecharacterawayfromthetarget.

Youcanseebothseekandfleerepresentedgraphicallyinthefollowingdiagram.Thecurvedpathsshowthefinaldirectiononcetheforcesarecombined,therightonetowardsthetarget(seek),andtheleftoneawayfromthetarget(flee).

Forourimplementation,weneedaclassthatrepresentstheautonomouscharacterwiththefollowingmembers:

velocity:Atwo-dimensionalvectorrepresentingtheactor’svelocityspeed:Theactor’sspeed,measuredinthenumberofpixelsperframemax_force:Themaximummagnitudeofthesteeringforcemax_velocity:Themaximummagnitudeofthevelocityvectortarget:Thepositionthattheactortriestoreach

Besides,toputourparticlesystemsintopractice,wewillextendCocosNodeinsteadof

SpriteandrepresentouractorwiththeSunparticlesystem:

importcocos

importcocos.euclidaseu

importcocos.particle_systemsasps

classActor(cocos.cocosnode.CocosNode):

def__init__(self,x,y):

super(Actor,self).__init__()

self.position=(x,y)

self.velocity=eu.Vector2(0,0)

self.speed=2

self.max_force=5

self.max_velocity=200

self.target=None

self.add(ps.Sun())

self.schedule(self.update)

Theupdate()methodwillcomputethenewpositionofthecharacterforeachframe,basedonthecurrentvelocityandthetargetthatitisseeking:

defupdate(self,dt):

ifself.targetisNone:

return

distance=self.target-eu.Vector2(self.x,self.y)

steering=distance*self.speed-self.velocity

steering=truncate(steering,self.max_force)

self.velocity=truncate(self.velocity+steering,

self.max_velocity)

self.position+=self.velocity*dt

Stepbystep,weperformthefollowingoperations:

Calculatethedistancetothetarget.Wescalethisdistanceagainstthespeedandsubtractthecurrentvelocity.Thisgivesusthesteeringforce.Truncatethisforcewiththemaximumforce.Sumthesteeringforcewiththecurrentvelocityandlimitittothemaximumvelocitythatcanbereached.Updatethepositionwiththevelocityperframe.

Thisfigureshowshowthesevectorsareaddedtoachievetheresultingpathtothetarget:

Thetruncate()functionlimitsthemagnitudeofavector,preventingthevelocityfromreachingagreatermodulethanallowed:

deftruncate(vector,m):

magnitude=abs(vector)

ifmagnitude>m:

vector*=m/magnitude

returnvector

Ifthevectormoduleisgreaterthanthemaximumvalue,thevectorisnormalizedandscaledbythisvalue.

Thecharacter’stargetwillbethemousepointer,andthetargetcoordinatesmustbeupdatedwhenthemouseismoved.Todoso,wewilldefineacustomlayerthathandlesthemousemotioneventsandsetstheactor’starget:

classMainLayer(cocos.layer.Layer):

is_event_handler=True

def__init__(self):

super(MainLayer,self).__init__()

self.actor=Actor(320,240)

self.add(self.actor)

defon_mouse_motion(self,x,y,dx,dy):

self.actor.target=eu.Vector2(x,y)

if__name__=='__main__':

cocos.director.director.init(caption='SteeringBehaviors')

scene=cocos.scene.Scene(MainLayer())

cocos.director.director.run(scene)

Whenyourunthiscode,whichispresentintheseek.pyscript,thecharacterwillseekthemousepointerwhenyoumoveitoverthewindow.

Forthefleebehavior,simplychangethesignofthevelocitywhenthepositionisupdated:

defupdate(self,dt):

ifself.targetisNone:

return

#...

self.position+=self.velocity*dt*-1

Thiswillmakethecharacterfleefromthemousepointer,andeventuallyleavethewindowifthemouseisclosertothecenterthanthecharacter.Intheseek_and_flee.pyscript,youcanfindacombinationoftheseimplementations,whereitispossibletoswitchthemwiththemouseclick.

ArrivalYoumaynoticethatwiththeseekbehavior,ifthecharacter’svelocityistoohigh,itwillpassthroughthetargetandcomebacksmoothly.

Thearrivalbehaviorlowersthevelocityoncethecharacterhasreachedaminimumdistanceclosetothetarget,decreasingitgradually.Thisdistancegivesustheradiusoftheslowingarea,acirclecenteredatthetargetpositionthatcausesthecharacter’svelocitytodecrease.

Theimplementationofthisisquitesimilartothatoftheseekbehavior.Weneedtoaddtheslow_radiusmembertoindicatetheradiusoftheslowingarea:

classActor(cocos.cocosnode.CocosNode):

def__init__(self,x,y):

super(Actor,self).__init__()

self.position=(x,y)

self.slow_radius=200

self.velocity=eu.Vector2(0,0)

#...

Withthisattribute,wecanmodifyourupdate()methodsothatthesteeringforceisdecreasedlinearlybytherampfactor.Whenthedistanceisgreaterthantheslowradius,therampfactorisataminimumof1.0,sothesteeringforceremainsunmodified:

defupdate(self,dt):

ifself.targetisNone:

return

distance=self.target-eu.Vector2(self.x,self.y)

ramp=min(abs(distance)/self.slow_radius,1.0)

steering=distance*self.speed*ramp-self.velocity

steering=truncate(steering,self.max_force)

self.velocity=truncate(self.velocity+steering,

self.max_velocity)

self.position+=self.velocity*dt

Notethatwhenthedistanceapproaches0,therampfactorequals0andthesteeringforcebecomes-self.velocity.

Checkoutthearrival.pyfilewiththecompleteimplementationofthebehavior.

PursuitandevadeThepursuitbehaviorissimilartoseek,withthedifferencebeingthatthecharacterwillmovetowardsthepositionatwhichthetargetwillbeinthefuturebasedonthetarget’scurrentvelocity.

Thefuturepositionisthesumofthetargetpositionplusthevelocityvectormultipliedbyaunitoftime.Itgivesthecoordinateswherethetargetwillbeplacedwithin1secondifitdoesnotmodifyitsvelocity:

defupdate(self,dt):

ifself.targetisNone:

return

pos=self.target.position

future_pos=pos+self.target.velocity*1

distance=future_pos-eu.Vector2(self.x,self.y)

steering=distance*self.speed-self.velocity

steering=truncate(steering,self.max_force)

self.velocity=truncate(self.velocity+steering,

self.max_velocity)

self.position+=self.velocity*dt

Ifyouwanttosetthefuturepositionasthecoordinateswherethetargetwillbewithin2seconds,multiplythetarget’svelocityby2.

Inthisexample,wewillreplaceourmousepointertargetwithamovingnode.Itwillhavealinearvelocity,andouractorcanusethisvelocitytocalculatethefutureposition:

classMainLayer(cocos.layer.Layer):

def__init__(self):

super(MainLayer,self).__init__()

self.target=ps.Sun()

self.target.position=(40,40)

self.target.start_color=ps.Color(0.2,0.7,0.7,1.0)

self.target.velocity=eu.Vector2(50,0)

self.add(self.target)

self.actor=Actor(320,240)

self.actor.target=self.target

self.add(self.actor)

self.schedule(self.update)

defupdate(self,dt):

self.target.position+=self.target.velocity*dt

Ontheotherhand,fortheevadebehavior,weneedtochangeonlythelaststatementoftheupdate()methodtoself.position+=self.velocity*dt*-1,exactlyaswedidforthefleebehavior.

Thecodeofthisbehaviorisincludedinthepursuit.pyscript.

WanderThewandersteeringsimulatesarandomwalkwithoutanyspecifictarget.Itproducesacasualmovementwithoutsharpturnsorpredictablepaths.

Itcanbeimplementedwithaseekbehaviorwithtargetscalculatedrandomly.However,amoreorganicsolutionistogeneratearandomsteeringforcetowardsapointonacircumferenceplacedaheadofthecharacter.

Thisforceiscalculatedperframe,givingsmallrandomdisplacementsthatproducethevisualeffectofthecharacterwanderingaround.Thisbehaviorcanbeparameterizedwiththefollowingvalues:

wander_angle:Thecurrentangleofdisplacement,towhichthesmallvariationswillbeaddedcircle_distance:Thedistanceofthecharacter’spositionfromthewandercircle’scentercircle_radius:Theradiusofthewandercircleangle_change:Thefactorbywhichtherandomvaluewillbemultipliedtoproduceachangeinthewanderangle

Nowthatwehaveseenhowtheseforcesarecalculated,wecanimplementourwanderbehavior.

Firstofall,wewilladdtheseattributestoourActorclass:

importmath

importrandom

classActor(cocos.cocosnode.CocosNode):

def__init__(self,x,y):

super(Actor,self).__init__()

self.position=(x,y)

self.velocity=eu.Vector2(0,0)

self.wander_angle=0

self.circle_distance=50

self.circle_radius=10

self.angle_change=math.pi/4

self.max_velocity=50

self.add(ps.Sun())

self.schedule(self.update)

Withthesemembers,wecanmodifyourupdate()method:

defupdate(self,dt):

circle_center=self.velocity.normalized()*\

self.circle_distance

dx=math.cos(self.wander_angle)

dy=math.sin(self.wander_angle)

displacement=eu.Vector2(dx,dy)*self.circle_radius

self.wander_angle+=(random.random()-0.5)*\

self.angle_change

self.velocity+=circle_center+displacement

self.velocity=truncate(self.velocity,

self.max_velocity)

self.position+=self.velocity*dt

self.position=(self.x%640,self.y%480)

Thesenewstatementsperformthefollowingoperations:

Thecircle’scenterisplacedatthegivendistanceaheadofthecurrentcharacter’svelocity.Wecalculatethedisplacementwithwander_angle,anditisscaledaspertheradiusofthecircle.Givenarandomvaluebetween0.0and1.0,wesubtract0.5sothatthevalueisbetween-0.5and0.5,thuspreventingtheanglefromalwayschangingbyapositivevalue.Wemultiplythisvaluebyangle_changeandaddittothewander_anglemember.

Youmaynoticeanotherstatementafterthepositionisupdatedwiththevelocity.Thislaststatementofthemethodisusedtorelocatethecharacter,sinceitisverylikelythatthecharacterwillleavethescreenduetotherandomnessofitsmovement.

Feelfreetotweakthecircle_distance,circle_radius,andangle_changevalues,andobservethedifferencesthattheycausetotheplayer’smovement.

Ourmainlayerissimplifiedsinceitonlyneedstoaddtheactor:

classMainLayer(cocos.layer.Layer):

def__init__(self):

super(MainLayer,self).__init__()

self.actor=Actor(320,240)

self.add(self.actor)

Thewander.pyscriptcontainsthecompleteimplementationofthisbehavior.

ObstacleavoidanceSofar,wehavenotplacedanybarrierinourworld,sothecharactercanfreelymovearoundwithoutavoidinganyarea.

However,mostgamesplacesomeobstacles,andthecharacterscannotpassthroughthem.Theobstacleavoidancebehaviorgeneratesasteeringforcethatcausesthecharactertotryanddodgetheseblockingitems.Notethatthisdoesnotperformcollisiondetections,soifthecurrentspeedishighandthesteeringforcemoduleisnotenoughtocounteractthecharacter’svelocity,thecharactermightoverlapwiththeshapeoftheobstacle.

Todetectthepresenceofobstaclesahead,thecharacterwillkeepanimaginaryvectoralongitsforwardaxis,whichrepresentsthedistanceuptowhichitcanseeahead.Theclosestobstaclethatcollideswiththissegment,ifany,willbethethreattoavoid.

WewilldefineanObstacleclasstorepresentthecircularobstaclesthatwewilluse.Eachofthemcanhaveadifferentradius,andwewillkeeptrackofallthecreatedinstanceswithaclassmemberlist:

classObstacle(cocos.cocosnode.CocosNode):

instances=[]

def__init__(self,x,y,r):

super(Obstacle,self).__init__()

self.position=(x,y)

self.radius=r

particles=ps.Sun()

particles.size=r*2

particles.start_color=ps.Color(0.0,0.7,0.0,1.0)

self.add(particles)

self.instances.append(self)

TheActorclasswillhavetwonewattributes:max_ahead,themaximumdistanceatwhichthecharactercandetectthepresenceofanobstacle,andmax_avoid_force,themaximumforcethatcanbeappliedtododgeanobstacle:

classActor(cocos.cocosnode.CocosNode):

def__init__(self,x,y):

super(Actor,self).__init__()

self.position=(x,y)

self.velocity=eu.Vector2(0,0)

self.speed=2

self.max_velocity=300

self.max_force=10

self.target=None

self.max_ahead=200

self.max_avoid_force=300

self.add(ps.Sun())

self.schedule(self.update)

Thesteeringforcekeepsseekingthetarget,withthedifferencethattheavoidforceisappliedaswell:

defupdate(self,dt):

ifself.targetisNone:

return

distance=self.target-eu.Vector2(self.x,self.y)

steering=distance*self.speed-self.velocity

steering+=self.avoid_force()

steering=truncate(steering,self.max_force)

self.velocity=truncate(self.velocity+steering,

self.max_velocity)

self.position+=self.velocity*dt

Theavoidforceiscalculatedwiththeclosestobstacle.Tocheckwhethertheaheadvectorintersectseachobstacle,wewillcalculatetheminimumdistancefromthecircle’scentertothevector.Ifthisdistanceislowerthanthecircle’sradiusanditistheminimumdistance,theobstacleistheclosestone.

Tofindthedistancebetweenavectorandapoint,wemustprojectthatpointtothevectorandseeifitfallsintoitslength.Ifitdoes,wecalculatethedistancebetweenthesetwopoints,whichisthedistancebetweenthevectorandthepoint.Otherwise,itmeansthatthepointisbehindorbeyondtheprojectionofthepointonthevector.

Withtheseoperationsinmind,wecandefinetheavoid_force()method:

defavoid_force(self):

avoid=eu.Vector2(0,0)

ahead=self.velocity*self.max_ahead/self.max_velocity

l=ahead.dot(ahead)

ifl==0:

returnavoid

closest,closest_dist=None,None

forobjinObstacle.instances:

w=eu.Vector2(obj.x-self.x,obj.y-self.y)

t=ahead.dot(w)

if0<t<l:

proj=self.position+ahead*t/l

dist=abs(obj.position-proj)

ifdist<obj.radiusand\

(closestisNoneordist<closest_dist):

closest,closest_dist=obj.position,dist

ifclosestisnotNone:

avoid=self.position+ahead-closest

avoid=avoid.normalized()*self.max_avoid_force

returnavoid

Thesearethestepsperformedbythismethod:

Wecalculatetheaheadvectoranditssquaredlength.Ifitiszero,itmeansthatitdoesnotseeanythingaheadandtheavoidanceforceis(0,0).Foreachobstacle,wekeeptrackofthedistancebetweentheaheadvectorandthecircle’scenter.Ifthisdistanceislowerthanitsradiusanditistheclosestobstacle,wesetitastheobstacletoavoid.Finally,ifthereisanyobstacleselected,wescaletheavoidforcebythemax_avoid_forcefactor.

WewilladdsomeobstaclestoourMainLayer,andtheactorwillavoidthemwithoutanyfurtherreferencetothesenewobjects,sincetheyareautomaticallyaddedtotheObstacle.instanceslist:

classMainLayer(cocos.layer.Layer):

is_event_handler=True

def__init__(self):

super(MainLayer,self).__init__()

self.add(Obstacle(200,200,40))

self.add(Obstacle(240,350,50))

self.add(Obstacle(500,300,50))

self.actor=Actor(320,240)

self.add(self.actor)

defon_mouse_motion(self,x,y,dx,dy):

self.actor.target=eu.Vector2(x,y)

Youcancheckouttheentirescriptintheobstacles.pyfile.Runitandmovethemousepointerabovethescreentoseehowthecharactertriestoavoidtheobstaclesrepresentedbythegreencircles.

GravitationgameToputintopracticewhatyouhavelearnedaboutsteeringbehaviors,wewillbuildabasicgame.Theplayer’sobjectiveistocollectsomepickupitemsthatrotatearounddifferentplanetsplacedintheworld,whileescapingfromenemies.Theenemiesarenon-playablecharactersthatseektheplayablecharacterandavoidtheplanets.

Wewillrepresentthecharactersasparticlesystems,andthefinalversionofthegamewilllooklikethis:

BasicgameobjectsAsusual,wewilldefineabaseclassthatwrapstheaccesstotheCircleShapeattributewiththeupdatedcenter:

fromcocos.cocosnodeimportCocosNode

fromcocos.directorimportdirector

importcocos.collision_modelascm

classActor(CocosNode):

def__init__(self,x,y,r):

super(Actor,self).__init__()

self.position=(x,y)

self._cshape=cm.CircleShape(self.position,r)

@property

defcshape(self):

self._cshape.center=eu.Vector2(self.x,self.y)

returnself._cshape

Sincesomecharacterswillrotatearoundtheplanets,wewilldefineanotherclasstoupdatethepositionoftheactorbasedonarotationalmovement.Thisclasswillhaveareferencetotheplanetitisrotatingaround,theangularspeed,andthecurrentangleofrotation:

classMovingActor(Actor):

def__init__(self,x,y,r):

super(MovingActor,self).__init__(x,y,r)

self._planet=None

self._distance=0

self.angle=0

self.rotationSpeed=0.6

self.schedule(self.update)

AccesstothePlanetmemberwillbeimplementedthroughaproperty,becausewhenwesetplanet,weneedtocalculatethedistanceandthecurrentangle:

@property

defplanet(self):

returnself._planet

@planet.setter

defplanet(self,val):

ifvalisnotNone:

dx,dy=self.x-val.x,self.y-val.y

self.angle=-math.atan2(dy,dx)

self._distance=abs(eu.Vector2(dx,dy))

self._planet=val

Thescheduledmethodincrementstherotationanglewiththeelapsedtimeandupdatestheactor’spositionwiththecosineandsineofthisangle:

defupdate(self,dt):

ifself.planetisNone:

return

dist=self._distance

self.angle+=self.rotationSpeed*dt

self.angle%=math.pi*2

self.x=self.planet.x+dist*math.cos(self.angle)

self.y=self.planet.y-dist*math.sin(self.angle)

PlanetsandpickupsOurPlanetclasswillrepresentthestaticactorsaroundwhichthepickupsandtheplayerrotate.Wewillkeeptrackofalltheinstances,aswedidwiththeObstacleclasspreviously:

classPlanet(Actor):

instances=[]

def__init__(self,x,y,r=50):

super(Planet,self).__init__(x,y,r)

particles=ps.Sun()

particles.start_color=ps.Color(0.5,0.5,0.5,1.0)

particles.size=r*2

self.add(particles)

self.instances.append(self)

Pickupswillbemovingactors,sowewillinheritfromMovingActor,whichhasimplementedthisfunctionality:

classPickupParticles(ps.Sun):

size=20

start_color=ps.Color(0.7,0.7,0.2,1.0)

classPickup(MovingActor):

def__init__(self,x,y,planet):

super(Pickup,self).__init__(x,y,10)

self.planet=planet

self.gravity_factor=50

self.particles=PickupParticles()

self.add(self.particles)

PlayerandenemiesTheplayercharacterrevolvesaroundtheplanetslikethepickupsdo,butwiththedifferencethatitcanswitchfromoneplanettoanotherwhenthespacebarispressed.

Thisisthecontrolthattheplayerhasovertheactor,sowewillneedtoaddalinearspeedtoimplementthiskindofmovement,whichextendsthelogicofferedbytheMovingActorclass:

classPlayer(MovingActor):

def__init__(self,x,y,planet):

super(Player,self).__init__(x,y,16)

self.planet=planet

self.rotationSpeed=1

self.linearSpeed=80

self.direction=eu.Vector2(0,0)

self.particles=ps.Meteor()

self.particles.size=50

self.add(self.particles)

Nowtheupdate()methodperformsalinearmovementwhentheplayerisnotrevolvingaroundanyplanet:

defupdate(self,dt):

ifself.planetisnotNone:

super(Player,self).update(dt)

gx=20*math.cos(self.angle)

gy=20*math.sin(self.angle)

self.particles.gravity=eu.Point2(gx,-gy)

else:

self.position+=self.direction*dt

Finally,weneedtoimplementthemethodthattriggerstheswitchfromoneplanettoanother.Itcalculatesthedirectionvectordependingonthecurrentangleofrotationwithrespecttotheplanet:

defswitch(self):

new_dir=eu.Vector2(self.y-self.planet.y,

self.planet.x-self.x)

self.direction=new_dir.normalized()*self.linearSpeed

self.planet=None

self.particles.gravity=eu.Point2(-self.direction.x,

-self.direction.y)

Youmaynoticethatthegravityoftheparticlesisupdatedwiththeplayer’smovement.Thisgivesvisualfeedbackofthecurrentdirectionofthecharacterandthepositionitispointingto.

TheimplementationoftheEnemyclassisalmostthesameastheonewesawwiththeobstacleavoidancebehavior.ThemaindifferencesarethatthisclassusesthePlanet.instances:listandtheradiusoftheobstaclesisretrievedfromthecshapememberofeachplanet:

defavoid_force(self):

#...

closest,closest_dist=None,None

forobjinPlanet.instances:

w=eu.Vector2(obj.x-self.x,obj.y-self.y)

t=ahead.dot(w)

if0<t<l:

proj=self.position+ahead*t/l

dist=abs(obj.position-proj)

ifdist<obj.cshape.rand\

(closestisNoneordist<closest_dist):

closest,closest_dist=obj.position,dist

ifclosestisnotNone:

avoid=self.position+ahead-closest

avoid=avoid.normalized()*self.max_avoid_force

returnavoid

ExplosionsTherearesomecollisionsthatcantriggeragameevent.Forinstance,whenanitemispickedupbytheplayer,itmustberemoved,orwhentheplayablecharacterhitsanenemyoraplanet,itisdestroyedandspawnsagaininitsoriginalposition.

Onewaytoenhancethesesituationsisthroughexplosions;theygivevisualfeedbacktotheplayer.Wewillimplementthemwithacustomparticlesystem:

classActorExplosion(ps.ParticleSystem):

total_particles=400

duration=0.1

gravity=eu.Point2(0,0)

angle=90.0

angle_var=360.0

speed=40.0

speed_var=20.0

life=3.0

life_var=1.5

emission_rate=total_particles/duration

start_color_var=ps.Color(0.0,0.0,0.0,0.2)

end_color=ps.Color(0.0,0.0,0.0,1.0)

end_color_var=ps.Color(0.0,0.0,0.0,0.0)

size=15.0

size_var=10.0

blend_additive=True

def__init__(self,pos,particles):

super(ActorExplosion,self).__init__()

self.position=pos

self.start_color=particles.start_color

ThegamelayerAllthegameobjectsthatwehavedevelopedwillbeaddedtothemaingamelayer.Asusual,itwillcontainacollisionmanagerthatcoverstheentirewindow:

classGameLayer(cocos.layer.Layer):

def__init__(self):

super(GameLayer,self).__init__()

x,y=director.get_window_size()

cell_size=32

self.coll_man=cm.CollisionManagerGrid(0,x,\

0,y,cell_size,cell_size)

self.planet_area=400

planet1=self.add_planet(450,280)

planet2=self.add_planet(180,200)

planet3=self.add_planet(270,440)

planet4=self.add_planet(650,480)

planet5=self.add_planet(700,150)

self.add_pickup(250,250,planet2)

self.add_pickup(740,480,planet4)

self.add_pickup(700,60,planet5)

self.player=Player(300,350,planet3)

self.add(self.player)

self.add(Enemy(600,100,self.player))

self.schedule(self.game_loop)

Wealsodefinedacoupleofhelpermethodstoavoidrepeatingthesamecodeduringinitialization:

defadd_pickup(self,x,y,target):

pickup=Pickup(x,y,target)

self.add(pickup)

defadd_planet(self,x,y):

planet=Planet(x,y)

self.add(planet)

returnplanet

Oncethescenarioissetupwiththepositionsofthecharacters,andwhatactorsarerotatingaroundeachplanet,wecanimplementthegameloop:

defgame_loop(self,_):

self.coll_man.clear()

fornodeinself.get_children():

ifisinstance(node,Actor):

self.coll_man.add(node)

ifself.player.is_running:

self.process_player_collisions()

defprocess_player_collisions(self):

player=self.player

forobjinself.coll_man.iter_colliding(player):

ifisinstance(obj,Pickup):

self.add(ActorExplosion(obj.position,

obj.particles))

obj.kill()

else:

self.add(ActorExplosion(player.position,

player.particles))

player.kill()

Thelayermustprocesstheinputevents,specificallythespacebarpressthattriggerswhentheplayer’scharacterstopsrotatingaroundaplanetandleavestheorbitinaperpendiculardirection.

Whenthespacebarispressedagain,thecollisionmanagercalculateswhattheclosestplanetiswithinaspecificdistance—whichwesetupinthe__init__method—andifthereisany,itissetastheplanetaroundwhichthetargetmustrotate:

is_event_handler=True

defon_key_press(self,k,_):

ifk!=key.SPACE:

return

ifself.player.planetisNone:

self.player.planet=self.find_closest_planet()

else:

self.player.switch()

deffind_closest_planet(self):

ranked=self.coll_man.ranked_objs_near(self.player,

self.planet_area)

planet=next(filter(lambdax:isinstance(x[0],Planet),

ranked))

returnplanet[0]ifplanetisnotNoneelseNone

Finally,donotforgettoaddtheconditionalblocktorunthescriptasthemainmodule.Here,wesetthewindow’swidthandheight:

if__name__=='__main__':

director.init(width=850,height=600,caption='Gravitation')

director.run(cocos.scene.Scene(GameLayer()))

Youcancheckoutthecompletegameinthegravitation.pyscript.Thegamedoesnotrequireanyextraassets,sincealltherenderedelementsarebasedonthecocos2dparticlesystems.

SummaryInthischapter,youlearnedseveralkindsofsteeringbehaviorthatproducerealisticandautonomousnavigationsaroundyourgameworld.Thesestrategiesmightbemixed,causingcomplexpatternsandseeminglymoreintelligentactions.

Trytochangethevaluesoftheconstantsusedineachbehaviortoseehowtoachievedifferentvelocitiesandforces.Keepinmindtheimportanceofgeneratingsmoothmovementsinsteadofpronouncedtwistssothattheexperienceisvisuallyengaging.

Finally,weappliedthisknowledgeinabasicgame.Sinceitisademonstrationofaddinganon-playablecharacter,thegamecanbecomplementedwithalltheingredientsthatwecoveredinthepreviouscharacter(suchasmenus,transitions,andsoon).

Inthenextchapter,wewilljumpinto3DgamedevelopmentwithOpenGL,anewtopicwithmoreadvancedfeaturesanddetailsofalowerlevel.

Chapter5.Pygameand3DInourpreviouschapters,wedevelopedour2DgameswithPythonmodulesthatarebuiltontopofagraphicaluserinterfacelibrary,suchasTkinterandPyglet.Thisallowedustostartcodingourgameswithoutworryingaboutthelower-leveldetails.

Nowwewilldevelopourfirst3DgamewithPython,whichwillrequireanunderstandingofsomebasicprinciplesofOpenGL,apopularmultiplatformAPIforbuilding2Dand3Dapplications.YouwilllearnhowtointegratetheseprogramswithPygame,aPythonlibrarycommonlyusedtocreatesprite-basedgames.

Inthischapter,wewillcoverthefollowingtopics:

AsteadyapproachtoPyOpenGLandPygameInitializinganOpenGLcontextUnderstandingthedifferentmodesthatcanbeenabledwithOpenGLHowtorenderlightsandsimpleshapesIntegratingOpenGLwithPygameDrawingprimitivesandperformanceimprovements

InstallingpackagesPyOpenGLisapackagethatoffersPythonbindingstoOpenGLandrelatedAPIs,suchasGLUandGLUT.ItisavailableonthePythonpackageIndex,soyoucaneasilyinstallitviapip:

$pipinstallPyOpenGL

However,wewillneedfreeglutforourfirstexamples,beforeweintegrateOpenGLwithPygame.Freeglutisathird-partylibrarythatisnotincludedifyouinstallthepackagefromPyPI.

OnWindows,analternativeistodownloadandinstallthecompiledbinariesfromhttp://www.lfd.uci.edu/~gohlke/pythonlibs/#pyopengl.RemembertoinstalltheversionforPython3.4.

Pygameistheotherpackagethatwewillneedinthischapter.Itcanbedownloadedfromtheofficialwebsiteathttp://www.pygame.org/download.shtml.Youcaninstallitfromsourceifyouwantto;thecompilationpagecontainsthestepsforbuildingPygameondifferentplatforms.

WindowsuserscandirectlyusetheMSIforPython3.2ordownloadUnofficialWindowsBinariesfromtheChristophGohlke’swebsite(http://www.lfd.uci.edu/~gohlke/pythonlibs/).

MacintoshuserscanfindtheinstructionsrequiredtocompileitfromsourceonthePygamewebsiteathttp://pygame.org/wiki/macintosh.

GettingstartedwithOpenGLOpenGLisabroadtopicinitself,anditispossibletofindplentyoftutorials,books,andotherresources,usuallytargetedatCorC++.

Sincethischapterisnotintendedtobeacomprehensiveguideforthisspecification,wewilltakeadvantageofGLUT,whichstandsforOpenGLUtilityToolkit.Itiswidelyusedinsmallapplicationsbecauseofitssimplicityandportability,andthebindingsareimplementedinPyOpenGL.

GLUTwillhelpusperformsomebasicoperations,suchascreatingwindowsandhandlinginputevents.

TipGLUTlicensing

Unfortunately,GLUTisnotinthepublicdomain.Thecopyrightismaintainedbyitsauthor,MarkKilgard,whowroteitforthesampleprogramsincludedinRedBook,theofficialOpenGLprogrammingguide.

Thisisthereasonweareusingfreeglut,oneoftheopensourcealternativesthatimplementtheGLUTAPI.

InitializingthewindowThefirstlinesofourscriptwillbetheimportstatementsaswellasthedefinitionofourAppclassandits__init__method.

ApartfromtheOpenGLAPIandGLUT,weimporttheOpenGLUtilityLibrary(GLU).GLUisusuallydistributedwiththebasicOpenGLpackage,andwewilluseacoupleoffunctionsofferedbythislibraryinourexample:

importsys

importmath

fromOpenGL.GLimport*

fromOpenGL.GLUimport*

fromOpenGL.GLUTimport*

classApp(object):

def__init__(self,width=800,height=600):

self.title=b'OpenGLdemo'

self.width=width

self.height=height

self.angle=0

self.distance=20

Youmaywonderwhatthebbeforethe'OpenGLdemo'stringmeans.Itrepresentsabinarystring,anditisoneofthedifferencesbetweenPython2and3.Therefore,ifyoufindaGLUTprogramwritteninPython2,rememberthatthestringtitleofthewindowmustbedefinedasabinarystringinordertoworkwithPython3.

Withtheseinstancemembers,wecancallourOpenGLinitializationfunctions:

defstart(self):

glutInit()

glutInitDisplayMode(GLUT_DOUBLE|GLUT_DEPTH)

glutInitWindowPosition(50,50)

glutInitWindowSize(self.width,self.height)

glutCreateWindow(self.title)

glEnable(GL_DEPTH_TEST)

glEnable(GL_LIGHTING)

glEnable(GL_LIGHT0)

Stepbystep,ourstartmethodperformsthefollowingoperations:

glutInit():ThisinitializestheGLUTlibrary.Whileitispossibletopassparameterstothisfunction,wewillleavethiscallwithoutanyarguments.glutInitDisplayMode():Thissetsthedisplaymodeofthetop-levelwindowthatwewillcreate.ThemodeisthebitwiseORofafewGLUTdisplaymodemasks.GLUT_DOUBLEisthemodeforthedoublebuffer,whichcreatesseparatefrontandbackbuffers.Whileoneofthesebuffersisbeingdisplayed,theotheroneisbeingrendered.Ontheotherhand,GLUT_DEPTHrequestsadepthbufferforthewindow.Itstoresthezcoordinateofeachgeneratedpixel,andifthesamepixelisrenderedforasecondtimebecausetwoobjectsoverlap,itdetermineswhichobjectisclosertothe

camera,thatis,reproducingthedepthperception.glutInitWindowPosition()andglutInitWindowsSize():Thesesettheinitialpositionofthewindowanditssize.Accordingtoourwidthandheightinstancemembers,itindicatestocreateawindowof800x600pixelswithanoffsetof50pixelsinthexandyaxesfromthetop-leftcornerofthescreen.glutCreateWindow():Thiscreatesthetop-levelwindowofourapplication.Theargumentpassedtothisfunctionisabinarystringforuseasthewindowtitle.glEnable():ThisisthefunctionusedtoenabletheGLcapabilities.Inourapp,wecallitwiththefollowingvalues:

GL_DEPTH_TEST:Thisperformsdepthcomparisonsandupdatesthedepthbuffer.GL_LIGHTING:Thisenableslighting.GL_LIGHT0:ThisenablesLight0.PyOpenGLdefinesaspecificnumberoflightconstants—fromGL_LIGHT0toGL_LIGHT8—buttheparticularimplementationofOpenGLthatyouarerunningmightallowmorethanthisnumber.

TipLightingandcolors

Whenlightingisenabled,thecolorsarenotdeterminedbytheglColorfunctionsbutbythecombinationofthelightingcomputationandthematerialcolorssetbyglMaterial.TocombinelightingwithglColor,itisrequiredthatyouenableGL_COLOR_MATERIALfirst:

glEnable(GL_COLOR_MATERIAL)

#...

glColor4f(r,g,b,a)

#Drawpolygons

OncewehaveinitializedGLUTandenabledtheGLcapabilities,wecompleteourstart()methodbyspecifyingtheclearcolor,settingtheperspective,andstartingthemainloop:

defstart(self):

#...

glClearColor(.1,.1,.1,1)

glMatrixMode(GL_PROJECTION)

aspect=self.width/self.height

gluPerspective(40.,aspect,1.,40.)

glMatrixMode(GL_MODELVIEW)

glutDisplayFunc(self.display)

glutSpecialFunc(self.keyboard)

glutMainLoop()

defkeyboard(self,key,x,y):

pass

Thesestatementsperformthefollowingoperations:

glClearColor():Thisdefinestheclearvaluesforthecolorbuffer;thatis,eachpixelwillhavethisvalueifnoothercolorisrenderedinthispixel.glMatrixMode():Thissetsthematrixstackmodeformatrixoperations,inthiscase

totheprojectionmatrixstack.OpenGLconcatenatesmatrixoperationsforhierarchicalmodes,makingiteasytocomposethetransformationofachildobjectrelativetoitsparent.WithGL_PROJECTION,wesetthematrixmodefortheprojectionmatrixstack.gluPerspective():Thepreviousstatementsetstheprojectionmatrixstackasthecurrentstack.Withthisfunction,wecangeneratetheperspectiveprojectionmatrix.Theparametersthatgeneratethismatrixareasfollows:

fovy:Theviewangleindegreesintheydirection.aspect:Thisistheaspectratioofthefieldofview.Itistheratiooftheviewportwidthtotheviewportheight.zNear:ThedistancefromtheviewertotheNearplane.zFar:ThedistancefromtheviewertotheFarplane.

WithglMatrixMode(GL_MODELVIEW),wesetthemodelviewmatrixstack,whichistheinitialvalue,asthecurrentmatrixmode.

ThelastthreeGLUTcallsdothefollowing:

glutDisplayFunc():Thisreceivesthefunctionthatwillbeinvokedtodisplaythewindow.

glutSpecialFunc():Thissetsthekeyboardcallbackforthecurrentwindow.NotethatthiscallbackwillbetriggeredonlywhenthekeysrepresentedbytheGLUT_KEY_*constantsarepressed.glutMainLoop():Thisstartsthemainloopoftheapplication.

WiththeOpenGLcontextinitialized,weareabletocalltheOpenGLfunctionsthatwillrenderourscene.

TipTheOpenGLandGLUTreference

Asyoumayhavealreadynoticed,theOpenGLandGLUTspecificationsdefinealargenumberoffunctions.YoucanfindthebindingsoftheseAPIsimplementedbyPyOpenGLontheofficialwebsiteathttp://pyopengl.sourceforge.net/documentation/manual-3.0/index.html.

DrawingshapesOurdisplay()functionperformstheverycommontasksofamaingameloop.

Itfirstclearsthescreen,thensetsupaviewingtransformation(wewillseewhatthismeansafterthesnippet),andfinallyrendersthelightanddrawsthegameobjects:

defdisplay(self):

x=math.sin(self.angle)*self.distance

z=math.cos(self.angle)*self.distance

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

glLoadIdentity()

gluLookAt(x,0,z,

0,0,0,

0,1,0)

glLightfv(GL_LIGHT0,GL_POSITION,[15,5,15,1])

glLightfv(GL_LIGHT0,GL_DIFFUSE,[1.,1.,1.,1.])

glLightfv(GL_LIGHT0,GL_CONSTANT_ATTENUATION,0.1)

glLightfv(GL_LIGHT0,GL_LINEAR_ATTENUATION,0.05)

glPushMatrix()

glMaterialfv(GL_FRONT,GL_DIFFUSE,[1.,1.,1.,1.])

glutSolidSphere(2,40,40)

glPopMatrix()

glPushMatrix()

glTranslatef(4,2,0)

glMaterialfv(GL_FRONT,GL_DIFFUSE,[1.,0.4,0.4,1.0])

glutSolidSphere(1,40,40)

glPopMatrix()

glutSwapBuffers()

Thesearetheoperationsthatdisplay()performs:

glClear():WiththeGL_COLOR_BUFFER_BITandGL_DEPTH_BUFFER_BITmasks,thisclearsthecoloranddepthbuffers.glLoadIdentity():Thisloadstheidentitymatrixasthecurrentmatrix.Theidentitymatrixisa4x4matrixwithonesinthemaindiagonalandzeroseverywhereelse.Thismakesthestackmatrixstartoverattheorigin,whichisusefulifyouhavepreviouslyappliedsomematrixtransformations.gluLookAt():Thiscreatesaviewingmatrix.Thefirstthreeparametersarethex,y,andzcoordinatesoftheeyepoint.Thenextthreeparametersarethex,y,andzcoordinatesofthereferencepoint,thatis,thepositionthecameraislookingat.Finally,thelastthreeparametersspecifythedirectionoftheupvector(usually,itis0,1,0).glLightfv():Thissetstheparametersoflightsource0(GL_LIGHT0).Thefollowingparametersarespecifiedinourexample:

GL_POSITION:Thisdefinesthepositionofthelight

GL_DIFFUSE:ThissetstheRGBAintensityofthelightGL_CONSTANT_ATTENUATION:ThisspecifiestheconstantattenuationfactorGL_LINEAR_ATTENUATION:Thisspecifiesthelinearattenuationfactor

Oncethelightingattributesareset,wecanstartrenderingbasicshapeswithGLUT.Ifwedrawtheobjectsfirst,lightingwillnotbeappliedcorrectly:

glPushMatrix():Thispushesanewmatrixintothecurrentmatrixstack,identicaltotheonebelowit.Whilewedothis,wecanapplytransformationssuchasglTranslateandglRotate,tothismatrix.Wewillrenderourfirstsphereattheorigin,butthesecondonewillbetransformedwithglTranslate.glTranslate():Thismultipliesthecurrentmatrixbythetranslationmatrix.Inourexample,thetranslationvaluesforthesecondsphereare4forthexaxis,and2fortheyaxis.glMaterialfv():Thissetsthematerialparametersofthefrontface,asitiscalledwithGL_FONT.WithGL_DIFFUSE,wespecifythatwearesettingtheRGBAreflectanceofthematerial.glutSolidSphere():ThroughGLUT,thisroutineallowsustoeasilydrawasolidsphere.Itreceivesthesphere’sradiusasthefirstargument,andthenumberofslicesandstacksintowhichthespherewillbesubdivided.Thegreaterthesevaluesare,therounderthespherewillbe.glPopMatrix():Thispopsthecurrentmatrixfromthestack.Ifwedidnotdothis,eachnewobjectrenderedwouldbeachildofthepreviousone.

Finally,weswitchthebufferswithglutSwapBuffers().Ifdoublebufferingwasnotenabled,weshouldcallthesinglebufferequivalent—glFlush().

RunningthedemoAsusual,wecheckwhetherthemoduleisthemainscriptforstartingtheapplication:

if__name__=='__main__':

app=App()

app.start()

Ifyourunthecompleteapplication,theresultwilllooklikewhatisshowninthefollowingscreenshot:

RefactoringourOpenGLprogramAsyoumayhaveseen,thisexampleusesenoughOpenGLcallstogrowoutofcontrolifwedonotstructureourcode.That’swhywearegoingtoapplysomeobject-orientedprinciplestoachieveabetterorganization,withoutmodifyingtheorderofthecallsorlosinganyfunctionality.

ThefirststepwillbetodefineaLightclass.Itwillholdtheattributesneededtorenderthelight:

classLight(object):

enabled=False

colors=[(1.,1.,1.,1.),(1.,0.5,0.5,1.),

(0.5,1.,0.5,1.),(0.5,0.5,1.,1.)]

def__init__(self,light_id,position):

self.light_id=light_id

self.position=position

self.current_color=0

Besides,thismodularizationwillhelpusimplementanewfunctionality:changingthecolorofthelight.Wesetthecurrentcolorindexto0,andwewilliterateoverthedifferentcolorsdefinedinLight.colorseachtimetheswitch_color()methodiscalled.

Therender()methodrespectstheoriginalimplementationoflightingfromournon-refactoredversion:

defrender(self):

light_id=self.light_id

color=Light.colors[self.current_color]

glLightfv(light_id,GL_POSITION,self.position)

glLightfv(light_id,GL_DIFFUSE,color)

glLightfv(light_id,GL_CONSTANT_ATTENUATION,0.1)

glLightfv(light_id,GL_LINEAR_ATTENUATION,0.05)

defswitch_color(self):

self.current_color+=1

self.current_color%=len(Light.colors)

Finally,wewrapthecalltoenablelightingwiththeenable()methodandaclassattribute:

defenable(self):

ifnotLight.enabled:

glEnable(GL_LIGHTING)

Light.enabled=True

glEnable(self.light_id)

AnotherimprovementisthecreationofaSphereclass.Thisclasswillallowustocustomizetheradius,position,andcolorofeachinstance:

classSphere(object):

slices=40

stacks=40

def__init__(self,radius,position,color):

self.radius=radius

self.position=position

self.color=color

defrender(self):

glPushMatrix()

glTranslatef(*self.position)

glMaterialfv(GL_FRONT,GL_DIFFUSE,self.color)

glutSolidSphere(self.radius,Sphere.slices,Sphere.stacks)

glPopMatrix()

Withtheseclasses,wecanadaptourAppclassandcreatetheinstancesthatwewillrenderinthemainloop:

classApp(object):

def__init__(self,width=800,height=600):

#...

self.light=Light(GL_LIGHT0,(15,5,15,1))

self.sphere1=Sphere(2,(0,0,0),(1,1,1,1))

self.sphere2=Sphere(1,(4,2,0),(1,0.4,0.4,1))

Rememberthatbeforerenderingthelightobject,weneedtoenableOpenGLlightingthroughthelight.enable()method:

defstart(self):

#...

glEnable(GL_DEPTH_TEST)

self.light.enable()

#...

Nowthedisplay()methodbecomessuccinctandexpressive,sincetheapplicationdelegatestheOpenGLcallstotheobjectinstances:

defdisplay(self):

x=math.sin(self.angle)*self.distance

z=math.cos(self.angle)*self.distance

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

glLoadIdentity()

gluLookAt(x,0,z,

0,0,0,

0,1,0)

self.light.render()

self.sphere1.render()

self.sphere2.render()

glutSwapBuffers()

Tocompleteourfirstsampleapplication,wewilladdinputhandling.Itallowstheplayertorotatethecameraaroundthespheresandmoveforwardorawayfromthecenterofthescene.

ProcessingtheuserinputAswesawearlier,glutSpecialFunctakesacallbackfunctionthatreceivesthepressedkeyasthefirstargument,andthexandycoordinatesofthemousewhenthekeywaspressed.

Wewillusetherightandleftarrowkeystomovearoundthespheres,andtheupanddownarrowkeystoapproximateormoveawayfromthespheres.Besidesallofthis,thecolorofthelightwillchangeifwepressF1,andtheapplicationwillbeclosediftheInsertkeyispressed.

Todoso,wewillcheckthevaluesofthekeyargumentwiththerespectiveGLUTconstants:

defkeyboard(self,key,x,y):

ifkey==GLUT_KEY_INSERT:

sys.exit()

ifkey==GLUT_KEY_UP:

self.distance-=0.1

ifkey==GLUT_KEY_DOWN:

self.distance+=0.1

ifkey==GLUT_KEY_LEFT:

self.angle-=0.05

ifkey==GLUT_KEY_RIGHT:

self.angle+=0.05

ifkey==GLUT_KEY_F1:

self.light.switch_color()

self.distance=max(10,min(self.distance,20))

self.angle%=math.pi*2

glutPostRedisplay()

Notethatwetrimmedthevalueoftheself.distancemember,soitsvalueisalwaysbetween10and20,andself.angleisalsoalwaysbetween0and2π.Tonotifythatthecurrentwindowneedstoberedisplayed,wecallglutPostRedisplay().

YoucancheckouttheChapter5_02.pyscript,whichcontainsthisrefactoredversionofourapplication.

Whenyourunit,pressthearrowkeystorotatearoundthespheresandF1toseehowthelightingaffectsthespheres’materials.

AddingthePygamelibraryWithGLUT,wecanwriteOpenGLprogramsquickly,primarilybecauseitwasaimedtoprovideroutinesthatmakelearningOpenGLeasier.However,theGLUTAPIwasdiscontinuedin1998.Nonetheless,therearesomepopularsubstitutesinthePythonecosystem.

Pygameisoneofthesealternatives,andwewillseethatitcanbeseamlesslyintegratedwithOpenGL,evensimplifyingtheresultingcodeforthesameprogram.

Pygame101BeforeweintegratePygameintoourOpenGLprogram,wewillwriteasample2DapplicationtogetstartedwithPygame.

WewillimportPygameanditslocalsmodule,whichincludestheconstantsthatwewillneedinourapplication:

importsys

importpygame

frompygame.localsimport*

classApp(object):

def__init__(self,width=400,height=300):

self.title='Hello,Pygame!'

self.fps=100

self.width=width

self.height=height

self.circle_pos=width/2,height/2

Pygameusesregularstringsforthewindowtitle,sowewilldefinetheattributewithoutaddingb.Anotherchangeisthenumberofframespersecond(FPS),whichwewilllaterfindouthowtocontrolviaPygame’sclock:

defstart(self):

pygame.init()

size=(self.width,self.height)

screen=pygame.display.set_mode(size,DOUBLEBUF)

pygame.display.set_caption(self.title)

clock=pygame.time.Clock()

whileTrue:

dt=clock.tick(self.fps)

foreventinpygame.event.get():

ifevent.type==QUIT:

pygame.quit()

sys.exit()

pressed=pygame.key.get_pressed()

x,y=self.circle_pos

ifpressed[K_UP]:y-=0.5*dt

ifpressed[K_DOWN]:y+=0.5*dt

ifpressed[K_LEFT]:x-=0.5*dt

ifpressed[K_RIGHT]:x+=0.5*dt

self.circle_pos=x,y

screen.fill((0,0,0))

pygame.draw.circle(screen,(0,250,100),

(int(x),int(y)),30)

pygame.display.flip()

WeinitializethePygamemoduleswithpygame.init(),andthenwecreateascreenwithagivenwidthandheight.TheDOUBLEBUFflagispassedsoastoenabledoublebuffering,whichhasthebenefitswementionedpreviously.

Themaineventloopisimplementedwithawhileblock,andwiththeClockinstance,wecancontroltheframerateandcalculatetheelapsedtimebetweenframes.Thisvaluewill

bemultipliedbythespeedofmovement,sothecirclewillmoveatthesamespeediftheFPSvaluechanges.

Withpygame.event.get(),weretrievetheeventqueue,andifaQUITeventoccurs,thewindowisclosedandtheapplicationfinishesitsexecution.

Thepygame.key.get_pressed()returnsalistwiththepressedkeys,andwiththekeyconstants,wecancheckwhetherthearrowkeysarepressed.Ifso,thecircle’spositionisupdatedanditisdrawnonthenewcoordinates.

Finally,pygame.display.flip()updatesthescreen’ssurface.

TheChapter5_03.pyscriptcontainsthefullcodeofthisexample.

TipThePygamedocumentation

SincePygameisdividedintoseveralmodules,eachonewithvariousfunctions,classes,andconstants,theofficialdocumentationisausefulreference.

Weareusingsomefunctionsfromthekeymodule;youcanfindfurtherinformationaboutitathttps://www.pygame.org/docs/ref/key.html.Thesameappliesforthedisplayandtimemodules.

PygameintegrationLet’sseehowitispossibletoimplementthesamefunctionalitywithPygame.ThefirststepistoreplacetheOpenGL.GLUTimportwiththeonesweusedinourpreviousexample:

importsys

importmath

importpygame

frompygame.localsimport*

fromOpenGL.GLimport*

fromOpenGL.GLUimport*

Thetitlestringisnowaregularstring,andtheFPSattributecanbeaddedaswell:

classApp(object):

def__init__(self,width=800,height=600):

self.title='OpenGLdemo'

self.fps=60

self.width=width

self.height=height

#...

WeremovetheGLUTcallsfromourstart()method,andtheyarereplacedbythePygameinitialization.ApartfromDOUBLEBUF,wewilladdtheOPENGLflagtocreateanOpenGLcontext:

defstart(self):

pygame.init()

pygame.display.set_mode((self.width,self.height),

OPENGL|DOUBLEBUF)

pygame.display.set_caption(self.title)

glEnable(GL_CULL_FACE)

#...

glMatrixMode(GL_MODELVIEW)

clock=pygame.time.Clock()

whileTrue:

dt=clock.tick(self.fps)

self.process_input(dt)

self.display()

Thenewprocess_input()methodupdatesthesceneandtheinstanceattributesbyretrievingtheeventsfromtheeventqueueandprocessingthepressedkeys.

IfaQUITeventoccursortheEsckeyispressed,thePygameprogramisexecuted.Otherwise,thecamerapositionisupdatedwiththedistanceandangleofrotation,controlledbythearrowkeys:

defprocess_input(self,dt):

foreventinpygame.event.get():

ifevent.type==QUIT:

self.quit()

ifevent.type==KEYDOWN:

ifevent.key==K_ESCAPE:

self.quit()

ifevent.key==K_F1:

self.light.switch_color()

pressed=pygame.key.get_pressed()

ifpressed[K_UP]:

self.distance-=0.01*dt

ifpressed[K_DOWN]:

self.distance+=0.01*dt

ifpressed[K_LEFT]:

self.angle-=0.005*dt

ifpressed[K_RIGHT]:

self.angle+=0.005*dt

self.distance=max(10,min(self.distance,20))

self.angle%=math.pi*2

TheglutSwapBuffers()isreplacedbypygame.display.flip(),andthenewquit()methodquitsPygameandexitsPythongracefully:

defdisplay(self):

#...

self.light.render()

self.sphere1.render()

self.sphere2.render()

pygame.display.flip()

defquit(self):

pygame.quit()

sys.exit()

AnotherconsequenceofremovingGLUTisthatwecannotuseglutSolidSpheretorenderourspheres.

Fortunately,wecansubstituteitwiththegluSphereGLUfunction.TheonlydifferenceisthatweneedtocreateaGLUquadraticobjectfirst,andthencallthefunctionwiththisargumentandtheusualradius,numberofslices,andnumberofstacksintowhichthesphereisdivided:

classSphere(object):

slices=40

stacks=40

def__init__(self,radius,position,color):

self.radius=radius

self.position=position

self.color=color

self.quadratic=gluNewQuadric()

defrender(self):

glPushMatrix()

glTranslatef(*self.position)

glMaterialfv(GL_FRONT,GL_DIFFUSE,self.color)

gluSphere(self.quadratic,self.radius,

Sphere.slices,Sphere.stacks)

glPopMatrix()

Withthesechanges,theGLUTAPIisnowcompletelyreplacedbyPygame.Checkoutthechapter5_04.pyscripttoseethecompleteimplementation.

TipOpenGLandSDL

ByincludingPygame,wereplacetheGLUTAPIwithSimpleDirectMediaLayer(SDL),whichisthelibrarythatPygameisbuiltover.Likefreeglut,itisanothercross-platformalternativetoGLUT.

DrawingwithOpenGLUntilnow,wehavealwaysrenderedourobjectswithautilityroutine,butmostOpenGLapplicationsrequiretheuseofsomedrawingprimitives.

TheCubeclassWewilldefineanewclasstorendercubes,andwewillusethefollowingrepresentationtobetterunderstandthevertices’positions.From0to7,theverticesareenumeratedandrepresentedina3Dspace.

Thesidesofacubecannowberepresentedastuples:thebackfaceis(0,1,2,3),therightfaceis(4,5,1,0),andsoon.

Notethatwearrangetheverticesincounterclockwiseorder.Aswewilllearnlater,thiswillhelpusenableanoptimizationcalledfaceculling,whichconsistsofdrawingonlythevisiblefacesofapolygon:

classCube(object):

sides=((0,1,2,3),(3,2,7,6),(6,7,5,4),

(4,5,1,0),(1,5,7,2),(4,0,3,6))

The__init__methodwillstorethevaluesofthepositionandcolor,aswellasthevertexcoordinateswithrespecttothecenterpositionofthecube:

def__init__(self,position,size,color):

self.position=position

self.color=color

x,y,z=map(lambdai:i/2,size)

self.vertices=(

(x,-y,-z),(x,y,-z),

(-x,y,-z),(-x,-y,-z),

(x,-y,z),(x,y,z),

(-x,-y,z),(-x,y,z))

Therender()methodpushesanewmatrix,transformsitaccordingtoitscurrentposition,andcallsglVertex3fv()foreachvertexofthesixfacesofcube.

TheglVertex3fvtakesalistofthreefloatvaluesthatspecifythevertexposition.ThisfunctionisexecutedbetweentheglBegin()andglEnd()calls.Theydelimittheverticesthatdefineaprimitive.TheGL_QUADSmodetreatseachgroupoffourverticesasanindependentquadrilateral.

Thelaststatementpopsthecurrentmatrixfromthematrixstack:

defrender(self):

glPushMatrix()

glTranslatef(*self.position)

glBegin(GL_QUADS)

glMaterialfv(GL_FRONT,GL_DIFFUSE,self.color)

forsideinCube.sides:

forvinside:

glVertex3fv(self.vertices[v])

glEnd()

glPopMatrix()

EnablingfacecullingEventhoughacubehassixfaces,wecanseeamaximumofonlythreefacesatonce,andonlytwooronefromcertainangles.Therefore,ifwediscardthefacesthatarenotgoingtobevisible,wecanavoidrenderingatleast50percentofthefacesofourcubes.

Byenablingfaceculling,OpenGLcheckswhichfacesarefacingthevieweranddiscardsthefacesthatarefacingbackwards.Theonlyrequirementistodrawthefacesofthecubeinthecounterclockwiseorderofthevertices,whichisthedefaultfrontfaceinOpenGL.Theimplementationpartiseasy;weaddthefollowinglinetoourglEnablecalls:

#...

glEnable(GL_LIGHTING)

glEnable(GL_CULL_FACE)

#...

Inournextapplication,wewilladdsomecubesandenablefacecullingtoseethisoptimizationinpractice.

BasiccollisiondetectiongameWithalloftheseingredients,youarenowabletowriteasimplegamethatdetectssimplecollisionsbetweenshapes.

Thesceneconsistsofaninfinitelane.Blocksappearrandomlyattheendofthelaneandmovetowardstheplayer,representedasthesphereinthefollowingscreenshot.Heorshemustavoidhittingtheblocksbymovingthespherefromrighttoleftinthehorizontalaxis.

Thegameisoverwhentheplayer’scharactercollideswithoneoftheblocks.

Thisgameplayisdirectanduncomplicated,anditwillallowustodevelopa3Dgamewithoutworryingtoomuchaboutmorecomplicatedphysicscalculations.

SincewearegoingtoreusetheLight,Cube,andSphereclasses,weneedtodefineanewclassonlytorepresentourgameblocks:

classBlock(Cube):

color=(0,0,1,1)

speed=0.01

def__init__(self,position,size):

super().__init__(position,(size,1,1),Block.color)

self.size=size

defupdate(self,dt):

x,y,z=self.position

z+=Block.speed*dt

self.position=x,y,z

Itsupdate()methodsimplymovestheblocktowardstheplayerbyupdatingitszcoordinatewithuniformspeed.

OurAppclasssetstheinitialvaluesoftheattributesthatwewillneedduringtheexecutionofourgame,andcreatestheLightandthegameobjectinstancesasinourpreviousexamples:

classApp(object):

def__init__(self,width=800,height=600):

#...

self.game_over=False

self.random_dt=0

self.blocks=[]

self.light=Light(GL_LIGHT0,(0,15,-25,1))

self.player=Sphere(1,position=(0,0,0),

color=(0,1,0,1))

self.ground=Cube(position=(0,-1,-20),

size=(16,1,60),

color=(1,1,1,1))

Thestart()methodhassmallvariations,onlyaddingglEnable(GL_CULL_FACE),aswementionedpreviously:

defstart(self):

pygame.init()

#...

glMatrixMode(GL_MODELVIEW)

glEnable(GL_CULL_FACE)

self.main_loop()

Themain_loop()methodisnowaseparatemethodandincludestherandomgenerationofblocks,collisiondetection,aswellastheupdatingofthepositionsoftheblocks:

defmain_loop(self):

clock=pygame.time.Clock()

whileTrue:

foreventinpygame.event.get():

ifevent.type==QUIT:

pygame.quit()

sys.exit()

ifnotself.game_over:

self.display()

dt=clock.tick(self.fps)

forblockinself.blocks:

block.update(dt)

self.clear_past_blocks()

self.add_random_block(dt)

self.check_collisions()

self.process_input(dt)

Wewillimplementcollisiondetectionbycomparingtheboundariesoftheclosestblockswiththeextremesofthesphere.Sincethesphere’swidthissmallerthantheblocksize,ifoneoftheseextremesisbetweentherightandleftboundariesofablock,itwillbeconsideredasacollision:

defcheck_collisions(self):

blocks=filter(lambdax:0<x.position[2]<1,

self.blocks)

x=self.player.position[0]

r=self.player.radius

forblockinblocks:

x1=block.position[0]

s=block.size/2

ifx1-s<x-r<x1+sorx1-s<x+r<x1+s:

self.game_over=True

print("Gameover!")

Topreventthespawningoftoomanyblocks,wedefinedacountercalledrandom_dt.Itaccumulatestheelapsedtimeinmillisecondsbetweenframes,anditwilltrytospawnanewblockonlyifthesumisgreaterthan800milliseconds:

defadd_random_block(self,dt):

self.random_dt+=dt

ifself.random_dt>=800:

r=random.random()

ifr<0.1:

self.random_dt=0

self.generate_block(r)

defgenerate_block(self,r):

size=7ifr<0.03else5

offset=random.choice([-4,0,4])

self.blocks.append(Block((offset,0,-40),size))

Ifthegeneratedrandomnumberislowerthan0.1,anewblockisaddedtotheblocklistandtherandom_dtcounterisresetto0.Inthisway,theminimumelapsedtimebetweentwoblockscanbe0.8seconds,givingenoughtimetoleaveatolerabledistancefromoneblocktoanother.

Anotheroperationthatthemainloopperformsisremovingtheblocksthatarelocatedbehindthecameras’viewingarea,avoidingthecreationoftoomanyBlockinstances:

defclear_past_blocks(self):

blocks=filter(lambdax:x.position[2]>5,

self.blocks)

forblockinblocks:

self.blocks.remove(block)

delblock

Thecodefordisplayingthegameobjectsstaysassuccinctasusual,thankstothetransferofthedrawingprimitivestotherespectiverender()methods:

defdisplay(self):

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

glLoadIdentity()

gluLookAt(0,10,10,

0,0,-5,

0,1,0)

self.light.render()

forblockinself.blocks:

block.render()

self.player.render()

self.ground.render()

pygame.display.flip()

Tofinishourgame,wewillmodifytheinputhandlingofourprogram.Thischangeisstraightforward,sinceweonlyneedtoupdatethexcomponentofthecharacter’spositionandtrimitsothatitcannotmoveoutofthelane:

defprocess_input(self,dt):

pressed=pygame.key.get_pressed()

x,y,z=self.player.position

ifpressed[K_LEFT]:

x-=0.01*dt

ifpressed[K_RIGHT]:

x+=0.01*dt

x=max(min(x,7),-7)

self.player.position=(x,y,z)

Inthechapter5_05.pyscript,youcanfindthefullimplementationofthegame.Runitandfeelfreetomodifyandimproveit!Youcanaddpickupitemsandkeeptrackofthescore,orgivetheplayeranumberoflivesbeforethegameisover.

SummaryInthischapter,youlearnedhowitispossibletoworkwithPythonandOpenGL,andwithbasicknowledgeaboutOpenGLAPIs,wewereabletodevelopasimple3Dgame.

Wesawtwocross-platformalternativesforcreatinganOpenGLcontext:GLUTandPygame.Youcandecidewhichonebettersuitsyour3Dgames,dependingonthetrade-offsofeachoption.Keepthisinmind:anadvantageofusingbothisthatyoumayadaptexistingexamplesfromonelibrarytotheother!

Withthesefoundationsof3Dcovered,inthenextchapter,wewillseehowtodevelopa3Dplatformerbasedonthesetechnologies.

Chapter6.PyPlatformerSofar,youhavelearnedhowtoimplementgamesinPythonwithdifferentlibraries,withtheprimaryfocusbeingonthepracticeofgamedevelopment.

Now,wewillintroducesometheoreticalconceptsthatwillnotonlycomplementthepractice,butwillalsohelpusdevelopgamesefficiently.Thistheorywillaidusinunderstandingwhygamesareconceptuallydesignedthewaytheyare.

Inthischapter,wewillcoverthesetopics:

FoundationsofthegametheoryObject-orientedprinciplesappliedtogamedevelopmentHowtoimplementasmall3DgameframeworkModularizingourfunctionalitywithreusablecomponentsAddingaphysicsenginetosimulaterigidbodies’interactionsCreatingthebuildingblocksofaplatformergame

AnintroductiontogamedesignThereareseveralacademicdefinitionsofwhatagameis;however,mostofthemsharethekeyterms,suchasrules,objectives,andplayers.Assumingthatallgamessharetheseconcepts,wemayasksomeinterestingquestionswhileanalyzingagame:whatisthegame’smainobjective?Whataretherulesthattheplayermustfollow?Isitdifficulttorecognizethegoalandtherulesofthesystem?

Otherdefinitionsmakereferencestoconceptssuchasresourcemanagementandinefficiencies,becausethedecisionsoftheplayerareusuallyconditionedbythelimitationsofsomeusefultokensinthegame.

Thedecisionswemakewhenwecreateagamearedeeplyrelatedwiththeseconcepts,andaswewillseelater,itisagoodexercisetothinkaboutthemevenwhenwearestartingwiththedevelopment.

LeveldesignInordertosuccessfullyengageourplayers,ourgameneedstograduallyaddnewchallengesthatpreservetheirinterest.However,theseingredientsshouldbeintroducedinacoherentordersothattheplayerdoesnotbecomeconfusedbecausesheorhedoesnotknowhowtoreacttothegame’soutput.

Thismeansthattheplayerisnotonlyplayingyourgamebutalsolearninghowtoplayit.Thislearningcurveshouldbecarefullyconsideredintutorialsandthefirstlevel,becauseincorrectguidancecanleadtofrustration.

PlatformerskillsInourplatformergame,thefirstactionthattheplayerwilllearnishowtomovetheircharacter,whichisintuitivelyperformedbypressingthearrowkeys.Sincethereisnotanythreatnearby,theplayercanexperimentmovingaroundandcanbecomefamiliarwiththecontrolkeys.

Next,theplayerwillfaceanobstacle,andsheorheneedstolearnhowtojumpoverittocontinue.Thereisnogapbetweentheobstacleandtheground,sothereisnoriskoffallingfromtheplatformifthecharacter’sjumpistooshort,asshowninthefollowingscreenshot:

Oncethefirstobstacleiscleared,theplayerfacesacoupleofplatformswithgapsinbetween,asshowninthenextscreenshot.Nowitisrequiredtomeasurethejumpingforces,orelsethecharacterwillfallintothevoid.

Ifthecharacterdoesnotreachtheplatformandfalls,itwillrespawnattheinitialposition.That’sanotherlessonthattheplayerwilllearn:movewithcare,orelseyouwillhavetostartoveragain!

Onthenextplatform,theplayerwillencounteranewcharacter.Ifthecharactercollideswithit,itwillmovebacktothespawnposition.Therefore,theplayerlearnsthattheseobjectsshouldbeavoidedandtheymustjumpoverittoadvance.

Afterthis,theplayercanreachaplatformonwhichthereisaspinningbox.Itisplacedinthemiddleofanarrowplatform,soitisveryeasytocollidewith,asshowninthisscreenshot:

Whenthishappens,theplayer’sammoisincreased,sonowthecharacterisabletoshootbypressingthespacebar.Onthenextplatform,theplayercanfindanotherenemytotesttheirshootingability.Withthepickup,theplayercanshootuptofivetimes,sotheycantryoutthisabilityseveraltimesbeforerunningoutofammo.

Nowthatwehaveanalyzedtheskillsthatourplayerswillneedtolearn,wecanmoveontothearchitecturaldesignofourgame.

Component-basedgameenginesWhenwestartdevelopingagamefromscratch,thefirststepmaybetodefineabasicclasswithcommonattributesforallthegameobjects,suchasitspositioncoordinates,thecolor,thespeed,andsoon.ThiscouldbethebasicGameObjectorActorclasswedeclaredinpreviouschapters.

Thenyouneedtoaddothergameobjectswithmoreconcretekindsofbehavior,suchasthecharacterthatwillbecontrolledbytheplayer,ortheenemiesthatrandomlyshoottheplayablecharacter.Ifyourepresenttheseentitieswithseparateclasses,eachoneimplementsthatfunctionalitywithacustommethodinthecorrespondingclass.

Consequently,everyspecializationofanexistingentitymightbetranslatedintoanewsubclass.Supposewewanttoaddaspecialtypeofenemythatmovesalongaparticularroute,whichwewillcallPatrolEnemy.Inourclassdiagramshownhere,thisclasswillextendourEnemyclass:

Whilethisapproachmightworkforsmallgames,itbecomesmoredifficulttoscaletheorganizationoftheprojectwhentheinheritancehierarchygrows.Imaginethatwewanttoaddanothertypeofenemythatshootsonlywhenitislocatedwithinacertaindistancefromtheplayer,andathirdenemythatcombinesthisnewbehaviorwithPatrolEnemy.Thisisillustratedinthefollowingdiagram:

Ifsomegameobjectsfollowthebehavioroftheirancestorsandsharethefunctionalitywithotherentities,youmayneedtousemultipleinheritanceordefineintermediateclassesthatwouldbeunnecessaryotherwise.

Ontheotherhand,acomponent-baseddesignfollowstheprincipleoffavoringobjectcompositionoverclassinheritance.Thismeansthatthefunctionalityiscontainedinotherclasses,insteadofreusingitwithasubclass.

Translatedintoourexample,thisarchitectureleavesuswiththefollowingdiagram.Besides,aswewillseelater,acomponentbaseclassisalsoadded.

TheclassesundertheGameObjectsareaareGameObjectsubclasses,buttheirbehaviorisdeterminedbythecompositionoftheirComponentssubclasses.

Inthisway,wedonotintroducemultipleinheritancesolutionsandkeepthesepiecesoffunctionalityseparatedinsmallclasses,withtheadvantageofbeingabletoaddorremove

themdynamically.

Inlatersections,wewillseehowthesecomponentsarealsousedtorenderobjectsinourOpenGLcontext,butnowwewillmoveontothelibrarythatwewillusetosimulatephysicsinourgame.

IntroducingPymunkWewillimplementphysicsinourgamewithPymunk,a2DphysicsenginebuiltontopofChimpunk2D.

EventhoughweareusingOpenGLfor2Dgraphics,ourplatformergamewillrecreateatwo-dimensionalspace,sowewillworkontheplaneinwhichthezaxisequals0.

YoucaninstallPymunkdirectlyfrompip:

$pipinstallpymunk

TheChimpunk2DlibraryiswritteninC,soyoumightneedtocompileitonyourplatformifthedistributiondoesnotshipwiththeprecompiledlibrary.On32-bitWindowsand32-bitand64-bitLinuxversions,PymunkwillincludetheChimpunk2Dbinaries.

Forotherplatforms,orifyouwanttocompilethelibraryyourself,youcancheckouttheinstallationguideandthestepsforcompilingChimpunk2Dathttp://pymunk.readthedocs.org/en/latest/installation.html.

TipDon’treinventthewheel!

Implementingarigidbodylibraryisalaborioustask,especiallyifyouaredevelopingacasualgamefromscratch—aswearedoinginthischapter.

Fortunately,thePymunkAPIissimpleandwell-documented,soyoucangetstartedquicklyandavoidlosingtimecraftingacustomphysicsengine.

Themainclassesofthepymunkpackagearethefollowing:

Space:Atwo-dimensionalspaceinwhichthephysicssimulationwilloccur.WewilluseaSpaceinstancefortheentiregame,andwewillupdateeachframethroughitsstep()method.Body:Thisrepresentstherigidbody,thebasicunitofsimulation.Ithasapositionmember,whichwillalsorepresentthepositionofthegameobjectthatcontainstherigidbody.Theothermembersthatwewillcoverarethevelocityandforcevectors.Shape:Thebaseclassforallshapes.Wewilllimitourselvestocircleandboxshapes,definedbytheCircleandPolyclassesrespectively.Arbiter:Thisrepresentsacollisionpairbetweenshapesandisusedincollisioncallbacks.Italsocontainsinformationaboutthecontactpointsofthecollision.

Inthenextsection,wewillrelyontheseclassesandtheirattributestoholdinformationaboutgameobjectsandtheircollisions.

BuildingagameframeworkTheComponentandGameObjectclassesformthecoreofourgameengine.TheinterfaceofferedbytheComponentclassisaseasyasthis:

classComponent(object):

__slots__=['gameobject']

defstart(self):

pass

defupdate(self,dt):

pass

defstop(self):

pass

defon_collide(self,other,contacts):

pass

Thesemethodsdefinethelifecycleofourcomponents:

start():Thisiscalledwhenthecomponentisaddedtothegameobjectinstance.Thecomponentkeepsareferencetothegameobjectasself.gameobject.update(dt):Thisiscalledforeachframe,wherethedtargumentistheelapsedtimeinsecondssincethepreviousframe.on_collide(other,contacts):Thisiscalledwhenthecomponent’sgameobjectcollideswithanotherrigidbody.Theotherargumentistheothergameobjectofthecollisionpair,andcontactsincludesthecontactpointsbetweentheobjects.ThislastargumentisdirectlypassedfromthePymunkAPI,andwewillseelaterhowtoworkwithit.stop():Thisiscalledwhenthecomponentisremovedfromthegameobjectinstance.

ThesewillbeinvokedfromourGameObjectclass.Aswementionedearlier,agameobjectinternallyusesarigidbodytorepresentitspositioninthexandyaxesandanextravariableforthezaxis.Bydefault,wewillworkinthez=0plane.

TheGameObjectclassisdefinedasfollows:

importpymunkaspm

classGameObject(object):

instances=[]

def__init__(self,x=0,y=0,z=0,scale=(1,1,1)):

self._body=pm.Body()

self._body.position=x,y

self._shape=None

self._ax=0

self._ay=0

self._z=z

self.tag=''

self.scale=scale

self.components=[]

GameObject.instances.append(self)

Thetwo-dimensionalpositioniscomplementedwithaninner_zmember,andtherotationisextendedwiththeangleofrotationinthexandyaxiswiththe_axand_aymembers,respectively.Thevelocityvectorisdirectlyretrievedfromtherigidbody:

@property

defposition(self):

pos=self._body.position

returnpos.x,pos.y,self._z

@position.setter

defposition(self,pos):

self._body.position=pos[0],pos[1]

self._z=pos[2]

@property

defrotation(self):

returnself._ax,self._ay,self._body.angle

@rotation.setter

defrotation(self,rot):

self._ax=rot[0]

self._ay=rot[1]

self._body.angle=rot[2]

@property

defvelocity(self):

returnself._body.velocity

@velocity.setter

defvelocity(self,vel):

self._body.velocity=vel

Thegameobject’spositioncanbemodifiedbyapplyinganimpulseoraforcevectortotheunderlyingrigidbody:

defmove(self,x,y):

self._body.apply_impulse((x,y))

defapply_force(self,x,y):

self._body.apply_force((x,y))

Theinstancecomponentscanbemanipulated—added,removed,andretrievedbytype—inaflexiblewaywiththefollowingmethods:

defadd_components(self,*components):

forcomponentincomponents:

self.add_component(component)

defadd_component(self,component):

self.components.append(component)

component.gameobject=self

component.start()

defget_component_by_type(self,cls):

forcomponentinself.components:

ifisinstance(component,cls):

returncomponent

defremove_component(self,component):

component.stop()

self.components.remove(component)

Todisplaythegameobject,wepayspecialattentiontotheRenderablecomponents.TheinterfaceofthisComponentsubclassincludearender()method,whichiscalledinthedisplayloopforeachactivegameobject.

Theupdate()methoddirectlydelegatestheexecutiontoeveryattachedcomponent:

defrender(self):

forcomponentinself.components:

ifisinstance(component,Renderable):

component.render()

defupdate(self,dt):

forcomponentinself.components:

component.update(dt)

Finally,whenweremoveagameobject,weneedtodeletetheshapeofitsrigidbodyfromthePymunkspace;otherwise,thevisualresultwillbesuchthattheentitiesstillkeepcollidingwithaninvisibleobject:

defremove(self):

forcomponentinself.components:

self.remove_component(component)

ifself._shapeisnotNone:

Physics.remove(self._shape)

GameObject.instances.remove(self)

defcollide(self,other,contacts):

forcomponentinself.components:

component.on_collide(other,contacts)

Youcanfindtheimplementationofthesetwoclassesinthepyplatformer/enginecomponents.pyandpyplatformer/engine/__init__.pyscripts.

TipSavingspaceconsumption

Bydefault,eachobjectinstancehasaninternaldictionaryforattributestorage.The__slots__classvariabletellsPythontoallocatespaceonlyforeachvariable,savingspaceiftheinstancehasfewattributes.

Sincecomponentsaresmallclasses,weaddedthisoptimizationtoavoidinstantiatingunnecessarydictionaries.

Nowwewillmoveontothephysicsmodule,wheretheInputclassandtheshapecomponentsaredefined.

AddingphysicsThismoduleisresponsibleforinitializingthePymunkSpace.Thishappensatthebeginningofthefile,andthegravityandthecollisionhandleraresettodefaultvalues:

importpymunk

defcoll_handler(_,arbiter):

iflen(arbiter.shapes)==2:

obj1=arbiter.shapes[0].gameobject

obj2=arbiter.shapes[1].gameobject

obj1.collide(obj2,arbiter.contacts)

obj2.collide(obj1,arbiter.contacts)

returnTrue

space=pymunk.Space()

space.gravity=0,-10

space.set_default_collision_handler(coll_handler)

ThecollisionhandlerreceivesthespacewherethecollisionoccursasthefirstparameterandanArbiterinstancewiththecollisioninformation.Here,weobtainthegameobjectattachedtotheshapeandtriggerthecollide()method.

ThePhysicsclassissimilartoInput,sinceitcontainsacoupleofclassmethodsthatwrapthebasicfunctionalitywearelookingfor:

classPhysics(object):

@classmethod

defstep(cls,dt):

space.step(dt)

@classmethod

defremove(cls,body):

space.remove(body)

TheRigidbodyclassisabasecomponentthatreplacestheinitialstaticbodythateverygameobjecthaswithanon-staticone.Italsoaddsareferencetothegameobjectfromtheshape,soitispossibletoretrievethegameobjectfromthePymunkcollisioninformation:

classRigidbody(Component):

__slots__=['mass','is_static']

def__init__(self,mass=1,is_static=True):

self.mass=mass

self.is_static=is_static

defstart(self):

ifnotself.is_static:

#Replacethestaticbody

pos=self.gameobject._body.position

body=pymunk.Body(self.mass,1666)

body.position=pos

self.gameobject._body=body

defadd_shape_to_space(self,shape):

self.gameobject._shape=shape

shape.gameobject=self.gameobject

ifself.is_static:

space.add(shape)

else:

space.add(self.gameobject._body,shape)

NoteNotethatstaticbodiesarenotaddedtothespace;onlytheirshapesare.

TheBoxColliderandSphereCollidersubclassescreatethecorrespondingshapebycallingthePymunkAPI:

classBoxCollider(Rigidbody):

__slots__=['size']

def__init__(self,width,height,mass=1,is_static=True):

super(BoxCollider,self).__init__(mass,is_static)

self.size=width,height

defstart(self):

super(BoxCollider,self).start()

body=self.gameobject._body

shape=pymunk.Poly.create_box(body,self.size)

self.add_shape_to_space(shape)

classSphereCollider(Rigidbody):

__slots__=['radius']

def__init__(self,radius,mass=1,is_static=True):

super(SphereCollider,self).__init__(mass,is_static)

self.radius=radius

defstart(self):

super(SphereCollider,self).start()

body=self.gameobject._body

shape=pymunk.Circle(body,self.radius)

self.add_shape_to_space(shape)

Eachimplementationonlydiffersintheinformationthatispassedtobuildtheshape,butyoucaneasilyaddanotherrigidbodycomponentbyfollowingthesamepattern.

RenderablecomponentsThecomponentsmoduleincludesthedefinitionoftheRenderableclass,whichwementionedearlierwhenwelookedathowtodisplaygameobjects.

Itpushesamatrixtothecurrentstackandperformssomebasicoperationsbeforedelegatingtherenderingtothe_render()method,savingusfromrepeatingthisboilerplatecode:

classRenderable(Component):

__slots__=['color']

def__init__(self,color):

self.color=color

defrender(self):

pos=self.gameobject.position

rot=self.gameobject.rotation

scale=self.gameobject.scale

glPushMatrix()

glTranslatef(*pos)

ifrot!=(0,0,0):

glRotatef(rot[0],1,0,0)

glRotatef(rot[1],0,1,0)

glRotatef(rot[2],0,0,1)

ifscale!=(1,1,1):

glScalef(*scale)

ifself.colorisnotNone:

glColor4f(*self.color)

self._render()

glPopMatrix()

def_render(self):

pass

TheCubeandSpheresubclassesareanadaptationoftheimplementationwecoveredinChapter5,PyGameand3D,andalongwiththeLightclass,theyhavebeenomittedforbrevity.

TheCameracomponentYoumayrememberhowtosetupthecameraperspectivewithGLU,whichwasoneofthefirstactionsofthedisplayloopinthepreviouschapter.

Nowwewilluseacomponenttoperformthesametask,butitwilldifferfromothercomponentsinthewayinwhichitiscalledinthemainloop.ThishappensbecausethesematrixoperationsneedtobethefirstonesoftheOpenGLmatrixstackifwewanttosetupthecamerapositioncorrectly:

classCamera(Component):

instance=None

def__init__(self,dy,dz):

self.dy=dy

self.dz=dz

Camera.instance=self

defrender(self):

pos=self.gameobject.position

glLoadIdentity()

gluLookAt(pos[0],self.dy,self.dz,

pos[0],pos[1],pos[2],

0,1,0)

Thecamerawilllookatthegameobjectwithwhichitisattachedandthedistancealongtheyandzaxesareparameterizedinits__init__method.

Thisimplementationtakesthelastcamerathathasbeeninstantiatedasthecurrentone.Inourgame,wewillonlyuseasinglecameracomponent,andthiscanbeconsideredacommonscenarioinbasicgames.

However,ifyouneedtoswitchbetweenmultiplecameras,youcandefineamorecomplexcomponent.Asyoumaynotice,theseimplementationsareverycleanandconcisethankstotheassumptionofthesesimplifications.

TheInputManagermoduleTohandleinputevents,wecreateaseparatemodulethatwillwraptheprocessingofourpygameevents.SincewewillregisterkeystrokesandthespecialQUITeventonly,thisclassisquiteuncomplicated:

fromcollectionsimportdefaultdict

importpygame

classInput(object):

quit_flag=False

keys=defaultdict(bool)

keys_down=defaultdict(bool)

@classmethod

defupdate(cls):

cls.keys_down.clear()

foreventinpygame.event.get():

ifevent.type==pygame.QUIT:

cls.quit_flag=True

ifevent.type==pygame.KEYUP:

cls.keys[event.key]=False

ifevent.type==pygame.KEYDOWN:

cls.keys[event.key]=True

cls.keys_down[event.key]=True

@classmethod

defget_key(cls,key):

returncls.keys[key]

@classmethod

defget_key_down(cls,key):

returncls.keys_down[key]

Notethatwiththisinterface,itisstraightforwardtogetthepressedkeyswithinanycomponent,likethisforinstance:

classHorizontalMovement(Component):

defupdate(self,dt):

direction=Input.get(K_RIGHT)-Input.get(K_RIGHT)

self.gameobject.move(dt*direction*5,0)

Wecansubtractbooleanvaluesbecauseboolisasubclassofint,andTruecorrespondsto1,whileFalsecorrespondsto0.

TheGameclassTheGameclassisresponsibleforbootstrappingtheapplication—settingtheinitialvaluesofthewindowattributes,initializingtheOpenGLcontext,andenteringthegameloop:

classGame(object):

def__init__(self,caption,width=800,height=600):

self.caption=caption

self.width=width

self.height=height

self.fps=60

defmainloop(self):

self.setup()

clock=pygame.time.Clock()

whilenotInput.quit_flag:

dt=clock.tick(self.fps)

dt/=1000

Physics.step(dt)

self.update(dt)

self.render()

pygame.quit()

sys.exit()

Someoftheseinitialoperationshavebeenextractedintothesetup()method,somainloop()staysclearandunderstandable:

defsetup(self):

pygame.init()

pygame.display.set_mode((self.width,self.height),

pygame.OPENGL|pygame.DOUBLEBUF)

pygame.display.set_caption(self.caption)

glEnable(GL_LIGHTING)

glEnable(GL_COLOR_MATERIAL)

glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE)

glEnable(GL_DEPTH_TEST)

glClearColor(0.5,0.7,1,1)

glMatrixMode(GL_PROJECTION)

aspect=self.width/self.height

gluPerspective(45,aspect,1,100)

glMatrixMode(GL_MODELVIEW)

Theupdate()andrender()callsdelegatetheexecutiontothegameobjectinstances,whichinturn,willinvoketheircomponents’methods:

defupdate(self,dt):

Input.update()

forgameobjectinGameObject.instances:

gameobject.update(dt)

defrender(self):

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

ifCamera.instanceisnotNone:

Camera.instance.render()

forgameobjectinGameObject.instances:

gameobject.render()

pygame.display.flip()

Thefinalarrangementofourenginepackagelookslikethis:

Thepyplatformer/game.pyscriptcontainsthegamelogic,andthankstothemicro-frameworkthatwehavedeveloped,itwillconsistofadozenshortclassesonly.

DevelopingPyPlatformerWiththisarchitecturalbackground,wecanstartcraftingthecustomcomponentsofourplatformergame.TheComponentAPImaylooksimple,butitallowsustoimplementthefunctionalityinasuccinctmanner.

CreatingtheplatformsEachplatformofourgameisnothingbutacubewithastaticrigidbody.However,sincewewillcreateseveralinstanceswiththesecomponents,itisconvenienttodefineaclasstoavoidrepeatingthiskindofinstantiation:

classPlatform(GameObject):

def__init__(self,x,y,width,height):

super(Platform,self).__init__(x,y)

color=(0.2,1,0.5,1)

self.add_components(Cube(color,size=(width,height,2)),

BoxCollider(width,height))

AddingpickupsInplatformergames,itiscommonthattheplayerisabletocollectsomeitemsthatgivevaluableresources.Inourgame,whenthecharacterhitsoneofthesepickups,itsammoisincrementedbyfiveunits.

Todecoratethistypeofgameobject,wewilladdacomponentthatrotatestheattachedgameobjectarounditsyaxis:

classRotating(Component):

speed=50

defupdate(self,dt):

ax,ay,az=self.gameobject.rotation

ay=(ay+self.speed*dt)%360

self.gameobject.rotation=ax,ay,az

Finally,wecandefineaGameObjectsubclassthatwrapstheinstantiationofthesecomponents:

classPickup(GameObject):

def__init__(self,x,y):

super(Pickup,self).__init__(x,y)

self.tag='pickup'

color=(1,1,0.5,1)

self.add_components(Cube(color,size=(1,1,1)),

Rotating(),BoxCollider(1,1))

Weusethetagattributeasawayofidentifyingitstype.Thus,ifweneedtocheckthegameobject’stype,wedon’tneedtorelyontheisinstancefunctionandthehierarchymodel.

Shooting!Whentheplayercollideswithoneofthesepickups,itisdestroyedanditisnolongerdisplayedonthescene.Withaphysicsengine,thisalsomeansremovingthegameobjectanddisablingitsrigidbody.RememberthatwedefinedtheGameObject.remove()methodforthispurpose.

Tosimulatethegradualdisappearanceoftheobject,wewilldecreaseitsscaleuntilitdoesnotbecomevisible,andthenwewillremoveit:

classDisappear(Component):

defupdate(self,dt):

self.gameobject.velocity=0,0

s1,s2,s3=map(lambdas:s-dt*2,

self.gameobject.scale)

self.gameobject.scale=s1,s2,s3

ifs1<=0:self.gameobject.remove()

Thiscomponentisattacheddynamicallytothepickupandforcesitsremoval.TheabilitytoshootandcollecttheseitemsisdefinedintheShootercomponent:

classShoot(Component):

defon_collide(self,other,contacts):

self.gameobject.remove()

classShooter(Component):

__slots__=['ammo']

def__init__(self):

self.ammo=0

defupdate(self,dt):

ifInput.get_key_down(K_SPACE)andself.ammo>0:

self.ammo-=1

d=1ifself.gameobject.velocity.x>0else-1

pos=self.gameobject.position

shoot=GameObject(pos[0]+1.5*d,pos[1])

shoot.tag='shoot'

color=(1,1,0,1)

shoot.add_components(Sphere(0.3,color),Shoot(),

SphereCollider(0.3,mass=0.1,

is_static=False))

shoot.apply_force(20*direction,0)

defon_collide(self,other,contacts):

ifother.tag=='pickup':

self.ammo+=5

other.add_component(Disappear())

Eachtimetheplayershoots,agameobjectisinstantiatedandmovestowardsthecurrentgameobject’sdirection.IthasanauxiliaryShootcomponent.Thiscomponentremovestheshootinstancewhenitcollideswithotherrigidbodies.

Todecidewhichentitiesareaffectedbyaplayer’sshooting,weaddacomponentthat

checksthecollidingentity’staganddisappearsifitisashoot:

classShootable(Component):

defon_collide(self,other,contacts):

ifother.tag=='shoot':

self.gameobject.add_component(Disappear())

Noweachenemyhasthefollowingcomponents:

classEnemy(GameObject):

def__init__(self,x,y):

super(Enemy,self).__init__(x,y)

self.tag='enemy'

color=(1,0.2,0.2,1)

self.add_components(Sphere(1,color),Shootable(),

SphereCollider(1,is_static=False))

ThePlayerclassanditscomponentsApartfromtherigidbodyandtheShooter,ourplayercharacterhastwomaincomponents:

Respawn:Thisspawnstheplayerinitsinitialpositionifitfallsintothevoidorcollideswithanenemy.ThefollowingisanexampleoftheRespawnclass:

classRespawn(Component):

__slots__=['limit','spawn_position']

def__init__(self,limit=-15):

self.limit=limit

self.spawn_position=None

defstart(self):

self.spawn_position=self.gameobject.position

defupdate(self,dt):

ifself.gameobject.position[1]<self.limit:

self.respawn()

defon_collide(self,other,contacts):

ifother.tag=='enemy':

self.respawn()

defrespawn(self):

self.gameobject.velocity=0,0

self.gameobject.position=self.spawn_position

PlayerMovement:Thisqueriestheinputstateandcheckswhatforcescanbeappliedtothecharacter’srigidbodytomoveithorizontallyormakeitjump.HereisanexampleofthePlayerMovementclass:

classPlayerMovement(Component):

__slots__=['can_jump']

def__init__(self):

self.can_jump=False

defupdate(self,dt):

d=Input.get_key(K_RIGHT)-Input.get_key(K_LEFT)

self.gameobject.move(d*5*dt,0)

ifInput.get_key(K_UP)andself.can_jump:

self.can_jump=False

self.gameobject.move(0,8)

defon_collide(self,other,contacts):

self.can_jump|=any(c.normal.y<0forcincontacts)

NoteNotehowweusethelistofcontactpointstocheckwhethertheplayerhastouchedtheground,andthenenablethejumpmovementagain.

ThePlayerclasscombinesallofthesecomponentsintoasingleentity:

classPlayer(GameObject):

def__init__(self,x,y):

super(Player,self).__init__(x,y)

self.add_components(Sphere(1,(1,1,1,1)),

PlayerMovement(),Respawn(),

Shooter(),Camera(10,20),

SphereCollider(1,is_static=False))

ThePyPlatformerclassFinally,wedefineaGamesubclassthatinstantiatesallofourgameobjectsandplacestheminthescene:

classPyPlatformer(Game):

def__init__(self):

super(PyPlatformer,self).__init__('PyPlatformer')

self.player=Player(-2,0)

self.light=GameObject(0,10,0)

self.light.add_component(Light(GL_LIGHT0))

self.ground=[

#Platform1

Platform(3,-2,30,1),

Platform(-11,3,2,9),

Platform(8,0,2,3),

#Platform2&3

Platform(23,0,6,1),

Platform(40,2,24,1),

#Platform4&5

Platform(60,3,8,1),

Platform(84,4,26,1)

]

self.pickup=Pickup(60,5)

self.enemies=[Enemy(40,4),Enemy(90,6)]

if__name__=='__main__':

game=PyPlatformer()

game.mainloop()

Youcancheckoutallthegameobjectsandcomponentsinthepyplatformer/game.pyscript.

SummaryInthischapter,youlearnedaboutthebenefitsofacomponent-baseddesignandhowitallowsyoutobuildsmallpiecesthatcanbeadded,removed,andcombinedwithseveralgameobjects.

Notethatthemostimportantexerciseofthischapterisnothowtoimplementaplatformergame,buthowitispossibletosetupacomponent-basedframeworkandprovidethebasicbuildingblocksofagame.

Withthesefoundations,youcanaddsomebasicfunctionality,suchasmovingtheenemiesandtheplatforms,displayingmoreinformationabouttheammo,orkeepingtrackofascorebasedonthenumberofliveslostandenemiesdestroyed.

Asusual,thefinalversionofthischapter’sgameisnothingbutthestartingpointofamorecomplexapplication!

Inthenextchapter,wewillinteractwithareal-wordcheckersgameviaawebcam.ThisapplicationwillbedevelopedwithOpenCV,across-platformcomputervisionlibrary.

Chapter7.AugmentingaBoardGamewithComputerVisionComputervisionisthescienceandengineeringofsmartcamerasystems(ormorebroadly,smartimagesystems,sinceimagescancomefromanothersourcebesidesacamera).Examplesofsubtopicsincomputervisionincludefacerecognition,licenseplaterecognition,imageclassification(asusedinGoogle’sSearchbyimage),motioncapture(asusedinXboxKinectgames),and3Dscanning.

Computervision,likegamedevelopment,hasbecomemoreaccessibleinrecentyearsandisnowalmostaubiquitoustopic.“Howcanweleveragepeople’sinterestincameras?”or“Howcanweleverageallthecamerasthatareinourbuilding,ourcity,orourcountry?”isasnaturalaquestionas“Howcanweleveragepeople’sinterestingamesandsimulations?”andtheimplicationsextendbeyondentertainment.

Camerasareeverywhere.Manyofthemareattachedtopowerfulhostcomputersandnetworks.Welive,work,andplayamidstanarmyofdigitaleyes,includingwebcams,cameraphones,camera-controlledgameconsolesandsmartTVs,securitycameras,drones,andsatellites.Imagesofyoumaybecaptured,processed,ortransmittedmanytimesdaily.

ThesongwriterJohnLennonaskedusto“imagineallthepeoplelivingfortoday”but,foramoment,let’simagineallpixelsinstead.Asingleimagemaycontainmillionsofpixels,amountingtomorebytesofdatathanLeoTolstoy’sWarandPeace(anepic1500-pagenovelaboutRussiansocietyduringtheNapoleonicWars).Asinglecameramaycaptureavideostreamcontainingthousandsoftheseepic-sizedimagesperminute.Billionsofcamerasareactiveintheworld.Networksanddiskdrivesarecongestedwiththeaccumulationofimagedata,andonceanimageisonline,copiesofitmayremainincirculationindefinitely.Whenweusesoftwaretoacquire,edit,analyze,andreviewstreamsofimages,wemaynoticethatthecomputer’sCPUusagesoarswhileitsbatterypowerplummets.

Asgamedevelopers,weknowthatagoodgameenginesimplifiesalotofoptimizationproblems,suchasbatchingspritesor3DmodelstosendtotheGPUforrendering.Goodcomputervisionlibraries(andmorebroadly,goodnumericandscientificlibraries)alsosimplifyalotofoptimizationproblemsandhelpusconserveCPUusageandbatterylifewhileprocessingvideoinput(orotherlargestreamsofdata)inrealtime.Sincethe1990s,computervisionlibraries,likegameengines,havebecomemorenumerous,faster,easiertouse,andoftenfree.Thischapterleveragesthefollowinglibrariestocapture,process,anddisplayimages:

NumPy:Thisisanumericlibrary,whichwepreviouslyusedinChapter4,SteeringBehaviors.Itsdocumentationisathttp://docs.scipy.org/doc/.OpenCV:Thisisacross-platformlibraryforcomputationalphotography,computervision,andmachinelearning.OpenCV’sPythonversionrepresentsimagesasNumPyarrays.However,beneaththePythonlayer,OpenCVisimplementedinC++code

withawiderangeofhardware-specificoptimizations.Thankstotheseoptimizations,OpenCVfunctionstendtorunfasterthantheirNumPyequivalents.TheOpenCVdocumentationisathttp://docs.opencv.org/.scikit-learn:Thisisamachinelearninglibrary.ItprocessesNumPyarrays.Foritsdocumentation,refertohttp://scikit-learn.org/stable/documentation.html.WxPython:Thisisacross-platformGUIframework.Oneachplatform,WxPythoniscertaintohaveanativelookandfeelbecauseitusesnativeGUIwidgets(incomparison,manycross-platformGUIframeworks,suchasTkinter,usetheirownnon-nativewidgetswithaconfigurable“skin”thatmayemulateanativelook).WxPythonisawrapperaroundaC++librarycalledWxWidgets.ForWxPython’sdocumentation,refertohttp://wxpython.org/onlinedocs.php.

NoteLaterinthischapter,intheSettingupOpenCVandotherdependenciessection,wewilldiscusstheversionrequirementsandsetupstepsforeachlibrary.

Followingthepatternofotherchaptersinthisbook,wewillapplycomputervisiontoaclassicgame:checkers,alsoknownasdraughts.Thisboardgamehasmanyvariantsfromallculturesandallperiodsofhistoryinthepast5,000years.Itisthegrandfatherofallstrategygames.Acrossmostvariants,thegamehasthefollowingfeatures:

Therearetwoplayers,whoaresometimescalledlightanddark.Therearetwokindsofplayingpieces,sometimescalledpawnsandkings.Apawnisashort,circularplayingpiece.Akingisatall,circularplayingpiecemadebystackingtwopawns.Theboardisagridofalternatinglightanddarksquares.Piecesmayonlyoccupythedarksquares.Thesizeofthegriddependsonthevariantofthegame.Atthestartofthegame,eachplayerhasanarmyofpawns,occupyingmultipleadjacentrowsononesideoftheboard.Thetwoarmiesstartonoppositesidesoftheboardwithtwovacantrowsbetweenthem.Apiecemaycaptureanopposingpiecebyjumpingoveritintoanunoccupiedsquare.Apiecemaymakemultiplejumpsinoneturn.Onreachingthefarthestrow,apawnispromotedtoaking.Thedifferencebetweenapawnandakingdependsonthevariant.Insomevariants,onlyakingcanmovebacktowarditsstartingside.Moreover,insomevariants,apawncancrossonlyoneunoccupiedsquareatatimewhileakingcancrossmultipleunoccupiedsquares.Thelatterarecalledflyingkings.

NoteFordescriptionsofmanyinternationalvariantsofcheckers,ordraughts,refertohttps://en.wikipedia.org/wiki/Draughts.

Wewillbuildanapplicationthatmonitorsareal-worldgameofcheckersviaawebcam.Theapplicationwilldetectacheckerboardandclassifyeachsquareasanemptysquare,alightpawn,alightking,adarkpawn,oradarkking.Furthermore,theapplicationwillalterthewebcam’simagestocreateabird’s-eyeviewoftheboard,withlabelstoshowthe

resultsoftheclassification.Thisisasimplecaseofaugmentedreality,meaningthattheapplicationappliesspecialeffectsandannotationstoareal-timeviewofareal-worldobject.Wewillcallthisapplication,quitesimply,Checkers.

NoteThecompletedprojectforthischaptercanbedownloadedfromhttp://nummist.com/opencv/4507_07.zip,andanyFAQanderratacanbefoundathttp://nummist.com/opencv.

PlanningtheCheckersapplicationLet’sgivefurtherthoughttothereal-worldscenethatourCheckersprogramwillexpectandthevirtualsceneitwillcreate.Considerthefollowingclose-upphotograph,showingpartofan8x8checkerboard.Fromthetop-leftcornertoright-handside,weseealightking,alightpawn,adarkpawn,andadarkking.

Thischeckerboardisjustasheetofmattepaperonwhichblackandwhitesquaresareprinted(thepaperisgluedtoafoamboardtomakeitrigid).Theplayingpieceshappentobepokerchips—redchipsforthedarksideandgraychipsforthelightside.Astackoftwopokerchipsisapawn,whileastackoffourisaking.Asthisexamplesuggests,peoplemayplaycheckerswithhomemadeorimprovisedsets.Thereisnoguaranteethattwocheckerssetswilllookalike,sowewilltrytoavoidrigidassumptionsaboutthecolorscheme.However,highcontrastisgenerallyhelpfulincomputervision,andthisexampleisidealbecauseitusesfourcontrastingcolorsfordarksquares,lightsquares,darkpieces,andlightpieces.

Wewillassumethatalightbordersurroundsthecheckerboard(seetheprecedingimage).Thisassumptionmakesiteasiertodetecttheboard’sedge.

Lookcarefullyatthewhitesquarestotheleftofthekings.Sincethekingsaretallerthanthepawns,thekingscastlongershadowsintoadjacentlightsquares.Wewillrelyonthisobservationtodifferentiatebetweenpawnsandkings.Importantly,inabird’s-eyeviewoftheboard,theheightsoftheplayingpieceswillnotbevisiblebuttheshadowswillbe.Wewillassumethattheshadowsareapproximatelyorthogonal(notdiagonal),andaresufficientlylongtoreachanadjacentsquare.

Tofurthersimplifyourcomputervisionwork,wewillrequirethatthecameraandcheckerboardremainstationaryrelativetoeachother.Thecamerashouldhaveaviewoftheentireboardplusasmallmargin.Toachievethis,thewebcamwillneedtobeapproximately2feet,or0.6meters,awayfromtheboard(however,theexactrequirementwillvarydependingonthewebcam’sfieldofviewandtheboard’ssize).Asseeninthe

followingphotograph,atripodisagoodwaytokeepthewebcamstationaryinahighpositionfromwhereitcanviewthewholeboard:

Asthefollowingscreenshotshows,ourCheckersapplicationwilldisplayboththeunmodifiedcameraview(left)andanidealizedbird’s-eyeview(right):

Thebird’s-eyeviewcontainsannotations,orinotherwords,textforshowingtheclassificationresults.Theannotation1meansadarkpawn,11adarkking,2alightpawn,and22alightking.Alackofannotationmeansanemptysquare.

NotetheGUIwidgetsatthebottomofthescreenshot.Thesecontrolswillenabletheusertoadjustcertainparametersofourcheckerboarddetectorandsquareclassifier.Specifically,thefollowingadjustmentswillbesupported:

RotateboardCCW:Clickonthisbuttontorotatethebird’s-eyeview90degreescounterclockwise(CCW)FlipboardX:Clickonthisbuttontoflipthebird’s-eyeviewhorizontallyFlipboardY:Clickonthisbuttontoflipthebird’s-eyeviewverticallyShadowdirection:Selectoneoftheradiobuttons—up,left,down,orright—tospecifythedirectionofthekings’shadowsinthebird’s-eyeviewEmptythreshold:Movethisslidertothelefttoclassifymoresquaresasnonempty,ortotherighttoclassifymoresquaresasemptyPlayerthreshold:Movethisslidertothelefttoclassifymorepiecesaslight,ortotherighttoclassifymorepiecesasdarkShadowthreshold:Movethisslidertothelefttoclassifymorepiecesaskings,ortotherighttoclassifymorepiecesaspawns

WewilldivideourimplementationoftheCheckersapplicationintosixPythonmodules:

Checkers.py:ThismoduleimplementstheGUIusingWxPython.CheckersModel.py:ThismoduleusesOpenCV,NumPy,andscikit-learntocapture,analyze,andaugmentimagesofacheckersgameinrealtime.WxUtils.py:ThismoduleprovidesautilityfunctiontoconvertimagesfromOpenCVformatstoadisplayableWxPythonformat.TheimplementationincludesaworkaroundforaWxPythonbugthataffectsfirst-generationRaspberryPicomputers.ResizeUtils.py:UsingOpenCV,thismoduleprovidesautilityfunctiontotrytosetacamera’sresolutionandreturntheactualresolution.ColorUtils.py:UsingNumPy,thismoduleprovidesutilityfunctionstoextractacolorchannel(suchastheredcomponentofanimage)andquantifydifferencesthebetweencolors.CVBackwardCompat.py:ThismoduleprovidesaliasessothatthingsinOpenCV2.xappeartohavethesamenamesastheirequivalentsinOpenCV3.x.

Beforewewriteanyofthesemodules,let’ssetupthedependencies.

SettingupOpenCVandotherdependenciesThischapter’sprojectwillworkwithOpenCV2.xor3.xandPython2.7or3.4.NotethatOpenCV2.xdoesnotsupportPython3.x.However,onmostLinuxsystems,itismoreconvenienttoinstallOpenCV2.x(andusePython2.7)becauseOpenCV3.xdoesnothavebinarypackagesformostLinuxsystemsyet.

ThefollowingsubsectionscoveronlythesimplestwaystosetupourdependenciesonWindows,Mac,andseveralLinuxdistributions.Otherapproaches(andotherUnix-likeplatforms)canalsowork.Forexample,advancedusersmaywishtoconfigureandbuildOpenCV3.0’ssourcecodeonLinuxsystemsthatdonotyethaveprepackagedbuildsofOpenCV3.x.Forguidanceonalternativesetups,refertooneofPacktPublishing’sdedicatedbooksonOpenCV,suchasLearningOpenCV3ComputerVisionwithPythonbyJoeMinichinoandJosephHowse.

WindowsChristophGohlke,fromtheUniversityofCalifornia,Irvine,providesmanyreliableprebuiltversionsofscientificPythonlibrariesforWindows.Gotohisdownloadsiteathttp://www.lfd.uci.edu/~gohlke/pythonlibs/.IfyouhadnotinstalledNumPyinChapter4,Steeringbehaviors,getthelatestNumPy+MKLwhlfilefromGohlke’ssite(MKLisanIntelMathKernelLibrarythatprovidesoptimizations).Atthetimeofwritingthisbook,itisoneofthefollowing:

Python2.7,32-bit:numpy‑1.9.2+mkl‑cp27‑none‑win32.whlPython2.7,64-bit:numpy‑1.9.2+mkl‑cp27‑none‑win_amd64.whlPython3.4,32-bit:numpy‑1.9.2+mkl‑cp34‑none‑win32.whlPython3.4,64-bit:numpy‑1.9.2+mkl‑cp34‑none‑win_amd64.whl

OpenCommandPromptandusepiptoinstallNumPyfromthewheelfile.Therelevantcommandwillbesomethinglikethis:

$pipinstallnumpy‑1.9.2+mkl‑cp27‑none‑win32.whl

TheoutputfromthiscommandshouldincludeSuccessfullyinstallednumpy-1.9.2+mklorasimilarline.

NoteIfyourunintoproblemslaterwhileimportingmodulesfromOpenCVorscikit-learn,tryuninstallinganypreviousversionofNumPyfromChapter4,SteeringbehaviorsandusingthelatestNumPy+MKLwhlfilefromGohlke’ssite.

Similarly,getGohlke’slatestwhlfileforscikit-learnandinstallitwithpip.Atthetimeofwritingthisbook,itisoneofthefollowing:

Python2.7,32-bit:scikit_learn‑0.16.1‑cp27‑none‑win32.whlPython2.7,64-bit:scikit_learn‑0.16.1‑cp27‑none‑win_amd64.whlPython3.4,32-bit:scikit_learn‑0.16.1‑cp34‑none‑win32.whlPython3.4,64-bit:scikit_learn‑0.16.1‑cp34‑none‑win_amd64.whl

Also,atthetimeofwritingthisbook,GohlkeoffersdownloadsofOpenCV2.4forPython2.7andOpenCV3.0forPython3.4.Thefollowingarethelatestversions:

Python2.7,32-bit:opencv_python‑2.4.11‑cp27‑none‑win32.whlPython2.7,64-bit:opencv_python‑2.4.11‑cp27‑none‑win_amd64.whlPython3.4,32-bit:opencv_python‑3.0.0‑cp34‑none‑win32.whlPython3.4,64-bit:opencv_python‑3.0.0‑cp34‑none‑win_amd64.whl

IfyouaresatisfiedwithusingGohlke’slatestbuildofOpenCV,downloadthewhlfileandinstallitwithpip.

Alternatively,togetOpenCV3.xwithPython2.7bindings,wecandownloadanofficialbuildfromtheOpenCVsiteandperformsomeinstallationstepsmanually.TheavailabledownloadsofOpenCVarelistedathttp://opencv.org/downloads.html.GetthelatestversionforWindows.Atthetimeofwritingthisbook,thelatestfileiscalledopencv-

3.0.0.exe,anditincludesprebuiltbindingsforPython2.7butnot3.4.Runit,andwhenprompted,enterthefolderintowhichyouwanttoextractOpenCV.Wewillrefertothislocationas<opencv_unzip_path>.Amongotherthings,theextractedsubfolderscontainthepydanddllfiles,whicharecompiledlibraryfilesthatthePythoninterpretercanloadatruntime.Next,wemustensurethatPythoncanfindthesefiles.

CopyoneofthefollowingpydfilestothePythonsite-packagesfolder:

Python2.7,32-bit:<opencv_unzip_path>/opencv/build/python/x86/cv2.pydPython2.7,64-bit:<opencv_unzip_path>/opencv/build/python/x64/cv2.pyd

NoteTypically,thePython2.7site-packagesfolderwouldbelocatedatC:\Python27\Lib\site-packages.

Now,editthesystem’sPathvariabletoincludeoneofthefollowingfolders(whichcontaindllfilesofOpenCVandsomeofitsdependencies):

Python2.7,32-bit:<opencv_unzip_path>/build/x86/vc12/binPython2.7,64-bit:<opencv_unzip_path>/build/x64/vc12/bin

NoteThePathvariablecanbeeditedbygoingtoControlPanel|SystemandSecurity|System|Advancedsystemsettings|EnvironmentVariables….EdittheexistingvalueofPathbyappendingsomethinglike;<opencv_unzip_path>/build/x86/vc12/bin.Notetheuseofthesemicolonasaseparatorbetweenpaths.

Finally,werequirewxPython.ForPython2.7,wxPythoninstallersareavailableathttp://wxpython.org/download.php.Downloadandrunthelatestinstaller.Atthetimeofwriting,itisoneofthefollowingexefiles:

Python2.7,32-bit:wxPython3.0-win32-3.0.2.0-py27.exePython2.7,64-bit:wxPython3.0-win64-3.0.2.0-py27.exe

ForPython3.4,wemustusewxPythonPhoenix,whichisamoreactivelydevelopedforkofwxPython.SnapshotbuildsofwxPythonPhoenixarelistedathttp://wxpython.org/Phoenix/snapshot-builds/.DownloadthelatestwhlfileforPython3.4onWindows,andinstallitwithpip.Atthetimeofwritingthisbook,itisoneofthefollowing:

Python3.4,32-bit:wxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-none-win32.whl

Python3.4,64-bit:wxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-none-win_amd64.whl

Now,wehaveallthenecessarydependenciesfordevelopingourprojectonWindows.

MacTheMacPortspackagemanagerprovidesaneasycommand-linetooltoconfigure,build,andinstallopensourcesoftware,suchasOpenCV.MacPortsitselfisanopensourcecommunityproject,butitdependsonApple’sXcodedevelopmentenvironment,includingtheXcodeCommandLineTools.TosetupXcode,theXcodeCommandLineTools,andthenMacPorts,followtheinstructionsgivenathttps://www.macports.org/install.php.

OnceMacPortsissetup,wecanopentheterminalandstartinstallingpackages,alsoknownasports.Atypicalinstallationcommandhasthefollowingformat:

$sudoportinstall<package>+<variant_0>+<variant_1>...

Thevariantsarealternativeconfigurationsoftheport.Forexample,OpenCVcanbeconfiguredtobuilditsPythonbindingsandtouseoptimizationsforcertainpiecesofhardware.TheseoptimizationsaddmoredependenciestoOpenCVbutmaymakeitsfunctionsrunfaster.WewilltellOpenCVtooptimizeitselfviathreeframeworks:Eigen(forCPUvectorprocessing),IntelThreadBuildingBlocks(TBB,forCPUmultiprocessing),andOpenCL(forGPUmultiprocessing).TheOpenCLoptimizationsarenotusedbythePythoninterface,butitisgoodtohavethemjustincaseyouworkinfuturewithOpenCVinotherlanguages.

TipTosearchforportsbyname,runacommandsuchasthefollowing:

$portlist*cv*

Theprecedingcommandwilllistallportswhosenamescontaincv,notablytheopencvport.Tolistthevariantsofaport,runacommandsuchasthis:

$portvariantsopencv

ToinstallOpenCVforPython2.7,runthefollowingcommandintheterminal:

$sudoportinstallopencv+python27+eigen+tbb+opencl

Alternatively,toinstallitforPython3.4,runthiscommand:

$sudoportinstallopencv+python34+eigen+tbb+opencl

Notethatopencv+python27dependsonpython27andpy27-numpy,whileopencv+python34dependsonpython34andpy34-numpy.Thus,therelevantPythonversionandNumPyversionarealsoinstalled.AllMacPortsPythoninstallationsareseparatefromMac’sbuilt-inPythoninstallation(sometimescalledApplePython).TousetheMacPortsPython2.7installationasyourdefaultPythoninterpreter,runthefollowingcommand:

$sudoportselectpythonpython27

Alternatively,tomaketheMacPortsPython3.4installationyourdefaultPythoninterpreter,runthiscommand:

$sudoportselectpythonpython34

IfiteverbecomesnecessarytomakeApplePythonthedefaultagain,runacommandsuchasthefollowing:

$portselectpythonpython27-apple

ForPython2.7,hereisacommandthatservestoinstallscikit-learn:

$sudoportinstallpy27-scikit-learn

Similarly,forPython3.4,thefollowingcommandinstallsscikit-learn:

$sudoportinstallpy34-scikit-learn

ToinstallwxPythonforPython2.7,runthiscommand:

$sudoportinstallpy27-wxpython-3.0

ForPython3.x,weneedtousewxPythonPhoenix,whichisamoreactivelydevelopedforkofwxPython.Atthetimeofwritingthisbook,MacPortsdoesnotcontainanywxPythonPhoenixpackages,sowewillfirstsetuppipandthenuseittoinstallthelatestsnapshotbuildofwxPythonPhoenix.ToinstallpipforPython3.4,runthefollowingcommand:

$sudoportinstallpy34-pip

Wecanmakethenewlyinstalledpipthedefaultpipexecutable:

$sudoportselectpippip34

SnapshotbuildsofwxPythonPhoenixarelistedathttp://wxpython.org/Phoenix/snapshot-builds/.DownloadthelatestwhlfileforPython3.4onMac.Atthetimeofwritingthisbook,itiswxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-cp34m-macosx_10_6_intel.whl.Toinstallthispackage,runacommandsuchasthefollowing:

$sudopipinstallwxPython_Phoenix-3.0.3.dev1764+9289a7c-cp34-cp34m-

macosx_10_6_intel.whl

TheoutputfromthiscommandshouldincludeSuccessfullyinstalledwxPython-Phoenix-3.0.3.dev1764+9289a7corasimilarline.

Now,wehaveallthenecessarydependenciesfordevelopingourprojectonMac.

Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintForPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,opentheterminalandrunthefollowingcommand:

$sudoapt-getinstallpython-opencvpython-scikits-learnpython-wxgtk2.8

Notethatthepython-opencvpackagedependsonthepython-numpypackage,sopython-numpywillalsobeinstalled.

Asanalternative,youcaninstallwxPythonPhoenixinsteadofthepython-wxgtk2.8package.TosetupwxPythonPhoenixanditsdependencies,trythefollowingcommands:

$sudoapt-getinstalllibwxbase3.0-devlibwxgtk3.0-devwx-common

libwebkit-devlibwxgtk-webview3.0-devwx3.0-exampleswx3.0-headerswx3.0-

i18nlibwxgtk-media3.0-dev

$pipinstall--upgrade--pre-fhttp://wxpython.org/Phoenix/snapshot-

builds/--trusted-hostwxpython.orgwxPython_Phoenix

However,ifindoubt,justusethepython-wxgtk2.8package.

Fedoraanditsderivatives,includingRHELandCentOSForPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,opentheterminalandrunthefollowingcommand:

$sudoyuminstallopencv-pythonpython-scikit-learnwxPython

Notethattheopencv-pythonpackagedependsonthenumpypackage,sonumpywillalsobeinstalled.

OpenSUSEanditsderivativesAgain,forPython2.7,therequiredlibrariesareinthestandardrepository.Toinstallthem,opentheterminalandrunthefollowingcommand:

$sudoyuminstallpython-opencvpython-scikit-learnpython-wxWidgets

Notethatthepython-opencvpackagedependsonthepython-numpypackage,sopython-numpywillalsobeinstalled.

SupportingmultipleversionsofOpenCVOpenCV2.xandOpenCV3.xbothusethenamecv2fortheirtop-levelPythonmodule.However,insidethismodule,someclasses,functions,andconstantshavebeenrenamedinOpenCV3.x.Moreover,somefunctionalityisentirelynewinOpenCV3.x,butourprojectreliesonlyonfunctionalitythatispresentinbothmajorversions.

Tobridgeafewofthenamingdifferencesbetweenversions2.xand3.x,wecreateamodule,CVBackwardCompat.Itbeginsbyimportingcv2:

importcv2

OpenCV’sversionstringisstoredincv2.__version__.Forexample,itsvaluemaybe2.4.11or3.0.0.Wecanusethefollowinglineofcodetogetthemajorversionnumberasaninteger,suchas2or3:

CV_MAJOR_VERSION=int(cv2.__version__.split('.')[0])

ForOpenCV2.x(orearlier),wewillinjectnewnamesintotheimportedcv2modulesothatallthenecessaryOpenCV3.xnameswillbepresent.Specifically,weneedtocreatealiasesforseveralconstantsthathavenewnamesinOpenCV3.x,asseeninthefollowingcode:

ifCV_MAJOR_VERSION<3:

#CreatealiasestomakepartsoftheOpenCV2.xlibrary

#forward-compatible.

cv2.LINE_AA=cv2.CV_AA

cv2.CAP_PROP_FRAME_WIDTH=cv2.cv.CV_CAP_PROP_FRAME_WIDTH

cv2.CAP_PROP_FRAME_HEIGHT=cv2.cv.CV_CAP_PROP_FRAME_HEIGHT

cv2.FILLED=cv2.cv.CV_FILLED

ThisistheentireimplementationofCVBackwardCompat.OurothermoduleswillbeabletoimporttheCVBackwardCompatinstanceofcv2anduseanyofthealiasesthatwemayhaveinjected.Wewillseeanexampleinthenextmodule—ResizeUtils.

ConfiguringcamerasOpenCVprovidesaclass,calledcv2.VideoCapture,thatrepresentsastreamofimagesfromeitheravideofileoracamera.Thisclasshasmethodssuchasread(image)forexposingthestream’snextimageasaNumPyarray.Italsohastheget(propId)andset(propId,value)methodsforaccessingpropertiessuchasthewidthandheight(inpixels),colorformat,andframerate.Thevalidpropertiesandvaluesmaydependonthesystem’svideocodecsorcameradrivers.

Acrosscameras,thedefaultpropertyvaluesmaydifferdramatically.Forexample,onecameramightdefaulttoanimagesizeof640x480,whileanothermaydefaultto1920x1080.Forgreaterpredictability,weshouldtrytosetcrucialparametersratherthanrelyonthedefaults.Let’screateamodulecalledResizeUtilscontainingautilityfunctiontoconfiguretheimagesize.

TheResizeUtilsmodulebeginsbyimportingtheCVBackwardCompatinstanceofcv2,whichmaycontainaliases(dependingontheOpenCVversion).Hereistheimportstatement:

fromCVBackwardCompatimportcv2

ThepropertyIDsforwidthandheightarestoredinconstantscalledcv2.CAP_PROP_FRAME_WIDTHandcv2.CAP_PROP_FRAME_HEIGHT,respectively(referringtotheprevioussection,notethattheconstants’originalnamesinOpenCV2.xwerecv2.cv.CV_CAP_PROP_FRAME_WIDTHandcv2.cv.CV_CAP_PROP_FRAME_HEIGHT,butwecreatedaliasestomatchtheOpenCV3.xnames).Notetheuseoftheseconstantsinthefollowingimplementationoftheutilityfunction:

defcvResizeCapture(capture,preferredSize):

#Trytosettherequesteddimensions.

w,h=preferredSize

successW=capture.set(cv2.CAP_PROP_FRAME_WIDTH,w)

successH=capture.set(cv2.CAP_PROP_FRAME_HEIGHT,h)

ifsuccessWandsuccessH:

#Therequesteddimensionsweresuccessfullyset.

#Returntherequesteddimensions.

returnpreferredSize

#Therequesteddimensionsmightnothavebeenset.

#Returntheactualdimensions.

w=capture.get(cv2.CAP_PROP_FRAME_WIDTH)

h=capture.get(cv2.CAP_PROP_FRAME_HEIGHT)

return(w,h)

Asarguments,thefunctiontakesacv2.VideoCaptureobjectcalledcaptureandatwo-dimensionaltuplecalledpreferredSize.WetrytoconfigurecapturetousethepreferredwidthandheightinpreferredSize.Thepreferreddimensionsmayormaynotbesupported,soaswiththefeedback,wereturnatupleoftheactualwidthandheight.

TheResizeUtilsmodulewillbeusefulforourCheckersModelmodule,asCheckersModelisresponsibleforinstantiatingandreadingfromcv2.VideoCapture.

WorkingwithcolorsNormally,whenOpenCVobtainsanimagefromafileorcamera,itputstheimageintheblue-green-red(BGR)colorformat.Morespecifically,theimageisa3DNumPyarrayinwhichimage[y][x][0]isapixel’sbluevalue(intherangeof0to255),image[y][x][1]isitsgreenvalue,andimage[y][x][2]isitsredvalue.Theyandxindicesstartfromthetop-leftcorneroftheimage.Ifweconvertanimageintograyscale,itbecomesa2DNumPyarray,inwhichimage[y][x]isapixel’sgrayscalevalue.

Let’swritesomeutilityfunctionsintheColorUtilsmoduletoworkwithcolordata.OurfunctionswillusePython’sstandardmathmoduleandNumPy,asseeninthefollowingimportstatements:

importmath

importnumpy

Let’swriteafunctionthatallowsustocopyasinglecolorchannelfromasourceimage(whichisinBGRformat)toadestinationimage(whichisingrayscaleformat).IfthespecifieddestinationimageisNoneoritsformatiswrong,ourfunctionwillcreateit.Tocopythechannel,wejusttakeaflatviewofthesourceimage,sliceitwithastrideof3,andassigntheresulttoafullsliceofthedestinationimage,asseeninthefollowingimplementation:

defextractChannel(src,channel,dst):

dstShape=src.shape[:2]

ifdstisNoneordst.shape!=dstShapeor\

dst.dtype!=numpy.uint8:

dst=numpy.empty(dstShape,numpy.uint8)

dst[:]=src.flatten()[channel::3].reshape(dstShape)

returndst

Fortypicalcheckerboardsundertypicallightingconditions,theredchannelshowsahighcontrastbetweendarkandlightsquares.Later,wewillusethisobservationtoouradvantage.

Ontheotherhand,partsofouranalysiswillrelyonfullcolorsinsteadofonlyonechannel.WewillneedtoquantifythecontrastordistancebetweentwoBGRcolorsinordertodecidewhetherasquarecontainsalightpiece,adarkpiece,ashadow,ornothing.AnaïveapproachistousetheEuclideandistance,whichwouldbeimplementedlikethis:

defcolorDist(color0,color1):

returnmath.sqrt(

(color0[0]-color1[0])**2+

(color0[1]-color1[1])**2+

(color0[2]-color1[2])**2)

However,thisapproachassumesthatallcolorchannelshavethesamescaleandthescaleislinear.Thisassumptiondefiescommonsense.Forexample,mostpeoplewouldagreethatthecoloramber(b=0,g=191,r=255)isnotashadeofred(b=0,g=0,r=255)butthatthecoloremerald(b=191,g=255,r=0)isashadeofgreen(b=0,g=255,r=0),eventhoughthesetwopairingsrepresentthesameEuclideandistance(191).Manyalternative

formulationsassumethatthechannelsarestilllinearbuthavedifferentscales—typically,theyassumethatgreenhasthebiggestunit,followedbyred,andthenblue.Theseformulationsyieldhighcontrastbetweenyellowandblue(forexample,betweensunlightandshade),andyetyieldlowcontrastbetweenshadesofblue.Thisisstillratherunsatisfactorybecausemostpeoplearegoodatdistinguishingshadesofblue.ThiadmerRiemersmacomparesseveraldistanceformulationsathttp://www.compuphase.com/cmetric.htm,andproposesanalternativeinwhichthegreenscaleislinearwhiletheredandbluescalesarenonlinear,withreddifferenceshavingmoreweightincomparisonsofreddishcolorsandbluedifferenceshavingmoreweightincomparisonsofnon-reddishcolors.Let’sreplaceourpreviouscolorDistfunctionwiththefollowingimplementationofRiemersma’smethod:

defcolorDist(color0,color1):

#Calculateared-weightedcolordistance,asdescribed

#here:http://www.compuphase.com/cmetric.htm

rMean=int((color0[2]+color1[2])/2)

rDiff=int(color0[2]-color1[2])

gDiff=int(color0[1]-color1[1])

bDiff=int(color0[0]-color1[0])

returnmath.sqrt(

(((512+rMean)*rDiff*rDiff)>>8)+

4*gDiff*gDiff+

(((767-rMean)*bDiff*bDiff)>>8))

Basedontheprecedingformula,thedistancebetweenblackandwhiteisapproximately764.8.Thescaleofthisdistanceisnotintuitive.Wemightprefertoworkwithnormalizedvalues,wherebythenormalizeddistancebetweenblackandwhiteisdefinedas1.0.Thefollowingfunctionreturnsanormalizedcolordistance:

defnormColorDist(color0,color1):

#Normalizebasedonthedistancebetween(0,0,0)and

#(255,255,255).

returncolorDist(color0,color1)/764.8333151739665

NowwehavesufficientutilityfunctionstosupportourupcomingimplementationoftheCheckersModelmodule,whichwillcaptureandanalyzeimages.

BuildingtheanalyzerTheCheckersModelmoduleistheeyesandthebrainofourproject.ItbringstogethereverythingexcepttheGUI.Specifically,itdependsonNumPy,OpenCV,scikit-learn,andourColorUtilsandResizeUtilsmodules,asreflectedinthefollowingimportstatements:

importnumpy

importsklearn.cluster

fromCVBackwardCompatimportcv2

importColorUtils

importResizeUtils

NoteAlthoughwearecombiningimagecapturingandanalysisintoonemodule,theyarearguablydistinctresponsibilities.Forthisproject,theyshareadependencyonOpenCV.However,inafutureproject,youmightcaptureimagesfromacamerathatrequiresanotherlibrary,orfromanentirelydifferenttypeofsource,suchasanetwork.Youmightevensupportawidevarietyofcapturingtechniquesinoneproject.Wheneveryoufeelthatimagecaptureisacomplexprobleminitsownright,considerdedicatingatleastoneseparatemoduletoit.

Tomakeourcodemorereadable,wewilldefineseveralconstantsinthismodule.Theseconstantsrepresentthepossiblestatesoftheboard’srotation,theshadows’direction,andeachsquare’sclassification.Herearetheirdefinitions:

ROTATION_0=0

ROTATION_CCW_90=1

ROTATION_180=2

ROTATION_CCW_270=3

DIRECTION_UP=0

DIRECTION_LEFT=1

DIRECTION_DOWN=2

DIRECTION_RIGHT=3

SQUARE_STATUS_UNKNOWN=-1

SQUARE_STATUS_EMPTY=0

SQUARE_STATUS_PAWN_PLAYER_1=1

SQUARE_STATUS_KING_PLAYER_1=11

SQUARE_STATUS_PAWN_PLAYER_2=2

SQUARE_STATUS_KING_PLAYER_2=22

ThismodulewillalsocontaintheCheckersModelclass,declaredlikethis:

classCheckersModel(object):

NotethatwehavemadeCheckersModelasubclassofobject.ThisisimportantforPython2.xcompatibilitybecausewearegoingtousepropertygetterandsettermethods,asdiscussedinthenextsubsection.UnlikePython3.x,Python2.xdoesnotsupport

accessormethodsinallclasses,butratheronlyinsubclassesofobject.

ProvidingaccesstotheimagesandclassificationresultsThemembervariablesoftheCheckersModelclasswillincludecurrentimagesofthescene(thatis,everythingthatthewebcamcansee)inBGRandgrayscale,aswellasacurrentimageoftheboardinBGR.Unlikethescene,theboardwillbeabird’s-eyeviewandmaycontaintextinordertoshowtheclassificationresults.Wewillprovidepropertygetterssothatothermodulesmayreadtheimagesandtheirsizes,asseeninthefollowingcode:

@property

defsceneSize(self):

returnself._sceneSize

@property

defscene(self):

returnself._scene

@property

defsceneGray(self):

returnself._sceneGray

@property

defboardSize(self):

returnself._boardSize

@property

defboard(self):

returnself._board

NotePropertygettersandsetterssimplyprovideashorthandnotationsothatamethodlookslikeanon-callablevariable.Hereisanexamplethatdemonstrateshowtouseapropertygetter:

bm=BoardModel()

scene=bm.scene#Callsgetter,bm.scene()

Here,wehaveimplementedonlyagetterandnotasetterbecauseothermodulesshouldnotsettheimages.Thus,apieceofcodesuchasthefollowingwillproduceanerror:

bm.scene=None#Callssetter,bm.scene(None)

#Error!Thereisnosuchsetter.

Thenextsubsection,Providingaccesstoparametersfortheusertoconfigure,willgiveexamplesonhowtoimplementsettermethods.

Wealsoprovideagetterfortheclassificationresultsasa2DNumPyarray:

@property

defsquareStatuses(self):

returnself._squareStatuses

Forexample,ifPlayer1hasakinginthetop-leftcorneroftheboard,boardModel.squareStatuses[0,0]willbe11(thevalueoftheSQUARE_STATUS_KING_PLAYER_1constant,whichwedefinedearlier).

AlltheaforementionedpropertiesrepresenttheresultsoftheCheckersModelmodule’swork,soothermodulesshouldtreatthemasread-only(andthustheyhaveonlygetters,notsetters).Next,let’sconsiderotherpropertiesthatrepresenttheparametersoftheCheckersModelmodule’swork,andhavebothgettersandsetterssothatothermodulesmayreconfigurethem.

ProvidingaccesstoparametersfortheusertoconfigureTounderstandwhyCheckersModeloffersparametersthatarereconfigurableatruntime,let’slookatapreviewofthethingsthatwewillnotautomateinthisproject:

Thebird’s-eyeviewofthecheckerboardmayberotatedandflippedinawaydifferentfromwhattheuserexpects.ThisproblemisdiscussedintheCreatingandanalyzingthebird’s-eyeviewoftheboardsubsection.Weallowtheusertospecifyadifferentrotation(0,90,180,or270degrees)andflip(X,Y,neither,orboth).Theshadows’directionisnotdetectedautomatically.Bydefault,weassumethattheshadows’directionisup(negativey).Weallowtheusertospecifyadirection(up,right,down,orleft).Toclassifythecontentsofthesquares,wecomparethetwodominantcolorsinthesquare,andthiscomparisonreliesoncertainthresholdvalues.Forsomelightingconditionsandsomecolorsofcheckerssets,thedefaultthresholdsmightnotbeappropriate.Weallowtheusertospecifydifferentthresholds.TheprecisemeaningofeachthresholdisdiscussedintheAnalyzingthedominantcolorsinasquaresubsection.

Theboard’srotationisexpressedasaninteger,correspondingtooneoftheconstantsthatwedeclaredatthestartofthismodule.Thefollowingcodeimplementsthegettersandsettersfortherotation,andthesetterusesthemodulusoperatortoensurethattherotationswraparound:

@property

defboardRotation(self):

returnself._boardRotation

@boardRotation.setter

defboardRotation(self,value):

self._boardRotation=value%4

Thedirectionoftheshadowsisasimilarproperty:

@property

defshadowDirection(self):

returnself._shadowDirection

@shadowDirection.setter

defshadowDirection(self,value):

self._shadowDirection=value%4

Thethresholdsandflipdirectionsdonotrequireanyspeciallogicinagetterorsetter,sowewillsimplyimplementthemasplainoldmembervariables.Wecanseetheirdeclarationsinthefollowingcode,whichisthestartoftheclass’sinitializer:

def__init__(self,patternSize=(7,7),cameraDeviceID=0,

sceneSize=(800,600)):

self.emptyFreqThreshold=0.3

self.playerDistThreshold=0.4

self.shadowDistThreshold=0.45

self._boardRotation=ROTATION_0

self.flipBoardX=False

self.flipBoardY=False

self._shadowDirection=DIRECTION_UP

Let’sproceedtolookattherestofthemembervariablesandinitializationcode.

InitializingtheentiremodelofthegameAswehavepreviouslydiscussed,theroleoftheCheckersModelclassistoproduceimagesofthesceneandboardandclassifythecontentsofeachsquare.Theremainderofthe__init__methoddeclaresvariablesthatpertaintoimagingandclassification.

TheimagesofthesceneandboardwillinitiallybeNone,asseenhere:

self._scene=None

self._sceneGray=None

self._board=None

Lookingbackattheprevioussubsection,notethatpatternSizeisoneoftheinitializer’sarguments.Thisvariablereferstothenumberofinternalcornersintheboard,suchas(7,7)inastandardAmericancheckerboardwitheightrowsandeightcolumns.Later,wewillseethatthenumberofinternalcornersisimportantforcertainOpenCVfunctions.Let’sputthecornerdimensionsandcountintothemembervariables,likethis:

self._patternSize=patternSize

self._numCorners=patternSize[0]*patternSize[1]

Later,asthestepstowardclassification,wewillmeasurethefrequencyanddistanceofthetwodominantcolorsineachsquare.ThistaskisdiscussedintheAnalyzingthedominantcolorsinasquaresubsection.Fornow,wewillonlycreateemptyNumPyarraysforthisdata:

self._squareFreqs=numpy.empty(

(patternSize[1]+1,patternSize[0]+1),

numpy.float32)

self._squareDists=numpy.empty(

(patternSize[1]+1,patternSize[0]+1),

numpy.float32)

Similarly,wewillcreateaNumPyarrayfortheresultsofclassification,andwewillfillinthisarraywiththeSQUARE_STATUS_UNKNOWNvalue(whichwepreviouslydefinedas-1):

self._squareStatuses=numpy.empty(

(patternSize[1]+1,patternSize[0]+1),

numpy.int8)

self._squareStatuses.fill(SQUARE_STATUS_UNKNOWN)

Tofindeachsquare’sdominantcolors,wewillrelyonaclassinscikit-learncalledsklearn.cluster.MiniBatchKMeans.Thisclassrepresentsastatisticalprocesscalledk-meansclustering,whichseparatesdataintoagivennumberofgroupsandfindsthecentroidofeachgroup.Fornow,weonlyneedtoconstructaninstanceoftheclasswithasingleargument,n_clusters,whichindicatesthenumberofgroupsthattheclustererwilldistinguish(2inthiscase,becausewewanttoknowthetwodominantcolors):

self._clusterer=sklearn.cluster.MiniBatchKMeans(2)

Tosupportwebcaminput,wewillcreateaVideoCaptureobjectandsetitscapturedimensionsusingourutilityfunctionfromtheResizeUtilsmodule:

self._capture=cv2.VideoCapture(cameraDeviceID)

self._sceneSize=ResizeUtils.cvResizeCapture(

self._capture,sceneSize)

w,h=self._sceneSize

Later,aswecaptureimages,wewilltrytofindtheinternalcornersofthecheckerboard,andwewillstorethesuccessfullydetectedcornerssothatwedonothavetoperformthissearchfromscratchforeveryframe.Thefollowingtwomembervariableswillholdthepreviouslydetectedcorners(initiallyNone)andagrayscaleimageofthesceneatthetimeofthepreviousdetection(initiallyanemptyimage):

self._lastCorners=None

self._lastCornersSceneGray=numpy.empty(

(h,w),numpy.uint8)

Thescene’sdetectedcornercoordinateswillimplyahomography(amatrixthatdescribesadifferenceinperspective)whenwecomparethemwiththeknowncornercoordinatesinabird’s-eyeviewoftheboard.Likethecorners,thehomographydoesnotneedtoberecomputedforeveryframe,sowewillstoreitinthefollowingmembervariable(initiallyNone):

self._boardHomography=None

Wewillarbitrarilystipulatethatthebird’s-eyeviewoftheboardwillbenolargerthanthecapturedimageofthescene.Startingfromthisconstraint,wewillcalculatethesizeofasquareandtheboard:

self._squareWidth=min(w,h)//(max(patternSize)+1)

self._squareArea=self._squareWidth**2

self._boardSize=(

(patternSize[0]+1)*self._squareWidth,

(patternSize[1]+1)*self._squareWidth

)

Havingdeterminedthesizeofasquare,wewillcalculateasetofcornercoordinatesforthebird’s-eyeview.Weneedthecoordinatesofonlythoseinternalcornerswherefoursquaresmeet.Later,intheDetectingtheboard’scornersandtrackingtheirmotionsubsection,wewillcomparetheseidealcoordinatestotheinternalcornersofthecheckerboardthataredetectedinthesceneinordertofindthehomography.HereisourcodeforcreatingalistandthenaNumPyarrayofevenlyspacedcorners:

self._referenceCorners=[]

forxinrange(patternSize[0]):

foryinrange(patternSize[1]):

self._referenceCorners+=[[x,y]]

self._referenceCorners=numpy.array(

self._referenceCorners,numpy.float32)

self._referenceCorners*=self._squareWidth

self._referenceCorners+=self._squareWidth

ThisconcludestheinitializationoftheCheckersModelclass.Next,let’sconsiderhowtheimagesofthesceneandboardwillchange,alongwiththeresultsofclassification.

UpdatingtheentiremodelofthegameOurCheckersModelclasswillprovidethefollowingupdatemethod,whichothermodulesmaycall:

defupdate(self,drawCorners=False,

drawSquareStatuses=True):

Specifically,theGUIapplication(intheCheckersmodule)willcallthisupdatemethod.Logically,theapplicationisresponsibleformanagingresourcesandensuringthateverythingremainsresponsive,soitisinabetterpositiontodecidewhenanupdateshouldoccur.Theupdatemethod’sdrawCornersargumentspecifieswhethertheresultsofcornerdetectionshouldbedisplayedinthescene.ThedrawSquareStatusesargumentspecifieswhethertheresultsofclassificationshouldbedisplayedinthebird’s-eyeviewoftheboard.

Theupdatemethodreliesonseveralhelpermethods.First,wewillcallahelperthattriestocaptureanewimageofthescene.Ifthisfails,wereturnFalsetoinformthecallerthatnoupdatehasoccurred:

ifnotself._updateScene():

returnFalse#Failure

Wewillproceedtosearchforthecheckerboardinthescene.Asuccessfulsearchwillproduceasetofcornercoordinatesfortheboard’ssquares.Moreover,thesecoordinateswillimplyahomography.Thefollowinglineofcodecallsahelpermethodthatisresponsibleforfindingthecornersandahomography:

self._updateBoardHomography()

Next,wewillcallahelpermethodthatisresponsibleforcreatingthebird’s-eyeviewoftheboardaswellasanalyzingandclassifyingeachsquare:

self._updateBoard()

Atthispoint,theboarddetectionandsquareclassificationarecomplete,butwemaystillneedtodisplayresultsdependingonthedrawCornersanddrawSquareStatusesarguments.Conveniently,OpenCVprovidesafunction,cv2.drawChessboardCorners(image,patternSize,corners,patternWasFound),todisplayasetofdetectedcornersinascenecontainingachessboardorcheckerboard.Hereisthewayweuseit:

ifdrawCornersandself._lastCornersisnotNone:

#Drawtheboard'sgrid.

cv2.drawChessboardCorners(

self._scene,self._patternSize,

self._lastCorners,True)

Wehaveanotherhelpermethodfordisplayingtheclassificationresultinagivensquare.Thefollowingcodeshowshowweiterateoversquaresandcallthehelper:

ifdrawSquareStatuses:

foriinrange(self._patternSize[0]+1):

forjinrange(self._patternSize[1]+1):

self._drawSquareStatus(i,j)

Atthispoint,theupdatehassucceeded,andwereturnTruetoletthecallerknowthattherearenewresults:

returnTrue#Success

Let’sdelvedeeperintothehelpermethods’roles,startingwithimagecapture.

CapturingandconvertinganimageOpenCV’sVideoCaptureclasshasaread(image)method.Thismethodcapturesanimageandwritesittothegivendestinationarray(ifimageisNoneoritsformatiswrong,anewarrayiscreated).Themethodreturnsatuple,(retval,image),containingaboolean(Trueifthecapturehassucceeded)andthentheimage(eithertheoldarrayoranewlycreatedarray).Weusethismethodtotrytocaptureanewscenefromthecamera.Ifthissucceeds,weuseourextractChannelutilityfunctiontogettheredchannelasagrayscaleversionofthescene.Thefollowingmethodperformsthesestepsandreturnsabooleantoindicatesuccessorfailure:

def_updateScene(self):

success,self._scene=self._capture.read(self._scene)

ifnotsuccess:

returnFalse#Failure

#Usetheredchannelasgrayscale.

self._sceneGray=ColorUtils.extractChannel(

self._scene,2,self._sceneGray)

returnTrue#Success

Considerthefollowingstripofimages.TheleftmostimagerepresentstheoriginalsceneinBGRcolor(thoughitwillappearasgrayscaleinthisbook’sprintedition).Thisboard’scolorsareburntamberforthedarksquares,ochreforthelightsquares,andburntsiennafortheborder(thiscolorschemeisquitecommonincheckerboards).Thesecond,third,andfourthimages(fromlefttoright)representthescene’sblue,green,andredchannels,respectively:

Notethattheredchannelcapturesthecheckerboardbrightlyandwithgoodcontrastbetweenlighteranddarkersquares.Moreover,theboard’sborderappearsquitelight.Thisisgoodbecausewearesoongoingtouseadetectorthatisoptimizedforablack-and-whiteboardwithawhiteborder.

Detectingtheboard’scornersandtrackingtheirmotionWedonotwanttoupdatetheboard’scornersandhomographyineveryframe,butonlyinframeswheretheboardorcamerahasmoved.Thisrulereducesthecomputationalburdeninmostframesandallowsustokeepagooddetectionresultfromapreviousframesothatwedonotrequireanunobstructedviewoftheboardineveryframe.Particularly,wecanberelativelysureofgettinggooddetectionresultswhentheboardisempty,andwewouldwanttokeeptheseresultsforlaterframesinwhichtheboardisclutteredwithpiecesorwiththeplayers’movinghands.

Beforefindingnewcornersandthehomography,let’slookattrackingmotion.Supposewehavedetectedasetofcornersinapreviousframe.Ratherthansearchingtheentireimagefornewcorners,wecantrytofindthesamecornersatorneartheirpreviouslocations.Thisidea—mappingframe-to-framemotionofindividualpointsinanimage—iscalledopticalflow.OpenCVprovidesimplementationsofseveralopticalflowtechniques.WewilluseatechniquecalledpyramidalLukas-Kanade,whichisimplementedinthecv2.calcOpticalFlowPyrLK(prevImg,nextImg,prevPts)function.Thisfunctionreturnsatuple,(nextPts,status,error).Eachofthetuple’selementsisanarray.ThenextPtscontainsthepoints’estimatednewcoordinates.Thestatuscontainscodesforindicatingwhethereachestimateisvalid(1,meaningthepointwastracked)orinvalid(0,meaningitwaslost).Finally,errorcontainsameasurementofdissimilaritybetweenthepixelsintheoldandnewneighborhoodsforeachpoint.Ahighaverageerroracrossallpointsimpliesthateverythinglooksdifferentand,mostlikely,thattheboardorcamerahasmoved.Weapplythisreasoninginthefollowingcode,whichisthebeginningofthe_updateBoardHomographymethod:

def_updateBoardHomography(self):

ifself._lastCornersisnotNone:

corners,status,error=cv2.calcOpticalFlowPyrLK(

self._lastCornersSceneGray,

self._sceneGray,self._lastCorners,None)

#Statusis1iftracked,0ifnottracked.

numCornersTracked=sum(status)

#Ifnottracked,errorisinvalidsosetitto0.

error[:]=error*status

meanError=(sum(error)/numCornersTracked)[0]

ifmeanError<4.0:

#Theboard'soldcornersandhomographyare

#stillgoodenough.

return

NoteThethreshold,meanError<4.0,hasbeenchosenexperimentally.Ifyouseethattheestimateoftheboard’scornerschangesevenwhentheboardisstill,tryraisingthethreshold.Conversely,ifyoufindthattheestimateoftheboard’scornersremainsunchangedevenwhentheboardismoved,youmayneedtolowerthethreshold.

Ifwehavenotyetreturned,itmeansthateithertherearenopreviouslyfoundcorners,orthepreviouslyfoundcornersarenolongervalid(probablybecausetheboardorthecamerahasmoved).Eitherway,wemustsearchfornewcorners.OpenCVprovidesmanygeneral-purposefunctionsforfindinganykindofcorner,butconveniently,italsoprovidesaspecializedfunctionforfindingthecornersofsquaresinachessboardoracheckerboard.Thisfunction,cv2.findChessboardCorners(image,patternSize),canfindaboardofanyspecifieddimensions,evenanon-squareboard.ThepatternSizeargumentspecifiesthenumberofinternalcorners,suchas(7,7)forastandardAmericancheckerboardwitheightrowsandeightcolumns.Thisfunctionreturnsatuple,(retval,corners),whereretvalisaBoolean(whichisTrueifallthecornerswerefound)andcornersisalistofcornercoordinates.Itexpectsacheckerboardwithpureblackandpurewhitesquares,butothercolorschemesmayworkdependingonthestrengthoftheircontrastinthegivengrayscaleimage.Moreover,thefunctionexpectstheboardtohavealightborder,whichmakesiteasiertofindthecornersoftheoutermostdarksquares.ThefollowingcodeshowsourusageoffindChessboardCorners:

#Findthecornersintheboard'sgrid.

cornersWereFound,corners=cv2.findChessboardCorners(

self._sceneGray,self._patternSize,

flags=cv2.CALIB_CB_ADAPTIVE_THRESH)

NotethatfindChessboardCornershasanoptionalflagsargument.Weusedaflagcalledcv2.CALIB_CB_ADAPTIVE_THRESH,whichstandsforadaptivethresholding.Whenthisflagisset,thefunctionattemptstocompensatefortheoverallbrightnessoftheimagesothatitdoesnotnecessarilyrequirethesquarestolookreallyblackorreallywhite.

ThesmallcirclesandlinesshowninthefollowingimageareavisualizationofthecornersfoundusingfindChessboardCorners.TheyaredrawnusingthedrawChessboardCornersfunction,whichwecoveredintheprevioussubsection.

Ifthecornersarefound,wecanconverttheircoordinatesfromalisttoaNumPyarray.Then,wecancomparethefoundcoordinateswiththereferencecoordinatesforabird’s-eyeview(rememberthatwealreadyinitializedthereferencecoordinatesintheInitializingtheentiremodelofthegamesubsection).Tocomparethetwosetsofcoordinates,wewilluseanOpenCVfunctioncalledcv2.findHomography(srcPoints,dstPoints,method).Theoptionalmethodargumentrepresentsastrategytorejectoutliers(incongruouspointsthatshouldnotbecountedtowardtheresult).Thefunctionreturnsahomographymatrix,whichisatransformationthatmapssrcPointstodstPointswithminimalerror.ThefollowingcodedemonstratesouruseoffindHomography:

ifcornersWereFound:

#Findthehomography.

corners=numpy.array(corners,numpy.float32)

corners=corners.reshape(self._numCorners,2)

self._boardHomography,matches=cv2.findHomography(

corners,self._referenceCorners,cv2.RANSAC)

#Recordthecornersandtheirimage.

self._lastCorners=corners

self._lastCornersSceneGray[:]=self._sceneGray

Notethatwekeepacopyofthegrayscaleimageofthescene.Thenexttimethismethodiscalled,wewillcomparetheoldandnewgrayscalescenestotrackthecorners’motion.

Atthispoint,wemayhavefoundtheboard’shomography,butwehavenotyetcreatedanimagetoshowthebird’s-eyeview.Let’stacklethistasknext.

Creatingandanalyzingthebird’s-eyeviewoftheboardIftheboard’scornersandhomographyarefound,wecantransformthescene’sperspectivetoproduceabird’s-eyeviewoftheboard.TherelevantfunctioninOpenCViscv2.warpPerspective(src,M,dsize,dst),whereMisthehomographymatrixanddsizeisanarbitrarysizefortheoutputimage.Thisfunctionappliesthehomographymatrixtochangetheperspective,andthenitresizesandcropstheresult.Our_updateBoardmethodbeginswiththefollowingcode:

def_updateBoard(self):

ifself._boardHomographyisnotNone:

#Warptheboardtoobtainabird's-eyeview.

self._board=cv2.warpPerspective(

self._scene,self._boardHomography,

self._boardSize,self._board)

Thefollowingpairofimagesshowsthecamera’sviewofthescene(left),andtheresultingbird’s-eyeviewoftheboard(right)afterperspectivetransformation:

Atthisstage,thebird’s-eyeviewmightstillneedtoberotatedand/orflippedtomatchtheuser’ssubjectiveperceptionofdirections.Theuserisprobablysittingononesideoftheboardandperceivesthissideasthe“near”or“down”(positivey)side.Thecameramightbeonthesameside,anyotherside,oradiagonal!Moreover,dependingonitsdriversandconfiguration,thecameramayevencapturemirroredimages.ThefindChessboardCornersfunctiondoesnotlimititssearch.Asfarasitisconcerned,thescenecouldshowanupside-downandmirroredchessboard,andthissolutionisasgoodasanyothersolutionthatputsasetofcornersintherightplaces.Wecouldinspectandeditthevaluesinthe_boardHomographymatrixtoenforceourownassumptions,butinsteadofthis,wewilllettheuserinspecttheimageanddecideonanynecessarychanges.

Lookingcloselyattheprecedingpairofimages,notethattheboardhasadarkorblurryseamdownthemiddle(itcanfoldhere).Theseamisapproximatelyhorizontalinthecamera’sviewbutverticalinthebird’s-eyeview.Moreover,thetwoviewsdifferbyahorizontalflip.Ausermightfindthesedifferencesunintuitive.

OpenCVprovidesafunctioncalledcv2.flip(src,flipCode,dst)toflipanimageinthemannerspecifiedbyflipCode(-1orlesstoflipbothxandycoordinates,0toflipycoordinates,and1orgreatertoflipxcoordinates).Anotherfunction,cv2.transpose(src,dst),servestoswapxandycoordinateswitheachother.Arotationof90or270degreescanbeimplementedasacombinationofatransposeandaflip,whilearotationof180degreescanbeimplementedasaflipinbothdimensions.Let’susetheseapproachesinthefollowingcodetoapplyaspecifiedflipandrotation:

#Rotateandfliptheboard.

flipX=self.flipBoardX

flipY=self.flipBoardY

ifself._boardRotation==ROTATION_CCW_90:

cv2.transpose(self._board,self._board)

flipX=notflipX

elifself._boardRotation==ROTATION_180:

flipX=notflipX

flipY=notflipY

elifself._boardRotation==ROTATION_CCW_270:

cv2.transpose(self._board,self._board)

flipY=notflipY

ifflipX:

ifflipY:

cv2.flip(self._board,-1,self._board)

else:

cv2.flip(self._board,1,self._board)

elifflipY:

cv2.flip(self._board,0,self._board)

Later,wewillensurethatusercansetboardRotation,flipBoardX,andflipBoardYviatheGUI.Forexample,theusercanseetheprecedingpairofimagesandthenmakeadjustmentstoproducethefollowingpairofimagesinstead:

Oncethetransformationsarecomplete,wewilliterateoverallthesquaresandcallahelpermethodtogeneratedataabouteachsquare’scolor:

foriinrange(self._patternSize[0]+1):

forjinrange(self._patternSize[1]+1):

self._updateSquareData(i,j)

Then,wewilliterateoverallthesquaresagainandcallahelpermethodtoupdatethe

classificationofeachsquare:

foriinrange(self._patternSize[0]+1):

forjinrange(self._patternSize[1]+1):

self._updateSquareStatus(i,j)

Notethatasquare’sclassificationmayrelyondataaboutaneighboringsquare(sincewemaysearchforashadowtodistinguishakingfromapawn).Thisiswhyweanalyzethecolorsofallthesquaresinonestepandclassifythesquaresinanotherstep.Let’sconsidertheanalysisofcolorsnow.

AnalyzingthedominantcolorsinasquareOur_updateSquareDatamethodtakesasquare’sindicesasarguments,anditbeginsbycalculatingthesquare’stop-leftandbottom-rightpixelcoordinatesinthe_boardimage,asseeninthefollowingcode:

def_updateSquareData(self,i,j):

x0=i*self._squareWidth

x1=x0+self._squareWidth

y0=j*self._squareWidth

y1=y0+self._squareWidth

Rememberthatwehaveamembervariablecalled_clusterer.Itisaninstanceofthesklearn.cluster.MiniBatchKMeansclass.Thisclasshasamethod,calledfit(X),thatclassifiesthedatainXandstorestheresultsinthemembervariablesofMiniBatchKMeans.Xmustbea2DNumPyarray,sowemustreshapethesquare’simagedata.Forexample,ifasquarehas75x75pixelswiththreecolorchannels,wewillpassaviewofthesquareasanarrayofshape(75*75,3),not(75,75,3).Thefollowingcodeshowshowwesliceandreshapethesquareandpassittothefitmethod:

#Findthetwodominantcolorsinthesquare.

self._clusterer.fit(

self._board[y0:y1,x0:x1].reshape(

self._squareArea,3))

Theresultsofthecolorclusteringarestoredin_clusterer.centersand_clusterer.labels,whichareNumPyarrays.Theshapeofcentersis(2,3),anditrepresentsthetwodominantBGRcolorsinthesquare.Meanwhile,labelsisaone-dimensionalarraywhoselengthisequaltothenumberofpixelsinthesquare.Eachvalueinlabelsis0ifthepixelisclusteredwiththefirstdominantcolor,or1ifthepixelisclusteredwiththeseconddominantcolor.Thus,themeanoflabelsrepresentsthesecondcolor’sfrequency(theproportionofpixelsthatareclusteredwiththiscolor).Thefollowingcodeshowshowwecanfindthefrequencyofthelessdominantcolor,aswellasthenormalizeddistancebetweenthetwodominantcolors,basedontheformulainourColorUtilsmodule:

#Findtheproportionofthesquare'sareathatis

#occupiedbythelessdominantcolor.

freq=numpy.mean(self._clusterer.labels_)

iffreq>0.5:

freq=1.0-freq

#Findthedistancebetweenthedominantcolors.

dist=ColorUtils.normColorDist(

self._clusterer.cluster_centers_[0],

self._clusterer.cluster_centers_[1])

self._squareFreqs[j,i]=freq

self._squareDists[j,i]=dist

Thefrequencyanddistanceenableustotalkaboutthesquare’scolorsatamuchhigher

levelofabstractionthanrawchannelvalues.Forexample,wemayobserve,“Thissquarecontainsabigobjectthatcontrastsstronglywiththebackground,”(highfrequencyandhighdistance)withoutneedingtosearchforanyspecificsquarecolororplayingpiececolor.Next,wewillusesuchobservationstoclassifythecontentsofasquare.

ClassifyingthecontentsofasquareOur_updateSquareStatusesmethodtakesasquare’sindicesasarguments(again),anditbeginsbylookingupthesquare’sfrequencyanddistancedata,asseeninthiscode:

def_updateSquareStatus(self,i,j):

freq=self._squareFreqs[j,i]

dist=self._squareDists[j,i]

Wearealsointerestedinthefrequencyanddistancedataofaneighboringsquarethatmaypotentiallycontainashadow.Asdiscussedearlier,theusermayconfigureashadow’sdirection.Thefollowingcodeshowshowwecanselectaneighborbasedontheshadow’sdirection:

ifself._shadowDirection==DIRECTION_UP:

ifj>0:

neighborFreq=self._squareFreqs[j-1,i]

neighborDist=self._squareDists[j-1,i]

else:

neighborFreq=None

neighborDist=None

elifself._shadowDirection==DIRECTION_LEFT:

ifi>0:

neighborFreq=self._squareFreqs[j,i-1]

neighborDist=self._squareDists[j,i-1]

else:

neighborFreq=None

neighborDist=None

elifself._shadowDirection==DIRECTION_DOWN:

ifj<self._patternSize[1]:

neighborFreq=self._squareFreqs[j+1,i]

neighborDist=self._squareDists[j+1,i]

else:

neighborFreq=None

neighborDist=None

elifself._shadowDirection==DIRECTION_RIGHT:

ifi<self._patternSize[0]:

neighborFreq=self._squareFreqs[j,i+1]

neighborDist=self._squareDists[j,i+1]

else:

neighborFreq=None

neighborDist=None

else:

neighborFreq=None

neighborDist=None

Weexpectaking’sshadowtobeasmallregion(lowfrequency)thatcontrastsstrongly(highfrequency)withthebackgroundofalightsquare.Thus,wewilltestwhetherthefrequencyisbelowacertainthresholdandthatthedistanceisaboveanotherthreshold,asseeninthefollowingcode:

castsShadow=\

neighborFreqisnotNoneand\

neighborFreq<self.emptyFreqThresholdand\

neighborDistisnotNoneand\

neighborDist>self.shadowDistThreshold

Rememberthattheusermayconfigurethethresholdsinordertomanuallyadaptourapproachtodifferentlightingconditionsandcolorschemes.

NoteNotethatthesquaresononeedgeoftheboardwillnothaveanyneighborsintheshadow’sdirection,sowejustassumethatthereisnoshadowthere.Wecouldimproveonourapproachbyanalyzingtheborderareasjustpasttheboard’sedge.

Atthispoint,wehaveanideaofwhethertheneighbormightbeashadowornot,butwestillneedtoconsiderthecurrentsquare.Weexpectaplayingpiecetobealargeobject(highfrequency),andintheabsenceofsuchanobject,thesquaremustbeempty.Thislogicisreflectedinthefollowingcode,whichreliesonafrequencythreshold:

iffreq<self.emptyFreqThreshold:

squareStatus=SQUARE_STATUS_EMPTY

else:

Aplayingpiecemaybeeitherdarkorlight,andeitherapawnoraking.Weexpectalightplayingpiecetocontraststrongly(highdistance)withthebackgroundofadarksquare,whileadarkplayingpiecewillhaveweakercontrast.Thus,anotherdistancethresholdistested.Moreover,weexpectakingtohavealongshadowthatextendsintoaneighboringsquare,whileapawnshouldhaveashortershadow.Thefollowingcodereflectsthesecriteria:

ifdist<self.playerDistThreshold:

ifcastsShadow:

squareStatus=SQUARE_STATUS_KING_PLAYER_1

else:

squareStatus=SQUARE_STATUS_PAWN_PLAYER_1

else:

ifcastsShadow:

squareStatus=SQUARE_STATUS_KING_PLAYER_2

else:

squareStatus=SQUARE_STATUS_PAWN_PLAYER_2

Atthispoint,wehaveaclassificationresult.Wewillstoreitsothatothermethods(andothermodules,viaapropertygetter)mayaccessit:

self._squareStatuses[j,i]=squareStatus

Next,wewillprovideaconvenientwaytovisualizetheclassificationsofallthesquares.

DrawingtextAsthelaststepofupdatingtheimageoftheboard,wewilldrawtextatopeachnonemptysquaretoshowthenumericcodeoftheclassificationresult.OpenCVprovidesafunctioncalledcv2.putText(img,text,org,fontFace,fontScale,color,thickness,lineType)fordrawingtextatthepositionspecifiedbytheorg(origin)argument.Theoriginreferstothetop-leftcornerofthetext.However,wewantthetexttobecenteredinthesquare.Tofindthetext’soriginrelativetothesquare’scenter,weneedtoknowthesizeofthetextinpixels.Fortunately,OpenCVprovidesanotherfunction,cv2.getTextSize(text,fontFace,fontScale,thickness),forthispurpose.Thefollowingcodeusesthesetwofunctionstoplacethetextinthecenterofagivensquare:

def_drawSquareStatus(self,i,j):

x0=i*self._squareWidth

y0=j*self._squareWidth

squareStatus=self._squareStatuses[j,i]

ifsquareStatus>0:

text=str(squareStatus)

textSize,textBaseline=cv2.getTextSize(

text,cv2.FONT_HERSHEY_PLAIN,1.0,1)

xCenter=x0+self._squareWidth//2

yCenter=y0+self._squareWidth//2

textCenter=(xCenter-textSize[0]//2,

yCenter+textBaseline)

cv2.putText(self._board,text,textCenter,

cv2.FONT_HERSHEY_PLAIN,1.0,

(0,255,0),1,cv2.LINE_AA)

Notethatweusethecv2.FONT_HERSHEY_PLAINandcv2.LINE_AAconstantstoselecttheHersheyPlainfontandanti-aliasing.

ThiscompletesthefunctionalityoftheCheckersModelclass.Next,wewriteautilityfunctiontoconvertanOpenCVimageforusewithwxPython.Afterthis,wewillimplementtheGUIapplication.ItconfiguresaninstanceofCheckersModelanddisplaystheresultingimagesofthesceneandboard.

ConvertingOpenCVimagesforwxPythonAswehaveseenearlier,OpenCVtreatsimagesasNumPyarrays—typically,3DarraysinBGRformator2Darraysingrayscaleformat.Conversely,wxPythonhasitsownclassesforrepresentingimages,typicallyinRGBformat(thereverseofBGR).Theseclassesincludewx.Image(aneditableimage),wx.Bitmap(adisplayableimage),andwx.StaticBitmap(aGUIelementthatdisplaysaBitmap).

OurwxUtilsmodulewillprovideafunctionthatconvertsaNumPyarrayfromeitherBGRorgrayscaletoanRGBBitmap,readyfordisplayinawxPythonGUI.ThisfunctionalitydependsonOpenCVandwxPython,asreflectedinthefollowingimportstatements:

fromCVBackwardCompatimportcv2

importwx

Conveniently,wxPythonprovidesafactoryfunctioncalledwx.BitmapFromBuffer(width,height,dataBuffer),whichreturnsanewBitmap.ThisfunctioncanacceptaNumPyarrayinRGBformatasthedataBufferargument.However,abugcausesBitmapFromBuffertofailonthefirst-generationRaspberryPi,apopularsingle-boardcomputer(SBC).Asaworkaround,wecanuseapairoffunctions:wx.ImageFromBuffer(width,height,dataBuffer)andwx.BitmapFromImage(image).Thelatterislessefficient,soweshouldpreferablyuseitonlyonhardwarethatisaffectedbythebug.

Tocheckwhetherwearerunningonthefirst-generationPi,wecaninspectthenameofthesystem’sCPU,asseeninthefollowingcode:

#TrytodeterminewhetherweareonRaspberryPi.

IS_RASPBERRY_PI=False

try:

withopen('/proc/cpuinfo')asf:

forlineinf:

line=line.strip()

ifline.startswith('Hardware')and\

line.endswith('BCM2708'):

IS_RASPBERRY_PI=True

break

except:

pass

Next,let’slookatourconversionfunction’simplementationforthefirst-generationPi.First,wecheckthedimensionalityoftheNumPyarray,thenmakeaninformedguessaboutitsformat(BGRorgrayscale),andfinallyconvertittoanRGBarrayusinganOpenCVfunctioncalledcv2.cvtColor(src,code).Thecodeargumentspecifiesthesourceanddestinationformats,suchascv2.COLOR_BGR2RGB.Afterallofthis,weuseImageFromBufferandBitmapFromImagetoconverttheRGBarraytoanImageandtheImagetoaBitmap:

ifIS_RASPBERRY_PI:

defwxBitmapFromCvImage(image):

iflen(image.shape)<3:

image=cv2.cvtColor(image,cv2.COLOR_GRAY2RGB)

else:

image=cv2.cvtColor(image,cv2.COLOR_BGR2RGB)

h,w=image.shape[:2]

wxImage=wx.ImageFromBuffer(w,h,image)

bitmap=wx.BitmapFromImage(wxImage)

returnbitmap

Theimplementationforotherkindsofhardwareissimilar,exceptthatweconverttheRGBarraydirectlytoaBitmapusingBitmapFromBuffer:

else:

defwxBitmapFromCvImage(image):

iflen(image.shape)<3:

image=cv2.cvtColor(image,cv2.COLOR_GRAY2RGB)

else:

image=cv2.cvtColor(image,cv2.COLOR_BGR2RGB)

h,w=image.shape[:2]

#ThefollowingconversionfailsonRaspberryPi.

bitmap=wx.BitmapFromBuffer(w,h,image)

returnbitmap

WewillusethisfunctioninourGUIapplication,comingupnext.

BuildingtheGUIapplicationTheCheckersmodulewillcontainallofthecoderequiredfortheGUIapplication.ThismoduledependsonwxPythonaswellasPython’sstandardthreadingmoduletoallowustoputalloftheintensivecomputervisionworkontoabackgroundthread.Moreover,wewillrelyonourCheckersModelmoduleforthecapturingandanalysisofimages,andourWxUtilsmoduleforitsimageconversionutilityfunction.Herearetherelevantimportstatements:

importthreading

importwx

importCheckersModel

importWxUtils

Ourapplicationclass,Checkers,isasubclassofwx.Frame,whichrepresentsanormalwindow(notadialog).WeinitializeitwithaninstanceofCheckersModel,andawindowtitle(Checkersbydefault).Herearethedeclarationsoftheclassandthe__init__method:

classCheckers(wx.Frame):

def__init__(self,checkersModel,title='Checkers'):

WewillalsostoreCheckersModelinamembervariable,likethis:

self._checkersModel=checkersModel

Theimplementationoftheinitializercontinuesinthefollowingsubsections.WewillseehowtheapplicationlaysouttheGUI,handlesevents,andinteractswithCheckersModel.

CreatingawindowandbindingeventsWewillcreateawindowbyinitializingthewx.Framesuperclass.Ratherthanusethedefaultwindowstyle,wewillspecifyacustomstylethatdoesnotallowthewindowtoberesized.Wewillalsospecifythewindow’stitleandagraybackgroundcolorinthiscode:

style=wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.CAPTION|\

wx.SYSTEM_MENU|wx.CLIP_CHILDREN

wx.Frame.__init__(self,None,title=title,style=style)

self.SetBackgroundColour(wx.Colour(232,232,232))

MostwxPythonclassesinheritaBind(event,handler)methodfromahigh-levelclasscalledEvtHandler.Themethodregistersagivencallbackfunction(handler)foragiventypeofGUIevent(event).Whentheobjectreceivesaneventofthegiventype,thecallbackisinvoked.Forexample,let’saddthefollowinglineofcodetoensurethatagivenmethodiscalledwhenthewindowisclosed:

self.Bind(wx.EVT_CLOSE,self._onCloseWindow)

TheprecedingcallbackisimportantbecauseourCheckersclassneedstodosomecustomcleanupasitcloses.

Wecanalsogiveeventbindingsanidentifierandconnectthesebindingstokeyboardshortcutsviathewx.AcceleratorTableclass.Forexample,let’saddthiscodetobindacallbacktotheEsckey:

quitCommandID=wx.NewId()

self.Bind(wx.EVT_MENU,self._onQuitCommand,

id=quitCommandID)

acceleratorTable=wx.AcceleratorTable([

(wx.ACCEL_NORMAL,wx.WXK_ESCAPE,

quitCommandID)

])

self.SetAcceleratorTable(acceleratorTable)

Later,wewillimplementthecallbacksuchthattheEsckeymakesthewindowclose.

NowthatwehaveawindowandabasicgraspofwxPythoneventbinding,let’sstartputtingGUIelementsinthere!

CreatingandlayingoutimagesintheGUIThewx.StaticBitmapclassisaGUIelementthatdisplaysawx.Bitmap.Usingthefollowingcode,let’screateapairofStaticBitmapforourimagesofthesceneandtheboard:

self._sceneStaticBitmap=wx.StaticBitmap(self)

self._boardStaticBitmap=wx.StaticBitmap(self)

Later,wewillimplementahelpermethodtoshowtheCheckersModel'slatestimages.WewillalsocallthishelpernowtoinitializetheBitmapsofStaticBitmap:

self._showImages()

TodefinethelayoutsoftheStaticBitmapsandotherwxPythonwidgets,wemustaddthemtoakindofcollectioncalledwx.Sizer.Ithasseveraldirectandindirectsubclasses,suchaswx.BoxSizer(asimplehorizontalorverticallayout)andwx.GridSizer.Forthisproject,wewillusewx.BoxSizeronly.ThefollowingcodeputsourtwoStaticBitmapsinahorizontallayout,withthesceneimagefirst(leftmost)andtheboardimagesecond:

videosSizer=wx.BoxSizer(wx.HORIZONTAL)

videosSizer.Add(self._sceneStaticBitmap)

videosSizer.Add(self._boardStaticBitmap)

TherestoftheGUIwillconsistofbuttons,aradiobox,andsliders,allforthepurposeofenablingtheusertoconfiguretheCheckersModel.

CreatingandlayingoutcontrolsBydesign,severalpropertiesofourCheckersModelclassmustbeconfiguredinteractively.Apersonmustadjusttheseparameterswhileviewingtheresultsofboarddetectionandsquareclassification.Adjustmentsshouldbenecessaryonlyaftertheboardisinitiallydetectedandafteranymajorchangesinthelighting.

Rememberthattheboarddetectordoesnotlimititssearchtoanyrangeofrotations,anditmaychoosearotationinanyquadrant.Theusermaywishtochangetherotationtomatchhisorhersubjectiveideaofwhichwaytheboardisfacing.Usingthefollowingcode,wewillcreateabuttonlabeledRotateboardCCW,andwewillbindittoacallbackthatwillberesponsibleforadjustingtherotationbyincrementsof90degreescounterclockwise:

rotateBoardButton=wx.Button(

self,label='RotateboardCCW')

rotateBoardButton.Bind(

wx.EVT_BUTTON,

self._onRotateBoardClicked)

Alsorememberthatthedetectormayarbitrarilyfliptheboard,sinceitmakesnoassumptionaboutwhetherornotthecameraismirrored.Let’screateandbindtheFlipboardXandFlipboardYbuttonstogivetheuseradditionalcontrolovertheperspective:

flipBoardXButton=wx.Button(

self,label='FlipboardX')

flipBoardXButton.Bind(

wx.EVT_BUTTON,

self._onFlipBoardXClicked)

flipBoardYButton=wx.Button(

self,label='FlipboardY')

flipBoardYButton.Bind(

wx.EVT_BUTTON,

self._onFlipBoardYClicked)

Asthesquareclassifierreliesontheshadows’directiontodeterminewherethetallkingpieceslie,weenabletheusertospecifytheshadows’directionusingaradiobox(asetofradiobuttons).TheboxislabeledShadowdirectionandtheoptionsareup,left,down,andright,asspecifiedinthefollowingcode:

shadowDirectionRadioBox=wx.RadioBox(

self,label='Shadowdirection',

choices=['up','left','down','right'])

shadowDirectionRadioBox.Bind(

wx.EVT_RADIOBOX,

self._onShadowDirectionSelected)

Notethattheradiobuttonsintheboxarecollectivelyboundtoonecallback,whichisinvokedwheneveranybuttonispressed.

Lastamongthecontrols,wewillprovidesliderstoconfiguretheclassifier’sthresholdvalues.Thesedefineitsexpectationsaboutthecolorcontrastindifferentkindsofsquares.

Thecodeshownherecallsahelpermethodtomakeandbindthreesliders,labeledEmptythreshold,Playerthreshold,andShadowthreshold:

emptyFreqThresholdSlider=self._createLabeledSlider(

'Emptythreshold',

self._checkersModel.emptyFreqThreshold*100,

self._onEmptyFreqThresholdSelected)

playerDistThresholdSlider=self._createLabeledSlider(

'Playerthreshold',

self._checkersModel.playerDistThreshold*100,

self._onPlayerDistThresholdSelected)

shadowDistThresholdSlider=self._createLabeledSlider(

'Shadowthreshold',

self._checkersModel.shadowDistThreshold*100,

self._onShadowDistThresholdSelected)

NoteElsewhereintheCheckersclass,weimplementthefollowingmethodtohelpcreatesliders:

def_createLabeledSlider(self,label,initialValue,callback):

slider=wx.Slider(self,size=(180,20))

slider.SetValue(initialValue)

slider.Bind(wx.EVT_SLIDER,callback)

staticText=wx.StaticText(self,label=label)

sizer=wx.BoxSizer(wx.VERTICAL)

sizer.Add(slider,0,wx.ALIGN_CENTER_HORIZONTAL)

sizer.Add(staticText,0,wx.ALIGN_CENTER_HORIZONTAL)

returnsizer

Allourcontrolswillsharecertainlayoutproperties.Theywillbecenteredverticallyintheirsizerandwillhave8pixelsofpaddingontheirright-handside(toseparateeachcontrolfromitsneighbor).Let’sdeclarethesepropertiesonce,likethis:

controlsStyle=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT

controlsBorder=8

Usingtheseproperties,let’saddallthecontrolstoahorizontalsizer,asseeninthefollowingcode:

controlsSizer=wx.BoxSizer(wx.HORIZONTAL)

controlsSizer.Add(rotateBoardButton,0,

controlsStyle,controlsBorder)

controlsSizer.Add(flipBoardXButton,0,

controlsStyle,controlsBorder)

controlsSizer.Add(flipBoardYButton,0,

controlsStyle,controlsBorder)

controlsSizer.Add(shadowDirectionRadioBox,0,

controlsStyle,controlsBorder)

controlsSizer.Add(emptyFreqThresholdSlider,0,

controlsStyle,controlsBorder)

controlsSizer.Add(playerDistThresholdSlider,0,

controlsStyle,controlsBorder)

controlsSizer.Add(shadowDistThresholdSlider,0,

controlsStyle,controlsBorder)

Nowwehaveallourcontrolsinarow!

NestinglayoutsandsettingtherootlayoutSizerscanbenested.Let’screateaverticalsizerandaddourtwoprevioussizerstoitsothatourimageswillappearfirst(topmost)andourcontrolssecond:

rootSizer=wx.BoxSizer(wx.VERTICAL)

rootSizer.Add(videosSizer)

rootSizer.Add(controlsSizer,0,wx.EXPAND|wx.ALL,

border=controlsBorder)

Thewindowshouldadoptasizerastherootofthelayoutandshouldresizeitselftofitthislayout.Thenextlineofcodedoesthis:

self.SetSizerAndFit(rootSizer)

Now,wehaveaGUIwithalayoutandsomeeventbindings,buthowdowestartrunningupdatestothecheckersanalyzer?

StartingabackgroundthreadTheCheckersModelclassencapsulatesalloftheheavyprocessinginthisproject,particularlythecaptureandanalysisofimages.Ifweranitsupdate()methodonthemainthread(whichwxPythonusesforGUIevents),theGUIwouldbecomeunresponsive,becauseupdate()wouldhogtheprocessorallthetime.Thus,wemustcreateabackgroundthreadthatupdate()maysafelymonopolize.ThefollowingcodeusesPython’sstandardthreading.Threadclasstorunagivenfunctiononanewthread:

self._captureThread=threading.Thread(

target=self._runCaptureLoop)

self._running=True

self._captureThread.start()

Notethatweinitializedamembervariable,_running,beforewestartedthethread.Later,intheimplementationofthethread’sfunction,wewillusethe_runningvariabletocontroltheterminationofaloop.

Alittlelater,wewillseetheimplementationofthe_runCaptureLoop()method,whichwehaveassignedtorunonthebackgroundthread.First,let’slookattheimplementationsofthecallbackmethodsthatwehaveboundtovariousGUIevents.

ClosingawindowandstoppingabackgroundthreadWhenthewindowcloses,weneedtoensurethatthebackgroundthreadterminatesnormally,meaningthatthethread’sfunctionmustreturn.Thethreading.Threadclasshasamethodcalledjoin()thatblocksthecaller’sthreaduntilthecalleethreadreturns.Thus,itallowsustowaitforanotherthread’scompletion.Aswewillseelater,ourbackgroundthreadcontinuesuntil_runningisFalse,sowemustsetthisbeforecallingjoin().Finally,wemustcleanupthewx.FrameobjectbycallingitsDestroy()method.Hereisthecodefortherelevantcallback:

def_onCloseWindow(self,event):

self._running=False

self._captureThread.join()

self.Destroy()

NoteAlleventcallbacksinwxPythonrequireaneventargument.Dependingonthetypeofevent,theeventargumentmayhavepropertiesthatgivedetailsabouttheuser’sinputorothercircumstances.

Thewindowwillclose(andtheprecedingcallbackwillbecalled)whentheuserclicksonthestandardclosebutton(X),orwhenwecallthewx.FrameobjectsClose()method.RememberthatwewantthewindowtoclosewhentheuserpressestheEsckey.Thus,theEsckeyismappedtothefollowingcallback:

def_onQuitCommand(self,event):

self.Close()

Therestofourcallbacksrelatetocontrolsthatrepresenttheanalyzer’sconfiguration.

ConfiguringtheanalyzerbasedonuserinputRememberthattheCheckersModelclasshasaboardRotationproperty.Thisisanintegerintherangeof0to3,representingcounterclockwiserotationin90-degreeincrements.Theproperty’ssetterusesamodulusoperatortoensurethatavalueof4circlesbackto0,andsoon.Thus,whentheuserclicksontheGUI’sRotateboardCCWbutton,wecansimplyincrementboardRotation,likethis:

def_onRotateBoardClicked(self,event):

self._checkersModel.boardRotation+=1

Similarly,whentheuserclicksontheFlipboardXorFlipboardYbutton,wecannegatethevalueoftheflipBoardXorflipBoardYproperty(rememberthatthesepropertiesarebooleans).Herearetherelevantcallbacks:

def_onFlipBoardXClicked(self,event):

self._checkersModel.flipBoardX=\

notself._checkersModel.flipBoardX

def_onFlipBoardYClicked(self,event):

self._checkersModel.flipBoardY=\

notself._checkersModel.flipBoardY

LiketheboardRotationproperty,theshadowDirectionpropertyisanintegerthatrepresentsacounterclockwiserotationin90-degreeincrements.TheoptionsintheShadowdirectioncontrolboxarearrangedinthesameorder(up,right,down,andleft).Conveniently,wxPythongivestheselectedoption’sindextothecallbackinevent.Selection.WecanassignthisindextotheshadowDirectionproperty,asseeninthefollowingcode:

def_onShadowDirectionSelected(self,event):

self._checkersModel.shadowDirection=event.Selection

Thethresholdproperties(emptyFreqThreshold,playerDistThreshold,andshadowDistThreshold)arefloating-pointnumbersintherangeof0.0to1.0.Bydefault,wxPython’sslidersinterprettheuser’sinputasanintegerintherangeof0to100.Thus,whentheusermovesaslider,wechangethecorrespondingthresholdtoone-hundredthoftheslider’svalue,asseeninthesecallbacks:

def_onEmptyFreqThresholdSelected(self,event):

self._checkersModel.emptyFreqThreshold=\

event.Selection*0.01

def_onPlayerDistThresholdSelected(self,event):

self._checkersModel.playerDistThreshold=\

event.Selection*0.01

def_onShadowDistThresholdSelected(self,event):

self._checkersModel.shadowDistThreshold=\

event.Selection*0.01

Astheuserchangestheproperties,theeffectontheclassificationshouldbevisibleinrealtime,orwithonlyamomentarylag.Let’sconsiderhowtoupdateanddisplaytheresults.

UpdatingandshowingimagesOnthebackgroundthread,theapplicationwillcontinuallyaskitsCheckersModelinstancetoupdatetheimagesandtheanalysis.Wheneveranupdatesucceeds,theapplicationmustalteritsGUItoshowthenewimages.ThisGUIeventmustbeprocessedonthemain(GUI)thread;otherwise,theapplicationwillcrash,becausetwothreadscannotaccesstheGUIobjectsatonce.Conveniently,wxPythonprovidesafunctioncalledwx.CallAfter(callableObj)tocallatargetfunction(orsomeothercallableobject)onthemainthread.Thefollowingcodeimplementsourbackgroundthread’sloop,whichterminateswhen_runningisFalse:

def_runCaptureLoop(self):

whileself._running:

ifself._checkersModel.update():

wx.CallAfter(self._showImages)

Onthemainthread,wegettheCheckersModelimagesoftheunprocessedsceneandtheprocessedboard,convertthemtowxPython’sBitmapformatusingourutilityfunction,anddisplaythemintheapplication’sStaticBitmap.Thesetwomethodscarryoutthiswork:

def_showImages(self):

self._showImage(

self._checkersModel.scene,self._sceneStaticBitmap,

self._checkersModel.sceneSize)

self._showImage(

self._checkersModel.board,self._boardStaticBitmap,

self._checkersModel.boardSize)

def_showImage(self,image,staticBitmap,size):

ifimageisNone:

#Provideablackbitmap.

bitmap=wx.EmptyBitmap(size[0],size[1])

else:

#Converttheimagetobitmapformat.

bitmap=WxUtils.wxBitmapFromCvImage(image)

#Showthebitmap.

staticBitmap.SetBitmap(bitmap)

ThisistheendoftheCheckersclass,butwestillneedonemorefunctionintheCheckersmodule.

RunningtheapplicationLet’swriteamain()functiontolaunchaninstanceoftheCheckersapplicationclass.Thisfunctionmustalsocreatetheapplication’sinstanceofCheckersModel.Wewillallowtheusertospecifythecameraindexasacommand-lineargument.Forexample,iftheuserrunsthefollowingcommand,CheckersModelwilluseacameraindexof1:

$pythonCheckers.py1

Hereisthemain()function’simplementation:

defmain():

importsys

iflen(sys.argv)<2:

cameraDeviceID=0

else:

cameraDeviceID=int(sys.argv[1])

checkersModel=CheckersModel.CheckersModel(

cameraDeviceID=cameraDeviceID)

app=wx.App()

checkers=Checkers(checkersModel)

checkers.Show()

app.MainLoop()

if__name__=='__main__':

main()

That’sallforthecode!Let’sgrabawebcamandacheckersset!

Troubleshootingtheprojectinreal-worldconditionsAlthoughthisprojectshouldworkwithmanycheckerssets,cameraperspectives,andlightingsetups,itstillrequirestheusertosetthestagecarefully,inaccordancewiththefollowingguidelines:

1. Ensurethatthewebcamandtheboardarestationary.Thewebcamshouldbeonatripodorsomeothermount.Theboardshouldalsobesecuredinplace,oritshouldbesufficientlyheavysothatitdoesnotmovewhentheplayerstouchit.

2. Leavetheboardemptyuntiltheapplicationdetectsitanddisplaysthebird’s-eyeview.Ifthereareplayingpiecesontheboard,theywillprobablyinterferewiththeinitialdetection.

3. Forbestresults,useablack-and-whitecheckerboard.Othercolorschemes(suchasdarkwoodandlightwood)maywork.

4. Forbestresults,useaboardwithalightborderaroundtheplayingarea.5. Also,forbestresults,useaboardwithamattefinishornofinish.Reflectionsona

glossyboardwillprobablyinterferewiththeanalysisofthedominantcolorsinsomesquares.

6. Puttheplayingpiecesonthedarksquares.7. Useplayingpiecesthatarenotofthesamecolorasthedarksquares.Forexample,on

ablackandwhiteboard,useredandwhite(orredandgray)pieces.8. Useplayingpiecesthataresufficientlytall.Aking(apairofstackedpieces)must

castashadowonanadjacentsquare.9. Ensurethatthescenehasbrightlightcomingpredominantlyfromonedirection,such

asdaylightfromawindow.Alsoensurethattheboardisalignedsuchthatthelightisapproximatelyparalleltothexorygridlines.Thus,akings’shadowwillfallonanadjacentlightsquare.

Experimentwithvariousreal-worldconditionsandvarioussettingsintheapplicationtoseewhatworksandwhatfails.Everycomputervisionsystemhaslimits.Likeaperson,itdoesnotseeeverythingclearly,anditdoesnotunderstandallthatitsees.Aspartofourtestingprocess,weshouldalwaysstrivetofindthesystem’slimits.Thiswilldriveustoadoptorinventmorerobusttechniquesforourfuturework.

FurtherreadingonOpenCVOurcheckersapplicationgivesaglimpseoftheworldofcomputervision.Yet,thereismuchmoretosee!ForPythonprogrammers,thefollowingbooksfromPacktPublishingrevealabroadrangeofOpenCV’sfunctionality,withimpressiveapplications:

LearningOpenCV3ComputerVisionwithPython(October2015),byJoeMinichinoandJosephHowse:ThisisagrandtourofOpenCV’sPythoninterfaceandtheunderlyingtheoriesincomputervision,machinelearning,andartificialintelligence.Withagentlelearningcurve,itissuitableforeitherbeginnersorthosewhowanttoroundouttheirknowledgeofthelibraryanditsuses.RaspberryPiComputerVisionProgramming(May2015),byAshwinPajankar:Thisbeginner-friendlybookemphasizestechniquesthatarepracticalforlow-cost,low-poweredplatformssuchastheRaspberryPisingle-boardcomputers.OpenCVforSecretAgents(January,2015),byJosephHowse:Thisisacollectionofcreative,adventurousprojectsforintermediatetoadvanceddeveloperswhomaybenewtocomputervision.Ifyouwanttoperformabiometricrecognitionofacatorseepeople’sheartbeatsthroughamotion-amplifyingwebcam,thenthisisthe(only)bookforyou!

Besidestheseoptions,PacktPublishingoffersmorethanadozenotherbooksonOpenCVandcomputervisionforC++,Java,Python,iOS,andAndroiddevelopers.Wehopetomeetyouagaininanexplorationofthisexcitingandburgeoningtopic!

SummaryWhileanalyzingagameofcheckers,weencounteredseveralfundamentalaspectsofcomputervisioninthefollowingtasks:

CapturingimagesfromacameraPerformingcomputationsoncolorandgrayscaledataDetectingandrecognizingasetoffeatures(thecornersoftheboard’ssquares)TrackingmovementsofthefeaturesSimulatingadifferentperspective(abird’s-eyeviewoftheboard)Classifyingtheregionsoftheimage(thecontentsoftheboard’ssquares)

WealsosetupandusedOpenCVandotherlibraries.Havingbuiltacompleteapplicationusingtheselibraries,youareinabetterpositiontounderstandthepotentialofcomputervisionandplanfurtherstudiesandprojectsinthisfield.

ThischapteralsoconcludesourjourneytogetherinPythonGameProgrammingbyExample.Webuiltmodernversionsofclassicvideogamesaswellasananalyzerforaclassicboardgame.Alongtheway,yougainedpracticalexperienceinmanyofthemajorgameengines,graphicsAPIs,GUIframeworks,andscientificlibrariesforPython.Theseskillsaretransferabletootherlanguagesandlibraries,andwillserveasyourfoundationagainandagainasyoubuildyourowngamesandotherpiecesofvisualsoftware!

Gocreatesomemoregreatprojects,sharethefun,andstayintouchwithus(AlejandroRodasdePazat<[email protected]>andJosephHowseathttp://nummist.com/)!

IndexA

AARectShapeclass/ProcessingcollisionsActor/SpaceInvadersdesignadd(obj)method/Processingcollisionsadd_ballmethod/AddingtheBreakoutitemsadd_brickmethod/AddingtheBreakoutitemsanalyzer

building/Buildingtheanalyzeraccess,providingtoimages/Providingaccesstotheimagesandclassificationresultsaccess,providingtoclassificationresults/Providingaccesstotheimagesandclassificationresultsaccess,providingtoparametersforuserconfiguration/Providingaccesstoparametersfortheusertoconfigureentiregamemodel,initializing/Initializingtheentiremodelofthegameentiregamemodel,updating/Updatingtheentiremodelofthegameimage,capturing/Capturingandconvertinganimageimage,converting/Capturingandconvertinganimageboard’scorners,detecting/Detectingtheboard’scornersandtrackingtheirmotionboard’smotion,tracking/Detectingtheboard’scornersandtrackingtheirmotionbird’s-eyeview,creating/Creatingandanalyzingthebird’s-eyeviewoftheboarddominantcolors,analyzinginsquare/Analyzingthedominantcolorsinasquarecontentsofasquare,classifying/Classifyingthecontentsofasquaretext,drawing/Drawingtext

ApplePython/MacArbiter/Addingphysicsarrivalbehavior/Arrival

Bball

movement/Movementandcollisionscollisions/Movementandcollisions

Ballclass/TheBallclassbasicgameobjects

adding,togamelayer/ThegamelayerBreakoutgame

overview/AnoverviewofBreakoutplaying/PlayingBreakout

Breakoutitemsadding/AddingtheBreakoutitemsgameobjectinstantiation/AddingtheBreakoutitemskeyinputbinding/AddingtheBreakoutitems

Brickclass/TheBrickclass

Ccameras

configuring/ConfiguringcamerasCanvaswidget

about/DivingintotheCanvaswidgetcanvaswidget

canvas.coords()method/DivingintotheCanvaswidgetcanvas.move()method/DivingintotheCanvaswidgetcanvas.delete()method/DivingintotheCanvaswidgetcanvas.winfo_width()method/DivingintotheCanvaswidgetcanvas.itemconfig()method/DivingintotheCanvaswidgetcanvas.bind()method/DivingintotheCanvaswidgetcanvas.unbind()method/DivingintotheCanvaswidgetcanvas.create_text()method/DivingintotheCanvaswidgetcanvas.find_withtag()method/DivingintotheCanvaswidgetcanvas.find_overlapping()method/DivingintotheCanvaswidgetreferences,URL/DivingintotheCanvaswidget

Checkersapplicationplanning/PlanningtheCheckersapplication

check_collisionsmethod/StartingthegameChimpunk2D

URL/IntroducingPymunkCircleShapeclass/Processingcollisionsclear()method/ProcessingcollisionsCocos2d

actions/Cocos2dactionscocos2d

installing/Installingcocos2dabout/Gettingstartedwithcocos2dscene/Gettingstartedwithcocos2dlayer/Gettingstartedwithcocos2dsprite/Gettingstartedwithcocos2ddirector/Gettingstartedwithcocos2dclassmembers/Gettingstartedwithcocos2dfullscreenparameter/Gettingstartedwithcocos2dresizableparameter/Gettingstartedwithcocos2dvsyncparameter/Gettingstartedwithcocos2dwidthparameter/Gettingstartedwithcocos2dheightparameter/Gettingstartedwithcocos2dcaptionparameter/Gettingstartedwithcocos2dvisibleparameter/Gettingstartedwithcocos2duserinput,handling/Handlinguserinputscene,updating/Updatingthescene

collisions,processing/ProcessingcollisionsShootactor/Shoot’emup!HUD,adding/AddinganHUDmysteryship/Extrafeature–themysteryship

Cocos2dactionsabout/Cocos2dactionsinstantactions/Cocos2dactions,Instantactionsintervalactions/Intervalactionscombining/Combiningactionscustomactions/Customactions

CocosNode/Gettingstartedwithcocos2dcollisiondetectiongame

about/BasiccollisiondetectiongameCollisionManagerinterface

about/ProcessingcollisionsCollisionManagerBruteForce/ProcessingcollisionsCollisionManagerGrid/Processingcollisions

colorsworkingwith/Workingwithcolors

componentbaseclass/Component-basedgameenginesCshape/Processingcollisionscustomactions

about/Customactions

Ddistanceformulation

CentOS,settingupTopicnURL/WorkingwithcolorsDomain-specificLanguage(DSL)/Thescenariodefinitiondraw_textmethod/AddingtheBreakoutitems

Eevadebehavior/Pursuitandevade

Ffaceculling

about/TheCubeclassenabling/Enablingfaceculling

fleebehavior/Seekandfleeframespersecond(FPS)/Pygame101freeglut/Installingpackagesfutureposition/Pursuitandevade

Ggame

starting/Startingthegamegameassets

creating/CreatinggameassetsGameclass/TheGameclassgamedesign

about/Anintroductiontogamedesignleveldesign/Leveldesignplatformerskills/Platformerskillscomponent-basedgameengines/Component-basedgameengines

gameframeworkbuilding/Buildingagameframeworkstart()method/Buildingagameframeworkupdate(dt)method/Buildingagameframeworkon_collide(other,contacts)method/Buildingagameframeworkstop()method/Buildingagameframeworkphysics,adding/Addingphysicsrenderablecomponents/RenderablecomponentsInputManagermodule/TheInputManagermoduleGameclass/TheGameclass

gamelayerbasicgameobjects,addingto/Thegamelayer

GameLayerclass/ThePlayerCannonandGameLayerclassesgameobjects

about/BasicgameobjectsBallclass/TheBallclassPaddleclass/ThePaddleclassBrickclass/TheBrickclass

GameObjects/Component-basedgameenginesgamescene

about/GamesceneHUDclass/TheHUDclassassembling/Assemblingthescene

glClear()function/DrawingshapesglClearColor()function/InitializingthewindowglColorfunctions/InitializingthewindowglLightfv()function/DrawingshapesglLoadIdentity()function/DrawingshapesglMaterialfv()function/DrawingshapesglMatrixMode()function/InitializingthewindowglPopMatrix()function/DrawingshapesglPushMatrix()function/Drawingshapes

glTranslate()function/DrawingshapesgluLookAt()function/DrawingshapesgluPerspective()

function/InitializingthewindowglutDisplayFunc()function/InitializingthewindowGLUTlicensing/GettingstartedwithOpenGLglutMainLoop()function/InitializingthewindowglutSolidSphere()function/DrawingshapesglutSpecialFunc()function/Initializingthewindow,Processingtheuserinputgravitationgame

about/Gravitationgamebasicgameobjects/Basicgameobjectsplanets/Planetsandpickupspickups/Planetsandpickupsplayer/Playerandenemiesenemies/Playerandenemiesexplosions/Explosions

GUIapplicationbuilding/BuildingtheGUIapplicationwindow,creating/Creatingawindowandbindingeventsevents,binding/Creatingawindowandbindingeventsimages,layingout/CreatingandlayingoutimagesintheGUIimages,creating/CreatingandlayingoutimagesintheGUIcontrols,layingout/Creatingandlayingoutcontrolscontrols,creating/Creatingandlayingoutcontrolslayouts,nesting/Nestinglayoutsandsettingtherootlayoutrootlayout,setting/Nestinglayoutsandsettingtherootlayoutbackgroundthread,starting/Startingabackgroundthreadwindow,closing/Closingawindowandstoppingabackgroundthreadbackgroundthread,stopping/Closingawindowandstoppingabackgroundthreadanalyzerbasedonuserinput,configuring/Configuringtheanalyzerbasedonuserinputimages,updating/Updatingandshowingimagesimages,displaying/Updatingandshowingimagesrunning/Runningtheapplication

GUIlayout/ThebasicGUIlayout

Hhomography/Initializingtheentiremodelofthegame

Iinstallation

Python/InstallingPythoninstallation,NumPy/NumPyinstallationinstantactions

about/Cocos2dactions,InstantactionsPlace/InstantactionsCallFunc/InstantactionsCallFuncS/InstantactionsHide/InstantactionsShow/InstantactionsToggleVisibility/Instantactions

IntegratedDevelopmentEnvironments(IDE)about/Aquickdemonstration

intervalactionsabout/IntervalactionsLerp/IntervalactionsMoveTo/IntervalactionsMoveBy/IntervalactionsJumpTo/IntervalactionsJumpBy/IntervalactionsBezier/IntervalactionsBlink/IntervalactionsRotateTo/IntervalactionsRotateBy/IntervalactionsScaleTo/IntervalactionsScaleBy/IntervalactionsFadeOut/IntervalactionsFadeIn/IntervalactionsFadeTo/IntervalactionsDelay/IntervalactionsRandomDelay/Intervalactions

invadersabout/Invaders!Alienclass/Invaders!AlienColumnclass/Invaders!AlienGroup.class/Invaders!

iter_colliding(obj)method/Processingcollisions

Kk-meansclustering/Initializingtheentiremodelofthegameknownentities/Processingcollisionsknows(obj)method/Processingcollisions

Llayer/Cocos2dactionslearningcurve/Leveldesign

MMac

settingup/MacMacPorts

URL/Macmainmenu

adding/Addingamainmenumathmodule

about/IntervalactionsURL/Intervalactions

Menu/Addingamainmenumove()method/AddingtheBreakoutitems

NNumPy

installing/NumPyinstallationURL,forofficialbinaries/NumPyinstallationURL,forunofficialcompiledbinaries/NumPyinstallation

Oobstacleavoidancebehavior

about/ObstacleavoidanceOpenCV

settingup/SettingupOpenCVandotherdependenciesdependencies,settingup/SettingupOpenCVandotherdependenciesWindows,settingup/WindowsURL/WindowsMac,settingup/MacDebian,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintRaspbian,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintUbuntu,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintLinuxMint,settingup/Debiananditsderivatives,includingRaspbian,Ubuntu,andLinuxMintFedora,settingup/Fedoraanditsderivatives,includingRHELandCentOSFedora,derivates/Fedoraanditsderivatives,includingRHELandCentOSRHEL,settingup/Fedoraanditsderivatives,includingRHELandCentOSCentOS,settingup/Fedoraanditsderivatives,includingRHELandCentOSOpenSUSE/OpenSUSEanditsderivativesOpenSUSE,derivates/OpenSUSEanditsderivativesmultipleversions,supporting/SupportingmultipleversionsofOpenCVreferences/FurtherreadingonOpenCV

OpenCVimagesconverting,forwxPython/ConvertingOpenCVimagesforwxPython

OpenGLabout/GettingstartedwithOpenGLwindow,initializing/Initializingthewindowshapes,drawing/Drawingshapesdemo,running/Runningthedemoprogram,refactoring/RefactoringourOpenGLprogramuserinput,processing/Processingtheuserinputused,fordrawing/DrawingwithOpenGLCubeclass/TheCubeclassfaceculling,enabling/Enablingfaceculling

OpenGLUtilityLibrary/InitializingthewindowOpenGLUtilityToolkit/GettingstartedwithOpenGLopticalflow/Detectingtheboard’scornersandtrackingtheirmotion

Ppackageinstallation

PyOpenGL/Installingpackagesfreeglut/InstallingpackagesPygame/Installingpackages

packagesinstalling/Installingpackages

Paddleclass/ThePaddleclassParticleSystemclass

about/TheParticleSystemclassdemonstrating/Aquickdemonstration

PlayerCannonclass/ThePlayerCannonandGameLayerclassesPlayerclass

about/ThePlayerclassanditscomponentsrespawn/ThePlayerclassanditscomponentsPlayerMovement/ThePlayerclassanditscomponents

projecttroubleshooting/Troubleshootingtheprojectinreal-worldconditions

pursuitbehavior/PursuitandevadePygame

about/InstallingpackagesURL/Installingpackages,Pygame101Macintosh,URL/Installingpackagesadding/AddingthePygamelibrary101/Pygame101documentation/Pygame101integration/Pygameintegration

Pygletabout/Installingcocos2deventframework/Handlinguserinput

pyglet.window.keymoduleURL/Handlinguserinput

Pymunkabout/IntroducingPymunk

pymunkpackage,classesspace/IntroducingPymunkbody/IntroducingPymunkshape/IntroducingPymunkarbiter/IntroducingPymunk

PyOpenGLabout/InstallingpackagesURL/Installingpackages,Initializingthewindow

PyPlatformer

developing/DevelopingPyPlatformerplatforms,creating/Creatingtheplatformspickups,adding/Addingpickupsshootingaction/Shooting!Playerclass/ThePlayerclassanditscomponentsclass/ThePyPlatformerclass

PyPlatformerclass/ThePyPlatformerclasspyramidalLukas-Kanade/Detectingtheboard’scornersandtrackingtheirmotionPython

installing/InstallingPythonURL/InstallingPythonpackagingecosystem/Installingcocos2dspecialmethods/Combiningactions

Python2andTkinter/InstallingPython

PythonExtensionPackagesURL/Installingpackages

Pythonmodules,CheckersapplicationCheckers.py/PlanningtheCheckersapplicationCheckersModel.py/PlanningtheCheckersapplicationWxUtils.py/PlanningtheCheckersapplicationResizeUtils.py/PlanningtheCheckersapplicationColorUtils.py/PlanningtheCheckersapplicationCVBackwardCompat.py/PlanningtheCheckersapplication

PythonPackageIndex(PyPi)/Installingcocos2d

Rrender()method/Buildingagameframeworkrenderablecomponents

about/Renderablecomponentscameracomponent/TheCameracomponent

Sscenario

definition/Thescenariodefinitionscenarioclass/Thescenarioclassscenariomodule/Thescenarioclassscenes

about/Cocos2dactionstransitionsbetween/Transitionsbetweenscenesgameovercutscene/Gameovercutscene

seekbehavior/SeekandfleeSimpleDirectMediaLayer(SDL)/PygameintegrationSingle-boardComputer(SBC)/ConvertingOpenCVimagesforwxPythonslowingarea/ArrivalSpaceInvadersdesign

about/SpaceInvadersdesignPlayerCannonclass/SpaceInvadersdesign,ThePlayerCannonandGameLayerclassesAlienclass/SpaceInvadersdesignAlienColumnclass/SpaceInvadersdesignAlienGroupclass/SpaceInvadersdesignShootclass/SpaceInvadersdesignPlayerShootclass/SpaceInvadersdesignGameLayerclass/ThePlayerCannonandGameLayerclasses

sprite/Cocos2dactionsstartmethod

glutInit()/InitializingthewindowglutInitWindowPosition()/InitializingthewindowglutInitWindowsSize()/InitializingthewindowglutCreateWindow()/InitializingthewindowglEnable()/Initializingthewindow

start_gamemethod/Startingthegamesteeringbehaviors

implementing/Implementingsteeringbehaviorsseek/Seekandfleeflee/Seekandfleearrival/Arrivalpursuit/Pursuitandevadeevade/Pursuitandevadewander/Wanderobstacleavoidance/Obstacleavoidance

steeringbehaviors,forautonomouscharactersreferencelink/Implementingsteeringbehaviors

subclasses,MenuItem

ToggleMenuItem/AddingamainmenuMultipleMenuItem/AddingamainmenuEntryMenuItem/AddingamainmenuImageMenuItem/AddingamainmenuColorMenuItem/Addingamainmenu

super(MyClass,self).__init__(arguments)/ThebasicGUIlayoutsyntacticsugar/Combiningactions

TThreadBuildingBlocks(TBB)/MacTiledMapEditor

URL/TiledMapEditorabout/TiledMapEditor

tilemapsabout/TilemapsTiledMapEditor/TiledMapEditortiles,loading/Loadingtiles

TkinterURL/InstallingPythonandPython2/InstallingPython

TMXformat/TiledMapEditortowerdefenseactors

about/Thetowerdefenseactorsturret/Turretsandslotsslots/Turretsandslotsenemies/Enemiesbunker/Bunker

towerdefensegameplayabout/Thetowerdefensegameplay

Uupdate()method/Buildingagameframeworkupdatemethod/Movementandcollisionsupdate_lives_textmethod/AddingtheBreakoutitems

Vvalues,wanderbehavior

wander_angle/Wandercircle_distance/Wandercircle_radius/Wanderangle_change/Wander

Wwanderbehavior

about/Wandervalues/Wander

WindowsURL/Windowssettingup/Windows

wxPythonURL/WindowsOpenCVimages,converting/ConvertingOpenCVimagesforwxPython

wxPythonPhoenixURL/Windows,Mac

XXcodeCommandLineTools/Mac