+ All Categories
Home > Documents > Developing Games With Ruby: For those who write code for living

Developing Games With Ruby: For those who write code for living

Date post: 11-Sep-2021
Category:
Upload: others
View: 1 times
Download: 0 times
Share this document with a friend
188
Transcript
Page 1: Developing Games With Ruby: For those who write code for living
Page 2: Developing Games With Ruby: For those who write code for living

DevelopingGamesWithRuby

Forthosewhowritecodeforliving

TomasVaraneckas

Thisbookisforsaleathttp://leanpub.com/developing-games-with-ruby

Thisversionwaspublishedon2014-12-16

*****

ThisisaLeanpubbook.LeanpubempowersauthorsandpublisherswiththeLeanPublishingprocess.LeanPublishingistheactofpublishinganin-progressebookusinglightweighttoolsandmanyiterationstogetreaderfeedback,pivotuntilyouhavetherightbookandbuildtractiononceyoudo.

*****

©2014TomasVaraneckas

Page 3: Developing Games With Ruby: For those who write code for living

TableofContents

ABoyWhoWantedToCreateWorlds

WhyRuby?

WhatYouShouldKnowBeforeReadingThisBook

WhatAreWeGoingToBuild?GraphicsGameDevelopmentLibraryThemeAndMechanics

PreparingTheToolsGettingGosutorunonMacOsX

GettingTheSampleCode

OtherTools

GosuBasicsHelloWorldScreenCoordinatesAndDepthMainLoopMovingThingsWithKeyboardImagesAndAnimationMusicAndSound

WarmingUpUsingTilesetsIntegratingWithTexturePackerCombiningTilesIntoAMapUsingTiledToCreateMapsLoadingTiledMapsWithGosuGeneratingRandomMapWithPerlinNoisePlayerMovementWithKeyboardAndMouseGameCoordinateSystem

PrototypingTheGameSwitchingBetweenGameStatesImplementingMenuStateImplementingPlayStateImplementingWorldMapImplementingFloatingCameraImplementingTheTankImplementingBulletsAndExplosions

Page 4: Developing Games With Ruby: For those who write code for living

RunningThePrototype

OptimizingGamePerformanceProfilingRubyCodeToFindBottlenecksAdvancedProfilingTechniquesOptimizingInefficientCodeProfilingOnDemandAdjustingGameSpeedForVariablePerformanceFrameSkipping

RefactoringThePrototypeGameProgrammingPatternsWhatIsWrongWithCurrentDesignDecouplingUsingComponentPattern

SimulatingPhysicsAddingEnemyObjectsAddingBoundingBoxesAndDetectingCollisionsCatchingBulletsImplementingTurnSpeedPenaltiesImplementingTerrainSpeedPenalties

ImplementingHealthAndDamageAddingHealthComponentInflictingDamageWithBullets

CreatingArtificialIntelligenceDesigningAIUsingFiniteStateMachineImplementingAIVisionControllingTankGunImplementingAIInputImplementingTankMotionStatesWiringTankMotionStatesIntoFiniteStateMachine

MakingThePrototypePlayableDrawingWaterBeyondMapBoundariesGeneratingTreeClustersGeneratingRandomObjectsImplementingARadarDynamicSoundVolumeAndPanningGivingEnemiesIdentityRespawningTanksAndRemovingDeadOnesDisplayingExplosionDamageTrailsDebuggingBulletPhysicsMakingCameraLookAheadReviewingTheChanges

Page 5: Developing Games With Ruby: For those who write code for living

DealingWithThousandsOfGameObjectsSpatialPartitioningImplementingAQuadtreeIntegratingObjectPoolWithQuadTreeMovingObjectsInQuadTree

ImplementingPowerupsImplementingBasePowerupImplementingPowerupGraphicsImplementingPowerupSoundsImplementingRepairDamagePowerupImplementingHealthBoostImplementingFireRateBoostImplementingTankSpeedBoostSpawningPowerupsOnMapRespawningPowerupsAfterPickup

ImplementingHeadsUpDisplayDesignConsiderationsRenderingTextWithCustomFontImplementingHUDClass

ImplementingGameStatisticsTrackingKills,DeathsandDamageMakingDamagePersonalTrackingDamageFromChainReactionsDisplayingGameScore

BuildingAdvancedAIImprovingTankNavigationImplementingDemoStateToObserveAIVisualAIDebuggingMakingAICollectPowerupsSeekingHealthPowerupsAfterHeavyDamageEvadingCollisionsAndGettingUnstuck

WrappingItUpLessonsLearned

SpecialThanks

Page 6: Developing Games With Ruby: For those who write code for living

ABoyWhoWantedToCreateWorlds

Oncetherewasaboywhofellinlovewiththismagicaldevicethatcouldbringthingstolifeinsideaglaringscreen.Hespentendlesshoursexploringimaginaryworlds,fightingstrangecreatures,shootingpixelatedspaceships,racingboxycars.Theboykeptpondering.“Howisthismade?Iwanttocreatemyownworlds…”.

Thenhediscoveredprogramming.“Icanfinallydoit!”-hethought.Andhetried.Andfailed.Thenhetriedharder.Hefailedagainandagain.Hewastoonaivetorealizethatthoseworldshewastryingtocreateweretoosophisticated,andhisknowledgewastoolimited.Hegaveupcreatingthoseworlds.

Whathedidn’tgiveupiswritingcodeforthismagicaldevice.Herealizedheisn’tsmartenoughtocreateworlds,yethefoundouthecouldcreatesimplerthingslikesmallapplications-web,desktop,serversideorwhatnot.Fewyearslaterhefoundhimselfgettingpaidtomakethose.

Applicationsgotincreasinglybigger,theyspannedacrossmultipleservers,integratedwitheachother,becamepatsofhugeinfrastructures.Theboy,nowagrownman,wasallintoit.Itwasfunandchallengingenoughtospendover10000hourslearningandbuildingwhatotherswantedhimtobuild.

Someofthesethingswereuseful,somewhereboringandpointless.Somewereneverfinished.Therewerethingshewasproudof,therewereothersthathewouldn’twanttotalkabout,nonethelesseverythinghebuiltmadehimabetterbuilder.Yetheneverfoundthetime,courageorreasontobuildwhathereallywantedtobuildsincehewasalittleboy-hisownworlds.

Untilonedayherealizedthatnoonecanstophimfromfollowinghisdream.Hefeltthatequippedwithhiscurrentknowledgeandexperiencehewillbeabletolearntocreateworldsofhisown.Andhewentforit.

Thisboymustliveinmanysoftwaredevelopers,whodreamaboutcreatinggames,butinsteadselltheirsoftwarecraftsmanshipskillstothosewhoneedsomethingelse.Thisboyisme,andyou.Andit’stimetosethimfree.

Welcometotheworldofgamedevelopmentthatwaswaitingforyoualltheseyears.

Page 7: Developing Games With Ruby: For those who write code for living

WhyRuby?

Whenitcomestogamedevelopment,everyonewilltellyouthatyoushouldgowithC++orsomeotherstaticallytypedlanguagethatcompilesdowntobaremetalinstructions.OrthatyoushouldgowithfullblowngamedevelopmentplatformlikeUnity.Slow,dynamiclanguageslikeRubyseemlikethelastchoiceanysanegamedeveloperwouldgofor.

Afriendofminesaid“There’slittlereasontodevelopadesktopgamewithRuby”,andhewasabsolutelyright.Perhapsthisisthereasonwhytherearenobooksaboutit.Allthecasualgameactionhappensinmobiledevices,anddesktopgamesareforseasonedgamerswhodemandfastanddetailed3Dgraphics,motion-capturedanimationsandsophisticatedgamemechanics-thingsweknowwearenotgoingtobeabletobuildonourown,withoutmillionsfromVCpocketsandHollywoodgradeequipment.

Now,bearwithme.Yourgamewillnotbea3DMMORPGsetinhuge,photorealisticrepresentationofMiddle-earth.Let’sleavethosethingstoBethesda,UbisoftandRockstarGames.Afterall,everyonehastostartsomewhere,andyouhavetobesmartenoughtounderstand,thateventhoughthatlittleboyinyouwantstocreateanimprovedversionofGrandTheftAutoV,wewillhavetogoforsomethingthatresembleslesserknownSuperNintendotitlesinstead.

Whynotgomobilethen?Thosedevicesseemperfectforsimplergames.Ifyouareatruegameratheart,youwillagreethattouchscreengamesyoufindinmodernphonesandtabletsareonlygoodforkilling10minutesofyourtimewhiletakingadump.Youhavetofeeltheresistancewhenyouclickabutton!Screensizealsodoesmatter.Playinganythingonmobilephoneisatortureforthosewhoknowwhatplayingrealgamesshouldfeellike.

So,yourgamewillhavetobesmallenoughforyoutobeabletocompleteit,itwillhavetohavesimple2Dgraphics,andwouldnotrequirethelatestGeForcewithatleast512MBofRAM.Thisfactgivesyouthebenefitofchoice.Youdon’thavetoworryaboutperformancethatmuch.Youcanchooseafriendlyandproductivelanguagethatisdesignedforprogrammerhappiness.AndthisiswhereRubystartstoshine.It’sbeautiful,simpleandelegant.Itisclosetopoetry.

Page 8: Developing Games With Ruby: For those who write code for living

WhatYouShouldKnowBeforeReadingThisBook

Asyoucanreadonthecover,thisbookis“forthosewhowritecodeforliving”.It’snotarequirement,andyouwillmostlikelybeabletounderstandeverythingevenifyouareastudentorhobbyist,butthisbookwillnotteachyouhowtobeagoodprogrammer.Ifyouwanttolearnthat,startwithtimelessclassic:ThePragmaticProgrammer:FromJourneymantoMaster.

YoushouldunderstandRubyatleasttosomeextent.Thereareplentyofbooksandresourcescoveringthatsubject.TryWhy’sPoignantGuideToRubyorEloquentRuby.Youcanalsolearnitwhilereadingthisbook.Itshouldn’tbetoohard,especiallyifyoualreadywritecodeforliving.Afterallprogramminglanguageismerelyatool,andwhenyoulearnone,othersarerelativelyeasytoswitchto.

Youshouldknowhowtousethecommandline.BasicknowledgeofGitcanalsobehandy.

Youdon’thavetoknowhowtodraworcomposemusic.Wewillusemediathatisavailableforfree.However,knowledgeofgraphicsandaudioeditingsoftwarewon’thurt.

Page 9: Developing Games With Ruby: For those who write code for living

WhatAreWeGoingToBuild?

Thisquestionisofparamountimportance.Theanswerwillusuallydetermineifyouwilllikelytosucceed.Ifyouwanttooverstepyourboundaries,youwillfail.Itshouldn’tbetooeasyeither.Ifyouknowsomethingaboutprogrammingalready,IbetyoucanimplementTicTacToe,butwillyoufeelproudaboutit?Willyoubeabletosay“I’vebuiltaworld!”.Iwouldn’t.

GraphicsTobeginwith,weneedtoknowwhatkindofgraphicsweareaimingfor.Wewillinstantlyruleout3Dforseveralreasons:

Wedon’twanttoincreasethescopeandcomplexityRubymaynotbefastenoughfor3DgamesLearningproper3Dgraphicsprogrammingrequiresreadingaseparatebookthatisseveraltimesthickerthanthisone.

Now,wehavetoswallowourprideandacceptthefactthatthegamewillhavesimple2Dgraphics.Therearethreechoicestogofor:

ParallelProjectionTopDownSide-Scrolling

ParallelProjection(thinkFallout1&2)isprettycloseto3Dgraphics,itrequiresdetailedartifyouwantittolookdecent,sowewouldhavearoughstartifwewentforit.

TopDownview(oldtitlesofLegendofZelda)offersplentyoffreedomtoexploretheenvironmentinalldirectionsandrequireslessgraphicaldetail,sincethingslooksimplerfromabove.

SideScrollinggames(SuperMarioBros.)usuallyinvolvesomephysicsrelatedtojumpingandrequiremoreefforttolookgood.Feelingofexplorationislimited,sinceyouusuallymovefromlefttorightmostofthetime.

GoingwithTopDownviewwillgiveusachancetocreateourgameworldasopenforexplorationaspossible,whilehavingsimplegraphicsandmovementmechanics.Soundslikethebestchoiceforus.

IfyouareasbadatdrawingthingsasIam,youcouldstillwonderhowwearegoingtogetourgraphics.Thankfully,thereisthisopengameart.org.It’slikeGitHubofgamemedia,wewillsurelyfindsomethingthere.Italsocontainsaudiosamplesandtracks.

GameDevelopmentLibrary

Page 10: Developing Games With Ruby: For those who write code for living

Implementitallyourselforharnessthepowerofsomegamedevelopmentlibrarythatoffersyouboilerplatesandconvenientaccesstocommonfunctions?Ifyou’relikeme,youwoulddefinitelywanttoimplementitallyourself,butthatmaybethereasonwhyIfailedtomakeadecentgamesomanytimes.

Ifyouwilltrytoimplementitallyourself,youwillmostlikelyendupreimplementingsomeexistinggamelibrary,poorly.Itwon’ttakelongwhileyoureachapointwhereyouneedtointerfacewithunderlyingoperatingsystemlibrariestogetgraphics.Andguessifthosebindingswillworkinadifferentoperatingsystem?

So,swallowyourprideagain,becausewearegoingtouseanexistinggamedevelopmentlibrary.Goodnewsisthatyouwillbeabletoactuallyfinishthegame,anditwillbeportabletoWindows,MacandLinux.Wewillstillhavetobuildourowngameengineforourselvesontopofit,sodon’tthinkitwon’tbefun.

ThereareseveralgamelibrariesavailableforRuby,butit’sasimplechoice,becauseGosuisheadandshouldersaboveothers.It’sverymature,hasalargeandactivecommunity,anditismainlywritteninC++buthasfirstclassRubysupport,soitwillbebothfastandconvenienttouse.

ManyofotherRubygamelibrariesarebuiltontopofGosu,soit’sasolidchoice.

ThemeAndMechanicsChoosingtherightthemeisundoubtedlyimportant.Itshouldbesomethingthatappealstoyou,somethingyouwillwanttoplay,anditshouldnotimplydifficultgamemechanics.IloveMMORPGs,andIalwaysdreamedofmakinganopenworldgamewhereyoucanroamaround,meetotherplayers,fightmonstersandlevelup.GuesshowmanytimesIstartedbuildingsuchagame?EvenifIwouldn’thavelostthecount,Iwouldn’tbeproudtosaythenumber.

Thistime,equippedwithlogicandsanity,I’vepickedsomethingchallengingenough,yetstillprettysimpletobuild.Areyouready?

Drumroll…

Wewillbebuildingamultidirectionalshooterarcadegamewhereyoucontrolatank,roamaroundanisland,shootenemytanksandtrynottogetdestroyedbyothers.

IfyouhaveplayedBattleCityorTankForce,youshouldeasilygettheidea.Ibelievethatimplementingsuchagame(withseveraltwists)wouldexposeustoperfectlevelofdifficultyandprovidesubstantialamountofexperience.

Wewilluseasubsetofthesegorgeousgraphicswhichareavailableonopengameart.org,generouslyprovidedbyCsabaFelvegi.

Page 11: Developing Games With Ruby: For those who write code for living

PreparingTheTools

Whilewritingthisbook,IwillbeusingMacOSX(10.9),butitshouldbepossibletorunalltheexamplesonotheroperatingsystemstoo.

GosuWikihas“GettingStarted”pagesforMac,LinuxandWindows,soIwillnotbegoingintomuchdetailhere.

GettingGosutorunonMacOsXIfyouhaven’tsetupyourMacfordevelopment,firstinstallXcodeusingAppStore.SystemRubyshouldworkjustfine,butyoumaywanttouseRbenvorRVMtoavoidpollutingsystemRuby.I’vehadtroubleinstallingGosuwithRVM,butyourexperiencemayvary.

Toinstallthegem,simplyrun:$geminstallgosu

YoumayneedtoprefixitwithsudoifyouareusingsystemRuby.

Totestifgemwasinstalledcorrectly,youshouldbeabletorunthistoproduceanemptyblackwindow:$irb

irb(main):001:0>require'gosu'

=>true

irb(main):002:0>Gosu::Window.new(320,240,false).show

=>nil

MostdeveloperswhouseMaceverydaywillalsorecommendinstallingHomebrewpackagemanager,replaceTerminalappwithiTerm2anduseOh-My-ZshtomanageZSHconfiguration.

Page 12: Developing Games With Ruby: For those who write code for living

GettingTheSampleCode

YoucanfindsamplecodeatGitHub:https://github.com/spajus/ruby-gamedev-book-examples.

Cloneittoaconvenientlocation:$cd~/gamedev

[email protected]:spajus/ruby-gamedev-book-examples.git

Thesourcecodeoffinalproductcanbefoundathttps://github.com/spajus/tank_island

Page 13: Developing Games With Ruby: For those who write code for living

OtherTools

Allyouneedforthisadventureisagoodtexteditor,terminalandprobablysomegraphicseditor.TryGIMPifyouwantafreeone.I’musingPixelmator,it’swonderful,butforMaconly.AnoteworthyfactisthatPixelmatorwasbuiltbyfellowLithuanians.

Whenitcomestoeditors,Idon’tleavehomewithoutVim,butaslongaswhatyouusemakesyouproductive,itdoesn’tmakeanydifference.Vim,EmacsorSublimeareallgoodenoughtowritecode,justhavesomegoodpluginsthatsupportRuby,andyou’reset.IfyoureallyfeelyouneedanIDE,whichmaybethecaseifyouarecomingfromastaticlanguage,youcan’tgowrongwithRubyMine.

Page 14: Developing Games With Ruby: For those who write code for living

GosuBasics

BynowGosushouldbeinstalledandreadyforaspin.Butbeforewerushintobuildingourgame,wehavetogetacquaintedwithourlibrary.Wewillgothroughseveralsimpleexamples,familiarizeourselveswithGosuarchitectureandcoreprinciples,andtakeacoupleofbabystepstowardsunderstandinghowtoputeverythingtogether.

Tomakethischaptereasiertoreadandunderstand,IrecommendwatchingWritingGamesWithRubytalkgivenbyMikeMooreatLARubyConference2014.Infact,thistalkpushedmetowardsrethinkingthiscrazyideaofusingRubyforgamedevelopment,sothisbookwouldn’texistwithoutit.Thankyou,Mike.

HelloWorldTohonorthetraditions,wewillstartbywriting“HelloWorld”togetatasteofwhatGosufeelslike.ItisbasedonRubyTutorialthatyoucanfindinGosuWiki.01-hello/hello_world.rb

1require'gosu'

2

3classGameWindow<Gosu::Window

4definitialize(width=320,height=240,fullscreen=false)

5super

6self.caption='Hello'

7@message=Gosu::Image.from_text(

8self,'Hello,World!',Gosu.default_font_name,30)

9end

10

11defdraw

[email protected](10,10,0)

13end

14end

15

16window=GameWindow.new

17window.show

Runthecode:$ruby01-hello/hello_world.rb

Youshouldseeaneatsmallwindowwithyourmessage:

Page 15: Developing Games With Ruby: For those who write code for living

HelloWorld

Seehoweasythatwas?Nowlet’strytounderstandwhatjusthappenedhere.

WehaveextendedGosu::WindowwithourownGameWindowclass,initializingitas320x240window.superpassedwidth,heightandfullscreeninitializationparametersfromGameWindowtoGosu::Window.

Thenwedefinedourwindow’scaption,andcreated@messageinstancevariablewithanimagegeneratedfromtext"Hello,World!"usingGosu::Image.from_text.

WehaveoverriddenGosu::Window#drawinstancemethodthatgetscalledeverytimeGosuwantstoredrawourgamewindow.Inthatmethodwecalldrawonour@messagevariable,providingxandyscreencoordinatesbothequalto10,andz(depth)valueequalto0.

ScreenCoordinatesAndDepthJustlikemostconventionalcomputergraphicslibraries,Gosutreatsxashorizontalaxis(lefttoright),yasverticalaxis(toptobottom),andzasorder.

Page 16: Developing Games With Ruby: For those who write code for living

Screencoordinatesanddepth

xandyaremeasuredinpixels,andvalueofzisarelativenumberthatdoesn’tmeananythingonit’sown.Thepixelintop-leftcornerofthescreenhascoordinatesof0:0.

zorderinGosuisjustlikez-indexinCSS.Itdoesnotdefinezoomlevel,butincasetwoshapesoverlap,onewithhigherzvaluewillbedrawnontop.

MainLoopTheheartofGosulibraryisthemainloopthathappensinGosu::Window.ItisexplainedfairlywellinGosuwiki,sowewillnotbediscussingithere.

MovingThingsWithKeyboardWewillmodifyour“Hello,World!”exampletolearnhowtomovethingsonscreen.Thefollowingcodewillprintcoordinatesofthemessagealongwithnumberoftimesscreenwasredrawn.ItalsoallowsexitingtheprogrambyhittingEscbutton.01-hello/hello_movement.rb

1require'gosu'

2

3classGameWindow<Gosu::Window

4definitialize(width=320,height=240,fullscreen=false)

5super

6self.caption='HelloMovement'

7@x=@y=10

8@draws=0

9@buttons_down=0

10end

11

12defupdate

13@x-=1ifbutton_down?(Gosu::KbLeft)

14@x+=1ifbutton_down?(Gosu::KbRight)

Page 17: Developing Games With Ruby: For those who write code for living

15@y-=1ifbutton_down?(Gosu::KbUp)

16@y+=1ifbutton_down?(Gosu::KbDown)

17end

18

19defbutton_down(id)

20closeifid==Gosu::KbEscape

21@buttons_down+=1

22end

23

24defbutton_up(id)

25@buttons_down-=1

26end

27

28defneeds_redraw?

29@draws==0||@buttons_down>0

30end

31

32defdraw

33@draws+=1

34@message=Gosu::Image.from_text(

35self,info,Gosu.default_font_name,30)

[email protected](@x,@y,0)

37end

38

39private

40

41definfo

42"[x:#{@x};y:#{@y};draws:#{@draws}]"

43end

44end

45

46window=GameWindow.new

47window.show

Runtheprogramandtrypressingarrowkeys:$ruby01-hello/hello_movement.rb

Themessagewillmovearoundaslongasyoukeeparrowkeyspressed.

Page 18: Developing Games With Ruby: For those who write code for living

Usearrowkeystomovethemessagearound

Wecouldwriteashorterversion,butthepointhereisthatifwewouldn’toverrideneeds_redraw?thisprogramwouldbeslowerbyorderofmagnitude,becauseitwouldcreate@messageobjecteverytimeitwantstoredrawthewindow,eventhoughnothingwouldchange.

Hereisascreenshotoftopdisplayingtwoversionsofthisprogram.Secondscreenhasneeds_redraw?methodremoved.Seethedifference?

Page 19: Developing Games With Ruby: For those who write code for living

RedrawingonlywhennecessaryVSredrawingeverytime

Rubyisslow,soyouhavetouseitwisely.

ImagesAndAnimationIt’stimetomakesomethingmoreexciting.Ourgamewillhavetohaveexplosions,thereforeweneedtolearntoanimatethem.Wewillsetupabackgroundsceneandtriggerexplosionsontopofitwithourmouse.01-hello/hello_animation.rb

1require'gosu'

2

3defmedia_path(file)

4File.join(File.dirname(File.dirname(

5__FILE__)),'media',file)

6end

7

8classExplosion

9FRAME_DELAY=10#ms

10SPRITE=media_path('explosion.png')

11

12defself.load_animation(window)

13Gosu::Image.load_tiles(

14window,SPRITE,128,128,false)

15end

16

17definitialize(animation,x,y)

18@animation=animation

19@x,@y=x,y

20@current_frame=0

21end

22

23defupdate

24@current_frame+=1ifframe_expired?

25end

26

27defdraw

28returnifdone?

29image=current_frame

30image.draw(

[email protected]/2.0,

[email protected]/2.0,

330)

34end

35

36defdone?

37@done||=@[email protected]

38end

Page 20: Developing Games With Ruby: For those who write code for living

39

40private

41

42defcurrent_frame

43@animation[@current_frame%@animation.size]

44end

45

46defframe_expired?

47now=Gosu.milliseconds

48@last_frame||=now

49if(now-@last_frame)>FRAME_DELAY

50@last_frame=now

51end

52end

53end

54

55classGameWindow<Gosu::Window

56BACKGROUND=media_path('country_field.png')

57

58definitialize(width=800,height=600,fullscreen=false)

59super

60self.caption='HelloAnimation'

61@background=Gosu::Image.new(

62self,BACKGROUND,false)

63@animation=Explosion.load_animation(self)

64@explosions=[]

65end

66

67defupdate

[email protected]!(&:done?)

[email protected](&:update)

70end

71

72defbutton_down(id)

73closeifid==Gosu::KbEscape

74ifid==Gosu::MsLeft

[email protected](

76Explosion.new(

77@animation,mouse_x,mouse_y))

78end

79end

80

81defneeds_cursor?

82true

83end

84

85defneeds_redraw?

86!@scene_ready||@explosions.any?

87end

88

89defdraw

90@scene_ready||=true

[email protected](0,0,0)

[email protected](&:draw)

93end

94end

95

96window=GameWindow.new

97window.show

Runitandclickaroundtoenjoythosebeautifulspecialeffects:$ruby01-hello/hello_animation.rb

Page 21: Developing Games With Ruby: For those who write code for living

Multipleexplosionsonscreen

Nowlet’sfigureouthowitworks.OurGameWindowinitializeswith@backgroundGosu::Imageand@animation,thatholdsarrayofGosu::Imageinstances,oneforeachframeofexplosion.Gosu::Image.load_tileshandlesitforus.

Explosion::SPRITEpointsto“tileset”image,whichisjustaregularimagethatcontainsequallysizedsmallerimageframesarrangedinorderedsequence.Rowsofframesarereadlefttoright,likeyouwouldreadabook.

Page 22: Developing Games With Ruby: For those who write code for living

Explosiontileset

Giventhatexplosion.pngtilesetis1024x1024pixelsbig,andithas8rowsof8tilesperrow,itiseasytotellthatthereare64tiles128x128pixelseach.So,@animation[0]holds128x128Gosu::Imagewithtop-lefttile,and@animation[63]-thebottom-rightone.

Gosudoesn’thandleanimation,it’ssomethingyouhavefullcontrolover.Wehavetodraweachtileinasequenceourselves.YoucanalsousetilestoholdmapgraphicsThelogicbehindthisisprettysimple:

1. Explosionknowsit’s@current_framenumber.Itbeginswith0.2. Explosion#frame_expired?checksthelasttimewhen@current_framewas

rendered,andwhenitisolderthanExplosion::FRAME_DELAYmilliseconds,@current_frameisincreased.

3. WhenGameWindow#updateiscalled,@[email protected],explosionsthathavefinishedtheiranimation(displayedthelast

Page 23: Developing Games With Ruby: For those who write code for living

frame)[email protected]. GameWindow#drawdrawsbackgroundimageandall@explosionsdrawtheir

current_frame.5. Again,wearesavingresourcesandnotredrawingwhenthereareno@explosionsin

progress.needs_redraw?handlesit.

Itisimportanttounderstandthatupdateanddraworderisunpredictable,thesemethodscanbecalledbyyoursystematdifferentrate,youcan’ttellwhichonewillbecalledmoreoftenthantheotherone,soupdateshouldonlybeconcernedwithadvancingobjectstate,anddrawshouldonlydrawcurrentstateonscreenifitisneeded.Theonlyreliablethinghereistime,consultGosu.millisecondstoknowhowmuchtimehavepassed.

Ruleofthethumb:drawshouldbeaslightweightaspossible.Prepareallcalculationsinupdateandyouwillhaveresponsive,smoothgraphics.

MusicAndSoundOurpreviousprogramwasclearlymissingasoundtrack,sowewilladdone.Abackgroundmusicwillbelooping,andeachexplosionwillbecomeaudible.01-hello/hello_sound.rb

1require'gosu'

2

3defmedia_path(file)

4File.join(File.dirname(File.dirname(

5__FILE__)),'media',file)

6end

7

8classExplosion

9FRAME_DELAY=10#ms

10SPRITE=media_path('explosion.png')

11

12defself.load_animation(window)

13Gosu::Image.load_tiles(

14window,SPRITE,128,128,false)

15end

16

17defself.load_sound(window)

18Gosu::Sample.new(

19window,media_path('explosion.mp3'))

20end

21

22definitialize(animation,sound,x,y)

23@animation=animation

24sound.play

25@x,@y=x,y

26@current_frame=0

27end

28

29defupdate

30@current_frame+=1ifframe_expired?

31end

32

33defdraw

34returnifdone?

35image=current_frame

36image.draw(

[email protected]/2.0,

[email protected]/2.0,

390)

40end

41

42defdone?

43@done||=@[email protected]

Page 24: Developing Games With Ruby: For those who write code for living

44end

45

46defsound

[email protected]

48end

49

50private

51

52defcurrent_frame

53@animation[@current_frame%@animation.size]

54end

55

56defframe_expired?

57now=Gosu.milliseconds

58@last_frame||=now

59if(now-@last_frame)>FRAME_DELAY

60@last_frame=now

61end

62end

63end

64

65classGameWindow<Gosu::Window

66BACKGROUND=media_path('country_field.png')

67

68definitialize(width=800,height=600,fullscreen=false)

69super

70self.caption='HelloAnimation'

71@background=Gosu::Image.new(

72self,BACKGROUND,false)

73@music=Gosu::Song.new(

74self,media_path('menu_music.mp3'))

[email protected]=0.5

[email protected](true)

77@animation=Explosion.load_animation(self)

78@sound=Explosion.load_sound(self)

79@explosions=[]

80end

81

82defupdate

[email protected]!(&:done?)

[email protected](&:update)

85end

86

87defbutton_down(id)

88closeifid==Gosu::KbEscape

89ifid==Gosu::MsLeft

[email protected](

91Explosion.new(

92@animation,@sound,mouse_x,mouse_y))

93end

94end

95

96defneeds_cursor?

97true

98end

99

100defneeds_redraw?

101!@scene_ready||@explosions.any?

102end

103

104defdraw

105@scene_ready||=true

[email protected](0,0,0)

[email protected](&:draw)

108end

109end

110

111window=GameWindow.new

112window.show

Runitandenjoythecinematicexperience.Addingsoundreallymakesadifference.$ruby01-hello/hello_sound.rb

Weonlyaddedcoupleofthingsoverpreviousexample.

Page 25: Developing Games With Ruby: For those who write code for living

72@music=Gosu::Song.new(

73self,media_path('menu_music.mp3'))

[email protected]=0.5

[email protected](true)

GameWindowcreatesGosu::Songwithmenu_music.mp3,adjuststhevolumesoit’salittlemorequietandstartsplayinginaloop.16defself.load_sound(window)

17Gosu::Sample.new(

18window,media_path('explosion.mp3'))

19end

Explosionhasnowgotload_soundmethodthatloadsexplosion.mp3soundeffectGosu::Sample.ThissoundeffectisloadedonceinGameWindowconstructor,andpassedintoeverynewExplosion,whereitsimplystartsplaying.

HandlingaudiowithGosuisverystraightforward.UseGosu::Songtoplaybackgroundmusic,andGosu::Sampletoplayeffectsandsoundsthatcanoverlap.

Page 26: Developing Games With Ruby: For those who write code for living

WarmingUp

Beforewestartbuildingourgame,wewanttoflexourskillslittlemore,gettoknowGosubetterandmakesureourtoolswillbeabletomeetourexpectations.

UsingTilesetsAfterplayingaroundwithGosuforawhile,weshouldbecomfortableenoughtoimplementaprototypeoftop-downviewgamemapusingthetilesetofourchoice.Thisgroundtilesetlookslikeagoodplacetostart.

IntegratingWithTexturePackerAfterdownloadingandextractingthetileset,it’sobviousthatGosu::Image#load_tileswillnotsuffice,sinceitonlysupportstilesofsamesize,andthereisatilesetinthepackagethatlookslikethis:

Page 27: Developing Games With Ruby: For those who write code for living

Tilesetwithtilesofirregularsize

Page 28: Developing Games With Ruby: For those who write code for living

AndthereisalsoaJSONfilethatcontainssomemetadata:{"frames":{

"aircraft_1d_destroyed.png":

{

"frame":{"x":451,"y":102,"w":57,"h":42},

"rotated":false,

"trimmed":false,

"spriteSourceSize":{"x":0,"y":0,"w":57,"h":42},

"sourceSize":{"w":57,"h":42}

},

"aircraft_2d_destroyed.png":

{

"frame":{"x":2,"y":680,"w":63,"h":47},

"rotated":false,

"trimmed":false,

"spriteSourceSize":{"x":0,"y":0,"w":63,"h":47},

"sourceSize":{"w":63,"h":47}

},

...

}},

"meta":{

"app":"http://www.texturepacker.com",

"version":"1.0",

"image":"decor.png",

"format":"RGBA8888",

"size":{"w":512,"h":1024},

"scale":"1",

"smartupdate":"$TexturePacker:SmartUpdate:2e6b6964f24c7abfaa85a804e2dc1b05$"

}

LookslikethesetileswerepackedwithTexturePacker.AftersomediggingI’vediscoveredthatGosudoesn’thaveanyintegrationwithit,soIhadthesechoices:

1. Cuttheoriginaltilesetimageintosmallerimages.2. ParseJSONandharnessthebenefitsofTexturePacker.

Firstoptionwastoomuchworkandwouldprovetobelessefficient,becauseloadingmanysmallfilesisalwaysworsethanloadingonebiggerfile.Therefore,secondoptionwasthewinner,andIalsothought“whynotwriteagemwhileI’matit”.Andthat’sexactlywhatIdid,andyoushoulddothesameinsuchasituation.ThegemisavailableonGitHub:

https://github.com/spajus/gosu-texture-packer

Youcaninstallthisgemusinggeminstallgosu_texture_packer.Ifyouwanttoexaminethecode,easiestwayistocloneitonyourcomputer:[email protected]:spajus/gosu-texture-packer.git

Let’sexaminethemainideabehindthisgem.Hereisaslightlysimplifiedversionthatdoeshandleseverythinginunder20linesofcode:02-warmup/tileset.rb

1require'json'

2classTileset

3definitialize(window,json)

4@json=JSON.parse(File.read(json))

5image_file=File.join(

6File.dirname(json),@json['meta']['image'])

7@main_image=Gosu::Image.new(

8@window,image_file,true)

9end

10

11defframe(name)

12f=@json['frames'][name]['frame']

Page 29: Developing Games With Ruby: For those who write code for living

13@main_image.subimage(

14f['x'],f['y'],f['w'],f['h'])

15end

16end

IfbynowyouarefamiliarwithGosudocumentation,youwillwonderwhatthehellisGosu::Image#subimage.Atthepointofwritingitwasnotdocumented,andIaccidentallydiscovereditwhilediggingthroughGosusourcecode.

I’mluckythisfunctionexisted,becauseIwasreadytobringouttheheavyartilleryanduseRMagicktoextractthosetiles.WewillprobablyneedRMagickatsomepointoftimelater,butit’sbettertoavoiddependenciesaslongaspossible.

CombiningTilesIntoAMapWithtilesetloadingissueoutoftheway,wecanfinallygetbacktodrawingthatcoolmapofours.

Thefollowingprogramwillfillthescreenwithrandomtiles.02-warmup/random_map.rb

1require'gosu'

2require'gosu_texture_packer'

3

4defmedia_path(file)

5File.join(File.dirname(File.dirname(

6__FILE__)),'media',file)

7end

8

9classGameWindow<Gosu::Window

10WIDTH=800

11HEIGHT=600

12TILE_SIZE=128

13

14definitialize

15super(WIDTH,HEIGHT,false)

16self.caption='RandomMap'

17@tileset=Gosu::TexturePacker.load_json(

18self,media_path('ground.json'),:precise)

19@redraw=true

20end

21

22defbutton_down(id)

23closeifid==Gosu::KbEscape

24@redraw=trueifid==Gosu::KbSpace

25end

26

27defneeds_redraw?

28@redraw

29end

30

31defdraw

32@redraw=false

33(0..WIDTH/TILE_SIZE).eachdo|x|

34(0..HEIGHT/TILE_SIZE).eachdo|y|

[email protected](

[email protected]_list.sample).draw(

37x*(TILE_SIZE),

38y*(TILE_SIZE),

390)

40end

41end

42end

43end

44

45window=GameWindow.new

46window.show

Page 30: Developing Games With Ruby: For those who write code for living

Runit,thenpressspacebartorefillthescreenwithrandomtiles.$ruby02-warmup/random_map.rb

Page 31: Developing Games With Ruby: For those who write code for living

Mapfilledwithrandomtiles

Theresultdoesn’tlookseamless,sowewillhavetofigureoutwhat’swrong.Afterplayingaroundforawhile,I’venoticedthatit’sanissuewithGosu::Image.

Whenyouloadatilelikethis,itworksperfectly:Gosu::Image.new(self,image_path,true,0,0,128,128)

Gosu::Image.load_tiles(self,image_path,128,128,true)

Andthefollowingproducessocalled“texturebleeding”:Gosu::Image.new(self,image_path,true)

Gosu::Image.new(self,image_path,true).subimage(0,0,128,128)

Goodthingwe’renotbuildingourgameyet,right?Welcometotheintricaciesofsoftwaredevelopment!

Now,Ihavereportedmyfindings,butuntilitgetsfixed,weneedaworkaround.AndtheworkaroundwastouseRMagick.Iknewwewon’tgettoofarawayfromit.Butourrandommapnowlooksgorgeous:

Page 32: Developing Games With Ruby: For those who write code for living

Mapfilledwithseamlessrandomtiles

UsingTiledToCreateMapsWhilelowlevelapproachtodrawingtilesinscreenmaybeappropriateinsomescenarios,likerandomlygeneratedmaps,wewillexploreanotheralternatives.Oneofthemisthisgreat,opensource,crossplatform,generictilemapeditorcalledTiled.

Ithassomelimitations,forinstance,alltilesintilesethavetobeofsameproportions.Ontheupside,itwouldbeeasytoloadTiledtilesetswithGosu::Image#load_tiles.

Page 33: Developing Games With Ruby: For those who write code for living

Tiled

Tiledusesit’sowncustom,XMLbasedtmxformatforsavingmaps.ItalsoallowsexportingmapstoJSON,whichiswaymoreconvenient,sinceparsingXMLinRubyisusuallydonewithNokogiri,whichisheavierandit’snativeextensionsusuallycausemoretroublethanonesJSONparseruses.So,let’sseehowthatJSONlookslike:02-warmup/tiled_map.json

1{"height":10,

2"layers":[

3{

4"data":[65,65,65,65,65,65,65,65,65,65,65,65,65,0,0,65,6\

55,65,65,65,65,65,65,0,0,65,65,65,65,65,65,65,65,0,0,0,65,65\

6,65,65,65,65,65,0,0,0,0,65,65,65,65,65,65,0,0,0,0,65,65,65\

7,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65\

8,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65\

9],

10"height":10,

11"name":"Water",

12"opacity":1,

13"type":"tilelayer",

14"visible":true,

15"width":10,

16"x":0,

17"y":0

18},

19{

20"data":[0,0,7,5,57,43,0,0,0,0,0,0,28,1,1,42,0,0,0,0,\

210,0,44,1,1,42,0,0,0,0,0,0,28,1,1,27,43,0,0,0,0,0,28,1,1\

22,1,27,43,0,0,0,0,28,1,1,1,59,16,0,0,0,0,48,62,61,61,16,0,\

230,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\

24,0,0,0,0,0],

25"height":10,

26"name":"Ground",

27"opacity":1,

28"type":"tilelayer",

Page 34: Developing Games With Ruby: For those who write code for living

29"visible":true,

30"width":10,

31"x":0,

32"y":0

33}],

34"orientation":"orthogonal",

35"properties":

36{

37

38},

39"tileheight":128,

40"tilesets":[

41{

42"firstgid":1,

43"image":"media\/ground.png",

44"imageheight":1024,

45"imagewidth":1024,

46"margin":0,

47"name":"ground",

48"properties":

49{

50

51},

52"spacing":0,

53"tileheight":128,

54"tilewidth":128

55},

56{

57"firstgid":65,

58"image":"media\/water.png",

59"imageheight":128,

60"imagewidth":128,

61"margin":0,

62"name":"water",

63"properties":

64{

65

66},

67"spacing":0,

68"tileheight":128,

69"tilewidth":128

70}],

71"tilewidth":128,

72"version":1,

73"width":10

74}

Therearefollowingthingslistedhere:

Twodifferenttilesets,“ground”and“water”Mapwidthandheightintilecount(10x10)Layerswithdataarraycontainstilenumbers

CoupleofextrathingsthatTiledmapscanhave:

ObjectlayerscontaininglistsofobjectswiththeircoordinatesPropertieshashontilesandobjects

Thisdoesn’tlooktoodifficulttoparse,sowe’regoingtoimplementaloaderforTiledmaps.Andmakeitopensource,ofcourse.

LoadingTiledMapsWithGosuProbablytheeasiestwaytoloadTiledmapistotakeeachlayerandrenderitonscreen,tilebytile,likeacake.Wewillnotcareaboutcachingatthispoint,andtheonly

Page 35: Developing Games With Ruby: For those who write code for living

optimizationwouldbenotdrawingthingsthatareoutofscreenboundaries.

Aftercoupleofdaysoftestdrivendevelopment,I’veendedupwritinggosu_tiledgem,thatallowsyoutoloadTiledmapswithjustafewlinesofcode.

Iwillnotgothroughdescribingtheimplementation,butifyouwanttoexaminethethoughtprocess,takealookatgosu_tiledgem’sgitcommithistory.

Tousethegem,dogeminstallgosu_tiledandexaminethecodethatshowsamapoftheislandthatyoucanscrollaroundwitharrowkeys:02-warmup/island.rb

1require'gosu'

2require'gosu_tiled'

3

4classGameWindow<Gosu::Window

5MAP_FILE=File.join(File.dirname(

6__FILE__),'island.json')

7SPEED=5

8

9definitialize

10super(640,480,false)

11@map=Gosu::Tiled.load_json(self,MAP_FILE)

12@x=@y=0

13@first_render=true

14end

15

16defbutton_down(id)

17closeifid==Gosu::KbEscape

18end

19

20defupdate

21@x-=SPEEDifbutton_down?(Gosu::KbLeft)

22@x+=SPEEDifbutton_down?(Gosu::KbRight)

23@y-=SPEEDifbutton_down?(Gosu::KbUp)

24@y+=SPEEDifbutton_down?(Gosu::KbDown)

25self.caption="#{Gosu.fps}FPS.Usearrowkeystopan"

26end

27

28defdraw

29@first_render=false

[email protected](@x,@y)

31end

32

33defneeds_redraw?

34[Gosu::KbLeft,

35Gosu::KbRight,

36Gosu::KbUp,

37Gosu::KbDown].eachdo|b|

38returntrueifbutton_down?(b)

39end

40@first_render

41end

42end

43

44GameWindow.new.show

Runit,usearrowkeystoscrollthemap.$ruby02-warmup/island.rb

Theresultisquitesatisfying,anditscrollssmoothlywithoutanyoptimizations:

Page 36: Developing Games With Ruby: For those who write code for living

ExploringTiledmapinGosu

GeneratingRandomMapWithPerlinNoiseInsomecasesrandomgeneratedmapsmakeallthedifference.WormsandDiablowouldprobablybejustaveragegamesifitwasn’tforthosealwaysunique,procedurallygeneratedmaps.

Wewilltrytomakeaveryprimitivemapgeneratorourselves.Tobeginwith,wewillbeusingonly3differenttiles-water,sandandgrass.Forimplementingfullytilededges,thegeneratormustbeawareofavailabletilesetsandknowhowtocombinetheminvalidways.Wemaycomebacktoit,butfornowlet’skeepthingssimple.

Now,generatingnaturallylookingrandomnessissomethingworthhavingabookofit’sown,soinsteadoftryingtopoorlyreinventwhatotherpeoplehavealreadydone,wewilluseawellknownalgorithmperfectlysuitedforthistask-Perlinnoise.

IfyouhaveeverusedPhotoshop’sCloudfilter,youalreadyknowhowPerlinnoiselookslike:

Page 37: Developing Games With Ruby: For those who write code for living

Perlinnoise

Now,wecouldimplementthealgorithmourselves,butthereisperlin_noisegemalreadyavailable,itlooksprettysolid,sowewilluseit.

Thefollowingprogramgenerates100x100mapwith30%chanceofwater,15%chanceofsandand55%chanceofgrass:02-warmup/perlin_noise_map.rb

1require'gosu'

2require'gosu_texture_packer'

3require'perlin_noise'

4

5defmedia_path(file)

6File.join(File.dirname(File.dirname(

7__FILE__)),'media',file)

8end

9

10classGameWindow<Gosu::Window

11MAP_WIDTH=100

12MAP_HEIGHT=100

13WIDTH=800

14HEIGHT=600

15TILE_SIZE=128

16

17definitialize

18super(WIDTH,HEIGHT,false)

19load_tiles

20@map=generate_map

21@zoom=0.2

22end

23

24defbutton_down(id)

25closeifid==Gosu::KbEscape

26@map=generate_mapifid==Gosu::KbSpace

27end

28

29defupdate

30adjust_zoom(0.005)ifbutton_down?(Gosu::KbDown)

31adjust_zoom(-0.005)ifbutton_down?(Gosu::KbUp)

32set_caption

33end

34

35defdraw

36tiles_x.timesdo|x|

37tiles_y.timesdo|y|

38@map[x][y].draw(

39x*TILE_SIZE*@zoom,

40y*TILE_SIZE*@zoom,

410,

Page 38: Developing Games With Ruby: For those who write code for living

42@zoom,

43@zoom)

44end

45end

46end

47

48private

49

50defset_caption

51self.caption='PerlinNoise.'<<

52"Zoom:#{'%.2f'%@zoom}."<<

53'UseUp/Downtozoom.Spacetoregenerate.'

54end

55

56defadjust_zoom(delta)

57new_zoom=@zoom+delta

58ifnew_zoom>0.07&&new_zoom<2

59@zoom=new_zoom

60end

61end

62

63defload_tiles

64tiles=Gosu::Image.load_tiles(

65self,media_path('ground.png'),128,128,true)

66@sand=tiles[0]

67@grass=tiles[8]

68@water=Gosu::Image.new(

69self,media_path('water.png'),true)

70end

71

72deftiles_x

73count=(WIDTH/(TILE_SIZE*@zoom)).ceil+1

74[count,MAP_WIDTH].min

75end

76

77deftiles_y

78count=(HEIGHT/(TILE_SIZE*@zoom)).ceil+1

79[count,MAP_HEIGHT].min

80end

81

82defgenerate_map

83noises=Perlin::Noise.new(2)

84contrast=Perlin::Curve.contrast(

85Perlin::Curve::CUBIC,2)

86map={}

87MAP_WIDTH.timesdo|x|

88map[x]={}

89MAP_HEIGHT.timesdo|y|

90n=noises[x*0.1,y*0.1]

91n=contrast.call(n)

92map[x][y]=choose_tile(n)

93end

94end

95map

96end

97

98defchoose_tile(val)

99caseval

100when0.0..0.3#30%chance

101@water

102when0.3..0.45#15%chance,wateredges

103@sand

104else#55%chance

105@grass

106end

107end

108

109end

110

111window=GameWindow.new

112window.show

Runtheprogram,zoomwithup/downarrowsandregenerateeverythingwithspacebar.$ruby02-warmup/perlin_noise_map.rb

Page 39: Developing Games With Ruby: For those who write code for living

MapgeneratedwithPerlinnoise

Thisisalittlelongerthanourpreviousexamples,sowewillanalyzesomepartstomakeitclear.81defgenerate_map

82noises=Perlin::Noise.new(2)

83contrast=Perlin::Curve.contrast(

84Perlin::Curve::CUBIC,2)

85map={}

86MAP_WIDTH.timesdo|x|

87map[x]={}

88MAP_HEIGHT.timesdo|y|

89n=noises[x*0.1,y*0.1]

90n=contrast.call(n)

91map[x][y]=choose_tile(n)

92end

93end

94map

95end

generate_mapistheheartofthisprogram.ItcreatestwodimensionalPerlin::Noisegenerator,thenchoosesarandomtileforeachlocationofthemap,accordingtonoisevalue.Tomakethemapalittlesharper,cubiccontrastisappliedtonoisevaluebeforechoosingthetile.Trycommentingoutcontrastapplication-itwilllooklikeaboringgolfcourse,sincenoisevalueswillkeepbuzzingaroundthemiddle.97defchoose_tile(val)

98caseval

99when0.0..0.3#30%chance

Page 40: Developing Games With Ruby: For those who write code for living

100@water

101when0.3..0.45#15%chance,wateredges

102@sand

103else#55%chance

104@grass

105end

106end

Herewecouldgocrazyifwehadmoredifferenttilestouse.Wecouldadddeepwatersat0.0..0.1,mountainsat0.9..0.95andsnowcapsat0.95..1.0.Andallthiswouldhavebeautifultransitions.

PlayerMovementWithKeyboardAndMouseWehavelearnedtodrawmaps,butweneedaprotagonisttoexplorethem.ItwillbeatankthatyoucanmovearoundtheislandwithWASDkeysanduseyourmousetotargetit’sgunatthings.Thetankwillbedrawnontopofourislandmap,anditwillbeaboveground,butbelowtreelayer,soitcansneakbehindpalmtrees.That’sasclosetorealdealasitgets!02-warmup/player_movement.rb

1require'gosu'

2require'gosu_tiled'

3require'gosu_texture_packer'

4

5classTank

6attr_accessor:x,:y,:body_angle,:gun_angle

7

8definitialize(window,body,shadow,gun)

9@x=window.width/2

10@y=window.height/2

11@window=window

12@body=body

13@shadow=shadow

14@gun=gun

15@body_angle=0.0

16@gun_angle=0.0

17end

18

19defupdate

20atan=Math.atan2([email protected]_x,

[email protected]_y)

22@gun_angle=-atan*180/Math::PI

23@body_angle=change_angle(@body_angle,

24Gosu::KbW,Gosu::KbS,Gosu::KbA,Gosu::KbD)

25end

26

27defdraw

[email protected]_rot(@x-1,@y-1,0,@body_angle)

[email protected]_rot(@x,@y,1,@body_angle)

[email protected]_rot(@x,@y,2,@gun_angle)

31end

32

33private

34

35defchange_angle(previous_angle,up,down,right,left)

[email protected]_down?(up)

37angle=0.0

[email protected]_down?(left)

[email protected]_down?(right)

[email protected]_down?(down)

41angle=180.0

[email protected]_down?(left)

[email protected]_down?(right)

[email protected]_down?(left)

45angle=90.0

[email protected]_down?(up)

[email protected]_down?(down)

[email protected]_down?(right)

Page 41: Developing Games With Ruby: For those who write code for living

49angle=270.0

[email protected]_down?(up)

[email protected]_down?(down)

52end

53angle||previous_angle

54end

55end

56

57classGameWindow<Gosu::Window

58MAP_FILE=File.join(File.dirname(

59__FILE__),'island.json')

60UNIT_FILE=File.join(File.dirname(File.dirname(

61__FILE__)),'media','ground_units.json')

62SPEED=5

63

64definitialize

65super(640,480,false)

66@map=Gosu::Tiled.load_json(self,MAP_FILE)

67@units=Gosu::TexturePacker.load_json(

68self,UNIT_FILE,:precise)

69@tank=Tank.new(self,

[email protected]('tank1_body.png'),

[email protected]('tank1_body_shadow.png'),

[email protected]('tank1_dualgun.png'))

73@x=@y=0

74@first_render=true

75@buttons_down=0

76end

77

78defneeds_cursor?

79true

80end

81

82defbutton_down(id)

83closeifid==Gosu::KbEscape

84@buttons_down+=1

85end

86

87defbutton_up(id)

88@buttons_down-=1

89end

90

91defupdate

92@x-=SPEEDifbutton_down?(Gosu::KbA)

93@x+=SPEEDifbutton_down?(Gosu::KbD)

94@y-=SPEEDifbutton_down?(Gosu::KbW)

95@y+=SPEEDifbutton_down?(Gosu::KbS)

[email protected]

97self.caption="#{Gosu.fps}FPS."<<

98'UseWASDandmousetocontroltank'

99end

100

101defdraw

102@first_render=false

[email protected](@x,@y)

[email protected]()

105end

106end

107

108GameWindow.new.show

Tankspriteisrenderedinthemiddleofscreen.Itconsistsofthreelayers,bodyshadow,bodyandgun.Bodyandit’sshadowarealwaysrenderedinsameangle,oneontopofanother.Theangleisdeterminedbykeysthatarepressed.Itsupports8directions.

Gunisalittlebitdifferent.Itfollowsmousecursor.Todeterminetheanglewehadtousesomemath.Theformulatogetangleindegreesisarctan(delta_x/delta_y)*180/PI.Youcanseeitexplainedinmoredetailonstackoverflow.

Runitandstrollaroundtheisland.Youcanstillmoveonwaterandintothedarkness,awayfromthemapitself,butwewillhandleitlater.

Page 42: Developing Games With Ruby: For those who write code for living

$ruby02-warmup/player_movement.rb

Seethattankhidingbetweenthebushes,readytogoin8directionsandblowthingsupwiththatpreciselyaimeddoublecannon?

Page 43: Developing Games With Ruby: For those who write code for living

Tankmovingaroundandaimingguns

GameCoordinateSystemBynowwemaystartrealizing,thatthereisonekeycomponentmissinginourdesigns.Wehaveavirtualmap,whichisbiggerthanourscreenspace,andweshouldperformallcalculationsusingthatmap,andonlythencutouttherequiredpieceandrenderitinourgamewindow.

Therearethreedifferentcoordinatesystemsthathavetomapwitheachother:

1. Gamecoordinates2. Viewportcoordinates3. Screencoordinates

Page 44: Developing Games With Ruby: For those who write code for living

Coordinatesystems

GameCoordinates

Thisiswherealllogicwillhappen.Playerlocation,enemylocations,poweruplocations-allthiswillhavegamecoordinates,anditshouldhavenothingtodowithyourscreenposition.

ViewportCoordinates

Viewportisthepositionofvirtualcamera,thatis“filming”worldinaction.Don’tconfuseitwithscreencoordinates,becauseviewportwillnotnecessarilybemappedpixeltopixeltoyourgamewindow.Imaginethis:youhaveahugeworldmap,yourplayerisstandinginthemiddle,andgamewindowdisplaystheplayerwhileslowlyzoomingin.Inthis

Page 45: Developing Games With Ruby: For those who write code for living

scenario,viewportisconstantlyshrinking,whilegamemapstaysthesame,andgamewindowalsostaysthesame.

ScreenCoordinates

Thisisyourgamedisplay,pixelbypixel.Youwilldrawstaticinformation,likeyourHUDdirectlyonit.

HowToPutItAllTogether

Inourgameswewillwanttoseparategamecoordinatesfromviewportandscreenasmuchaspossible.Basically,wewillprogramourselvesa“cameraman”whowillbebusyfollowingtheaction,zoominginandout,perhapschangingtheviewanglenowandthen.

Let’simplementaprototypethatwillallowustonavigateandzoomaroundabigmap.Wewillonlydrawobjectsthatarevisibleinviewport.Somemathwillbeunavoidable,butinmostcasesit’sprettybasic-that’sthebeautyof2Dgames:02-warmup/coordinate_system.rb

1require'gosu'

2

3classWorldMap

4attr_accessor:on_screen,:off_screen

5

6definitialize(width,height)

7@images={}

8(0..width).step(50)do|x|

9@images[x]={}

10(0..height).step(50)do|y|

11img=Gosu::Image.from_text(

12$window,"#{x}:#{y}",

13Gosu.default_font_name,15)

14@images[x][y]=img

15end

16end

17end

18

19defdraw(camera)

20@on_screen=@off_screen=0

[email protected]|x,row|

22row.eachdo|y,val|

23ifcamera.can_view?(x,y,val)

24val.draw(x,y,0)

25@on_screen+=1

26else

27@off_screen+=1

28end

29end

30end

31end

32end

33

34classCamera

35attr_accessor:x,:y,:zoom

36

37definitialize

38@x=@y=0

39@zoom=1

40end

41

42defcan_view?(x,y,obj)

43x0,x1,y0,y1=viewport

44(x0-obj.width..x1).include?(x)&&

45(y0-obj.height..y1).include?(y)

46end

47

48defviewport

Page 46: Developing Games With Ruby: For those who write code for living

49x0=@x-($window.width/2)/@zoom

50x1=@x+($window.width/2)/@zoom

51y0=@y-($window.height/2)/@zoom

52y1=@y+($window.height/2)/@zoom

53[x0,x1,y0,y1]

54end

55

56defto_s

57"FPS:#{Gosu.fps}."<<

58"#{@x}:#{@y}@#{'%.2f'%@zoom}."<<

59'WASDtomove,arrowstozoom.'

60end

61

62defdraw_crosshair

63$window.draw_line(

64@x-10,@y,Gosu::Color::YELLOW,

65@x+10,@y,Gosu::Color::YELLOW,100)

66$window.draw_line(

67@x,@y-10,Gosu::Color::YELLOW,

68@x,@y+10,Gosu::Color::YELLOW,100)

69end

70end

71

72

73classGameWindow<Gosu::Window

74SPEED=10

75

76definitialize

77super(800,600,false)

78$window=self

79@map=WorldMap.new(2048,1024)

80@camera=Camera.new

81end

82

83defbutton_down(id)

84closeifid==Gosu::KbEscape

85ifid==Gosu::KbSpace

[email protected]=1.0

[email protected]=0

[email protected]=0

89end

90end

91

92defupdate

[email protected]=SPEEDifbutton_down?(Gosu::KbA)

[email protected]+=SPEEDifbutton_down?(Gosu::KbD)

[email protected]=SPEEDifbutton_down?(Gosu::KbW)

[email protected]+=SPEEDifbutton_down?(Gosu::KbS)

97

[email protected]>0?0.01:1.0

99

100ifbutton_down?(Gosu::KbUp)

[email protected]=zoom_delta

102end

103ifbutton_down?(Gosu::KbDown)

[email protected]+=zoom_delta

105end

[email protected]_s

107end

108

109defdraw

[email protected]+width/2

[email protected]+height/2

[email protected]

[email protected]

114translate(off_x,off_y)do

[email protected]_crosshair

[email protected]

117scale(zoom,zoom,cam_x,cam_y)do

[email protected](@camera)

119end

120end

121info='Objectson/offscreen:'<<

122"#{@map.on_screen}/#{@map.off_screen}"

123info_img=Gosu::Image.from_text(

124self,info,Gosu.default_font_name,30)

Page 47: Developing Games With Ruby: For those who write code for living

125info_img.draw(10,10,1)

126end

127end

128

129GameWindow.new.show

Runit,useWASDtonavigate,up/downarrowstozoomandspacebartoresetthecamera.$ruby02-warmup/coordinate_system.rb

Itdoesn’tlookimpressive,butunderstandingtheconceptofdifferentcoordinatesystemsandbeingabletostitchthemtogetherisparamounttothesuccessofourfinalproduct.

Page 48: Developing Games With Ruby: For those who write code for living

Prototypeofseparatecoordinatesystems

Luckilyforus,GosuhelpsusbyprovidingGosu::Window#translatethathandlescameraoffset,Gosu::Window#scalethataidszooming,andGosu::Window#rotatethatwasnotusedyet,butwillbegreatforshakingtheviewtoemphasizeexplosions.

Page 49: Developing Games With Ruby: For those who write code for living

PrototypingTheGame

Warmingupwasreallyimportant,butlet’scombineeverythingwelearned,addsomenewchallenges,andbuildasmallprototypewithfollowingfeatures:

1. Cameralooselyfollowstank.2. Camerazoomsautomaticallydependingontankspeed.3. Youcantemporarilyoverrideautomaticcamerazoomusingkeyboard.4. Musicandsoundeffects.5. Randomlygeneratedmap.6. Twomodes:menuandgameplay.7. TankmovementwithWADSkeys.8. Tankaimingandshootingwithmouse.9. Collisiondetection(tanksdon’tswim).10. Explosions,visiblebullettrajectories.11. Bulletrangelimiting.

Soundsfun?Hellyes!However,beforewestart,weshouldplanaheadalittleandthinkhowourgamearchitecturewilllooklike.Wewillalsostructureourcodealittle,soitwillnotbesmashedintoonerubyclass,aswedidinearlierexamples.Booksshouldshowgoodmanners!

SwitchingBetweenGameStatesFirst,let’sthinkhowtohookintoGosu::Window.Sincewewillhavetwogamestates,Statepatternnaturallycomestomind.

So,ourGameWindowclasscouldlooklikethis:03-prototype/game_window.rb

1classGameWindow<Gosu::Window

2

3attr_accessor:state

4

5definitialize

6super(800,600,false)

7end

8

9defupdate

[email protected]

11end

12

13defdraw

[email protected]

15end

16

17defneeds_redraw?

[email protected]_redraw?

19end

20

21defbutton_down(id)

[email protected]_down(id)

Page 50: Developing Games With Ruby: For those who write code for living

23end

24

25end

Ithascurrent@state,andallusualmainloopactionsareexecutedonthatstateinstance.Wewilladdbaseclassthatallgamestateswillextend.Let’snameitGameState:03-prototype/states/game_state.rb

1classGameState

2

3defself.switch(new_state)

4$window.state&&$window.state.leave

5$window.state=new_state

6new_state.enter

7end

8

9defenter

10end

11

12defleave

13end

14

15defdraw

16end

17

18defupdate

19end

20

21defneeds_redraw?

22true

23end

24

25defbutton_down(id)

26end

27end

ThisclassprovidesGameState.switch,thatwillchangethestateforourGosu::Window,andallenterandleavemethodswhenappropriate.Thesemethodswillbeusefulforthingslikeswitchingmusic.

NoticethatGosu::Windowisaccessedusingglobal$windowvariable,whichwillbeconsideredananti-patternbymostgoodprogrammers,butthereissomelogicbehindthis:

1. TherewillbeonlyoneGosu::Windowinstance.2. Itlivesaslongasthegameruns.3. Itisusedinsomewaybynearlyallotherclasses,sowewouldhavetopassitaround

allthetime.4. AccessingitusingSingletonorstaticutilityclasswouldnotgiveanyclearbenefits,

justaddmorecomplexity.

Chingu,anothergameframeworkbuiltontopofGosu,alsousesglobal$window,soit’sprobablynottheworstideaever.

Wewillalsoneedanentrypointthatwouldfireupthegameandenterthefirstgamestate-themenu.03-prototype/main.rb

1require'gosu'

2require_relative'states/game_state'

3require_relative'states/menu_state'

4require_relative'states/play_state'

Page 51: Developing Games With Ruby: For those who write code for living

5require_relative'game_window'

6

7moduleGame

8defself.media_path(file)

9File.join(File.dirname(File.dirname(

10__FILE__)),'media',file)

11end

12end

13

14$window=GameWindow.new

15GameState.switch(MenuState.instance)

16$window.show

InourentrypointwealsohaveasmallhelperwhichwillhelploadingimagesandsoundsusingGame.media_path.

Therestisobvious:wecreateGameWindowinstanceandstoreitin$windowvariable,asdiscussedbefore.ThenweuseGameState.switch)toloadMenuState,andshowthegamewindow.

ImplementingMenuStateThisishowsimpleMenuStateimplementationlookslike:03-prototype/states/menu_state.rb

1require'singleton'

2classMenuState<GameState

3includeSingleton

4attr_accessor:play_state

5

6definitialize

7@message=Gosu::Image.from_text(

8$window,"TanksPrototype",

9Gosu.default_font_name,100)

10end

11

12defenter

13music.play(true)

14music.volume=1

15end

16

17defleave

18music.volume=0

19music.stop

20end

21

22defmusic

23@@music||=Gosu::Song.new(

24$window,Game.media_path('menu_music.mp3'))

25end

26

27defupdate

28continue_text=@play_state?"C=Continue,":""

29@info=Gosu::Image.from_text(

30$window,"Q=Quit,#{continue_text}N=NewGame",

31Gosu.default_font_name,30)

32end

33

34defdraw

[email protected](

36$window.width/[email protected]/2,

37$window.height/[email protected]/2,

3810)

[email protected](

40$window.width/[email protected]/2,

41$window.height/[email protected]/2+200,

4210)

43end

44

Page 52: Developing Games With Ruby: For those who write code for living

45defbutton_down(id)

46$window.closeifid==Gosu::KbQ

47ifid==Gosu::KbC&&@play_state

48GameState.switch(@play_state)

49end

50ifid==Gosu::KbN

51@play_state=PlayState.new

52GameState.switch(@play_state)

53end

54end

55end

It’saSingleton,sowecanalwaysgetitwithMenuState.instance.

Itstartsplayingmenu_music.mp3whenyouenterthemenu,andstopthemusicwhenyouleaveit.InstanceofGosu::Songiscachedin@@musicclassvariabletosaveresources.

Wehavetoknowifplayisalreadyinprogress,sowecanaddapossibilitytogobacktothegame.That’swhyMenuStatehas@play_statevariable,andeitherallowscreatingnewPlayStatewhenNkeyispressed,orswitchestoexisting@play_stateifCkeyispressed.

Herecomestheinterestingpart,implementingtheplaystate.

ImplementingPlayStateBeforewestartimplementingactualgameplay,weneedtothinkwhatgameentitieswewillbebuilding.WewillneedaMapthatwillholdourtilesandprovideworldcoordinatesystem.WewillalsoneedaCamerathatwillknowhowtofloataroundandzoom.TherewillbeBulletsflyingaround,andeachbulletwilleventuallycauseanExplosion.

Havingallthattakencareof,PlayStateshouldlookprettysimple:03-prototype/states/play_state.rb

1require_relative'../entities/map'

2require_relative'../entities/tank'

3require_relative'../entities/camera'

4require_relative'../entities/bullet'

5require_relative'../entities/explosion'

6classPlayState<GameState

7

8definitialize

9@map=Map.new

10@tank=Tank.new(@map)

11@camera=Camera.new(@tank)

12@bullets=[]

13@explosions=[]

14end

15

16defupdate

[email protected](@camera)

18@bullets<<bulletifbullet

[email protected](&:update)

[email protected]!(&:done?)

[email protected]

22$window.caption='TanksPrototype.'<<

23"[FPS:#{Gosu.fps}.Tank@#{@tank.x.round}:#{@tank.y.round}]"

24end

25

26defdraw

[email protected]

[email protected]

29off_x=$window.width/2-cam_x

30off_y=$window.height/2-cam_y

31$window.translate(off_x,off_y)do

[email protected]

Page 53: Developing Games With Ruby: For those who write code for living

33$window.scale(zoom,zoom,cam_x,cam_y)do

[email protected](@camera)

[email protected]

[email protected](&:draw)

37end

38end

[email protected]_crosshair

40end

41

42defbutton_down(id)

43ifid==Gosu::MsLeft

[email protected](*@camera.mouse_coords)

45@bullets<<bulletifbullet

46end

47$window.closeifid==Gosu::KbQ

48ifid==Gosu::KbEscape

49GameState.switch(MenuState.instance)

50end

51end

52

53end

Updateanddrawcallsarepassedtotheunderlyinggameentities,sotheycanhandlethemthewaytheywantitto.Suchencapsulationreducescomplexityofthecodeandallowsdoingeverypieceoflogicwhereitbelongs,whilekeepingitshortandsimple.

[email protected]@tank.shootmayproduceanewbullet,ifyourtank’sfirerateisnotexceeded,andifleftmousebuttoniskeptdown,hencetheupdate.Ifbulletisproduced,itisaddedto@bulletsarray,andtheylivetheirownlittlelifecycle,[email protected]!(&:done?)cleansupthegarbage.

PlayState#[email protected]@camera.ypointstogamecoordinateswhereCameraiscurrentlylookingat.Gosu::Window#translatecreatesablockwithinwhichallGosu::Imagedrawoperationsaretranslatedbygivenoffset.Gosu::Window#scaledoesthesamewithCamerazoom.

Crosshairisdrawnwithouttranslatingandscalingit,becauseit’srelativetoscreen,nottoworldmap.

Basically,thisdrawmethodistheplacethattakescaredrawingonlywhat@cameracansee.

Ifit’shardtounderstandhowthisworks,getbackto“GameCoordinateSystem”chapterandletitsinkin.

ImplementingWorldMapWewillstartanalyzinggameentitieswithMap.03-prototype/entities/map.rb

1require'perlin_noise'

2require'gosu_texture_packer'

3

4classMap

5MAP_WIDTH=100

6MAP_HEIGHT=100

7TILE_SIZE=128

8

9definitialize

10load_tiles

11@map=generate_map

Page 54: Developing Games With Ruby: For those who write code for living

12end

13

14deffind_spawn_point

15whiletrue

16x=rand(0..MAP_WIDTH*TILE_SIZE)

17y=rand(0..MAP_HEIGHT*TILE_SIZE)

18ifcan_move_to?(x,y)

19return[x,y]

20else

21puts"Invalidspawnpoint:#{[x,y]}"

22end

23end

24end

25

26defcan_move_to?(x,y)

27tile=tile_at(x,y)

28tile&&tile!=@water

29end

30

31defdraw(camera)

[email protected]|x,row|

33row.eachdo|y,val|

34tile=@map[x][y]

35map_x=x*TILE_SIZE

36map_y=y*TILE_SIZE

37ifcamera.can_view?(map_x,map_y,tile)

38tile.draw(map_x,map_y,0)

39end

40end

41end

42end

43

44private

45

46deftile_at(x,y)

47t_x=((x/TILE_SIZE)%TILE_SIZE).floor

48t_y=((y/TILE_SIZE)%TILE_SIZE).floor

49row=@map[t_x]

50row[t_y]ifrow

51end

52

53defload_tiles

54tiles=Gosu::Image.load_tiles(

55$window,Game.media_path('ground.png'),

56128,128,true)

57@sand=tiles[0]

58@grass=tiles[8]

59@water=Gosu::Image.new(

60$window,Game.media_path('water.png'),true)

61end

62

63defgenerate_map

64noises=Perlin::Noise.new(2)

65contrast=Perlin::Curve.contrast(

66Perlin::Curve::CUBIC,2)

67map={}

68MAP_WIDTH.timesdo|x|

69map[x]={}

70MAP_HEIGHT.timesdo|y|

71n=noises[x*0.1,y*0.1]

72n=contrast.call(n)

73map[x][y]=choose_tile(n)

74end

75end

76map

77end

78

79defchoose_tile(val)

80caseval

81when0.0..0.3#30%chance

82@water

83when0.3..0.45#15%chance,wateredges

84@sand

85else#55%chance

86@grass

87end

Page 55: Developing Games With Ruby: For those who write code for living

88end

89end

ThisimplementationisverysimilartotheMapwehadbuiltin“GeneratingRandomMapWithPerlinNoise”,withsomeextraadditions.can_move_to?verifiesiftileundergivencoordinatesisnotwater.Prettysimple,butit’senoughforourprototype.

Also,whenwedrawthemapwehavetomakesureiftileswearedrawingarecurrentlyvisiblebyourcamera,otherwisewewillendupdrawingoffscreen.camera.can_view?handlesit.Currentimplementationwillprobablybecausingabottleneck,sinceitbruteforcesthroughallthemapratherthancherry-pickingthevisibleregion.Wewillprobablyhavetogetbackandchangeitlater.

find_spawn_pointisonemoreaddition.Itkeepspickingarandompointonmapandverifiesifit’snotwaterusingcan_move_to?.Whensolidtileisfound,itreturnsthecoordinates,soourTankwillbeabletospawnthere.

ImplementingFloatingCameraIfyouplayedtheoriginalGrandTheftAutoorGTA2,youshouldrememberhowfascinatingthecamerawas.Itbackedawaywhenyouweredrivingathighspeeds,closedinwhenyouwerewalkingonfoot,andfloatedaroundasifasmartdronewasfollowingyourprotagonistfromabove.

ThefollowingCameraimplementationisfarinferiortotheoneGTAhadnearlytwodecadesago,butit’sastart:03-prototype/entities/camera.rb

1classCamera

2attr_accessor:x,:y,:zoom

3

4definitialize(target)

5@target=target

6@x,@y=target.x,target.y

7@zoom=1

8end

9

10defcan_view?(x,y,obj)

11x0,x1,y0,y1=viewport

12(x0-obj.width..x1).include?(x)&&

13(y0-obj.height..y1).include?(y)

14end

15

16defmouse_coords

17x,y=target_delta_on_screen

[email protected]+

19(x+$window.mouse_x-($window.width/2))/@zoom

[email protected]+

21(y+$window.mouse_y-($window.height/2))/@zoom

22[mouse_x_on_map,mouse_y_on_map].map(&:round)

23end

24

25defupdate

26@[email protected]@x<@target.x-$window.width/4

27@[email protected]@x>@target.x+$window.width/4

28@[email protected]@y<@target.y-$window.height/4

29@[email protected]@y>@target.y+$window.height/4

30

31zoom_delta=@zoom>0?0.01:1.0

32if$window.button_down?(Gosu::KbUp)

33@zoom-=zoom_deltaunless@zoom<0.7

34elsif$window.button_down?(Gosu::KbDown)

35@zoom+=zoom_deltaunless@zoom>10

Page 56: Developing Games With Ruby: For those who write code for living

36else

[email protected]>1.1?0.85:1.0

38if@zoom<=(target_zoom-0.01)

39@zoom+=zoom_delta/3

40elsif@zoom>(target_zoom+0.01)

41@zoom-=zoom_delta/3

42end

43end

44end

45

46defto_s

47"FPS:#{Gosu.fps}."<<

48"#{@x}:#{@y}@#{'%.2f'%@zoom}."<<

49'WASDtomove,arrowstozoom.'

50end

51

52deftarget_delta_on_screen

53[(@[email protected])*@zoom,(@[email protected])*@zoom]

54end

55

56defdraw_crosshair

57x=$window.mouse_x

58y=$window.mouse_y

59$window.draw_line(

60x-10,y,Gosu::Color::RED,

61x+10,y,Gosu::Color::RED,100)

62$window.draw_line(

63x,y-10,Gosu::Color::RED,

64x,y+10,Gosu::Color::RED,100)

65end

66

67private

68

69defviewport

70x0=@x-($window.width/2)/@zoom

71x1=@x+($window.width/2)/@zoom

72y0=@y-($window.height/2)/@zoom

73y1=@y+($window.height/2)/@zoom

74[x0,x1,y0,y1]

75end

76end

OurCamerahas@targetthatittriestofollow,@xand@ythatitcurrentlyislookingat,and@zoomlevel.

Allthemagichappensinupdatemethod.Itkeepstrackofthedistancebetween@targetandadjustitselftostaynearby.Andwhen@target.speedshowssomemovementmomentum,cameraslowlybacksaway.

Cameraalsotelsifyoucan_view?anobjectatsomecoordinates,sowhenotherentitiesdrawthemselves,theycancheckifthereisaneedforthat.

Anothernoteworthymethodismouse_coords.Ittranslatesmousepositiononscreentomousepositiononmap,sothegamewillknowwhereyouaretargetingyourguns.

ImplementingTheTankMostofourtankcodewillbetakenfrom“PlayerMovementWithKeyboardAndMouse”:03-prototype/entities/tank.rb

1classTank

2attr_accessor:x,:y,:body_angle,:gun_angle

3SHOOT_DELAY=500

4

5definitialize(map)

6@map=map

7@units=Gosu::TexturePacker.load_json(

Page 57: Developing Games With Ruby: For those who write code for living

8$window,Game.media_path('ground_units.json'),:precise)

9@[email protected]('tank1_body.png')

10@[email protected]('tank1_body_shadow.png')

11@[email protected]('tank1_dualgun.png')

12@x,@[email protected]_spawn_point

13@body_angle=0.0

14@gun_angle=0.0

15@last_shot=0

16sound.volume=0.3

17end

18

19defsound

20@@sound||=Gosu::Song.new(

21$window,Game.media_path('tank_driving.mp3'))

22end

23

24defshoot(target_x,target_y)

25ifGosu.milliseconds-@last_shot>SHOOT_DELAY

26@last_shot=Gosu.milliseconds

27Bullet.new(@x,@y,target_x,target_y).fire(100)

28end

29end

30

31defupdate(camera)

32d_x,d_y=camera.target_delta_on_screen

33atan=Math.atan2(($window.width/2)-d_x-$window.mouse_x,

34($window.height/2)-d_y-$window.mouse_y)

35@gun_angle=-atan*180/Math::PI

36new_x,new_y=@x,@y

37new_x-=speedif$window.button_down?(Gosu::KbA)

38new_x+=speedif$window.button_down?(Gosu::KbD)

39new_y-=speedif$window.button_down?(Gosu::KbW)

40new_y+=speedif$window.button_down?(Gosu::KbS)

[email protected]_move_to?(new_x,new_y)

42@x,@y=new_x,new_y

43else

44@speed=1.0

45end

46@body_angle=change_angle(@body_angle,

47Gosu::KbW,Gosu::KbS,Gosu::KbA,Gosu::KbD)

48

49ifmoving?

50sound.play(true)

51else

52sound.pause

53end

54

55if$window.button_down?(Gosu::MsLeft)

56shoot(*camera.mouse_coords)

57end

58end

59

60defmoving?

61any_button_down?(Gosu::KbA,Gosu::KbD,Gosu::KbW,Gosu::KbS)

62end

63

64defdraw

[email protected]_rot(@x-1,@y-1,0,@body_angle)

[email protected]_rot(@x,@y,1,@body_angle)

[email protected]_rot(@x,@y,2,@gun_angle)

68end

69

70defspeed

71@speed||=1.0

72ifmoving?

73@speed+=0.03if@speed<5

74else

75@speed=1.0

76end

77@speed

78end

79

80private

81

82defany_button_down?(*buttons)

83buttons.eachdo|b|

Page 58: Developing Games With Ruby: For those who write code for living

84returntrueif$window.button_down?(b)

85end

86false

87end

88

89defchange_angle(previous_angle,up,down,right,left)

90if$window.button_down?(up)

91angle=0.0

92angle+=45.0if$window.button_down?(left)

93angle-=45.0if$window.button_down?(right)

94elsif$window.button_down?(down)

95angle=180.0

96angle-=45.0if$window.button_down?(left)

97angle+=45.0if$window.button_down?(right)

98elsif$window.button_down?(left)

99angle=90.0

100angle+=45.0if$window.button_down?(up)

101angle-=45.0if$window.button_down?(down)

102elsif$window.button_down?(right)

103angle=270.0

104angle-=45.0if$window.button_down?(up)

105angle+=45.0if$window.button_down?(down)

106end

107angle||previous_angle

108end

109end

TankhastobeawareoftheMaptocheckwhereit’smoving,anditusesCameratofindoutwheretoaimtheguns.Whenitshoots,itproducesinstancesofBullet,thataresimplyreturnedtothecaller.Tankwon’tkeeptrackofthem,it’s“fireandforget”.

ImplementingBulletsAndExplosionsBulletswillrequiresomesimplevectormath.Youhaveapointthatmovesalongthevectorwithsomespeed.Italsoneedstolimitthemaximumvectorlength,soifyoutrytoaimtoofar,thebulletwillonlygoasfarasitcanreach.03-prototype/entities/bullet.rb

1classBullet

2COLOR=Gosu::Color::BLACK

3MAX_DIST=300

4START_DIST=20

5

6definitialize(source_x,source_y,target_x,target_y)

7@x,@y=source_x,source_y

8@target_x,@target_y=target_x,target_y

9@x,@y=point_at_distance(START_DIST)

10iftrajectory_length>MAX_DIST

11@target_x,@target_y=point_at_distance(MAX_DIST)

12end

13sound.play

14end

15

16defdraw

17unlessarrived?

18$window.draw_quad(@x-2,@y-2,COLOR,

19@x+2,@y-2,COLOR,

20@x-2,@y+2,COLOR,

21@x+2,@y+2,COLOR,

221)

23else

24@explosion||=Explosion.new(@x,@y)

[email protected]

26end

27end

28

29defupdate

30fly_distance=(Gosu.milliseconds-@fired_at)*0.001*@speed

31@x,@y=point_at_distance(fly_distance)

Page 59: Developing Games With Ruby: For those who write code for living

32@explosion&&@explosion.update

33end

34

35defarrived?

36@x==@target_x&&@y==@target_y

37end

38

39defdone?

40exploaded?

41end

42

43defexploaded?

44@explosion&&@explosion.done?

45end

46

47deffire(speed)

48@speed=speed

49@fired_at=Gosu.milliseconds

50self

51end

52

53private

54

55defsound

56@@sound||=Gosu::Sample.new(

57$window,Game.media_path('fire.mp3'))

58end

59

60deftrajectory_length

61d_x=@target_x-@x

62d_y=@target_y-@y

63Math.sqrt(d_x*d_x+d_y*d_y)

64end

65

66defpoint_at_distance(distance)

67return[@target_x,@target_y]ifdistance>trajectory_length

68distance_factor=distance.to_f/trajectory_length

69p_x=@x+(@target_x-@x)*distance_factor

70p_y=@y+(@target_y-@y)*distance_factor

71[p_x,p_y]

72end

73end

PossiblythemostinterestingpartofBulletimplementationispoint_at_distancemethod.Itreturnscoordinatesofpointthatisbetweenbulletsource,whichispointthatbulletwasfiredfrom,andit’starget,whichisthedestinationpoint.Thereturnedpointisasfarawayfromsourcepointasdistancetellsitto.

Afterbullethasdoneflying,itexplodeswithfanfare.InourprototypeExplosionisapartofBullet,becauseit’stheonlythingthattriggersit.ThereforeBullethastwostagesofit’slifecycle.Firstitfliestowardsthetarget,thenit’sexploding.ThatbringsustoExplosion:03-prototype/entities/explosion.rb

1classExplosion

2FRAME_DELAY=10#ms

3

4defanimation

5@@animation||=

6Gosu::Image.load_tiles(

7$window,Game.media_path('explosion.png'),128,128,false)

8end

9

10defsound

11@@sound||=Gosu::Sample.new(

12$window,Game.media_path('explosion.mp3'))

13end

14

15definitialize(x,y)

Page 60: Developing Games With Ruby: For those who write code for living

16sound.play

17@x,@y=x,y

18@current_frame=0

19end

20

21defupdate

22@current_frame+=1ifframe_expired?

23end

24

25defdraw

26returnifdone?

27image=current_frame

28image.draw(

[email protected]/2+3,

[email protected]/2-35,

3120)

32end

33

34defdone?

35@done||=@current_frame==animation.size

36end

37

38private

39

40defcurrent_frame

41animation[@current_frame%animation.size]

42end

43

44defframe_expired?

45now=Gosu.milliseconds

46@last_frame||=now

47if(now-@last_frame)>FRAME_DELAY

48@last_frame=now

49end

50end

51end

Thereisnothingfancyaboutthisimplementation.Mostofitistakenfrom“ImagesAndAnimation”chapter.

RunningThePrototypeWehavewalkedthroughallthecode.YoucangetitatGitHub.

Nowit’stimetogiveitaspin.ThereisavideoofmeplayingitavailableonYouTube,butit’salwaysbesttoexperienceitfirsthand.Runmain.rbtostartthegame:$ruby03-prototype/main.rb

HitNtostartnewgame.

Page 61: Developing Games With Ruby: For those who write code for living

TanksPrototypemenu

Timetogocrazy!

Page 62: Developing Games With Ruby: For those who write code for living

TanksPrototypegameplay

Onethingshouldbebuggingyouatthispoint.FPSshowsonly30,ratherthan60.Thatmeansourprototypeisslow.Wewillputitbackto60FPSinnextchapter.

Page 63: Developing Games With Ruby: For those who write code for living

OptimizingGamePerformance

Tomakegamesthatarefastanddon’trequireapowerhousetorun,wemustlearnhowtofindandfixbottlenecks.Goodnewsisthatifyouwasn’tthinkingaboutperformancetobeginwith,yourprogramcanusuallybeoptimizedtoruntwiceasfastjustbyeliminatingoneortwobiggestbottlenecks.

Wewillbeusingacopyoftheprototypecodetokeepbothoptimizedandoriginalversion,thereforeifyouareexploringsamplecode,lookat04-prototype-optimized.

ProfilingRubyCodeToFindBottlenecksWewilltrytofindbottlenecksinourTanksprototypegamebyprofilingitwithruby-prof.

It’sarubygem,justinstallitlikethis:$geminstallruby-prof

Thereareseveralwaysyoucanuseruby-prof,sowewillbeginwiththeeasiestone.Insteadofrunningthegamewithruby,wewillrunitwithruby-prof:$ruby-prof03-prototype/main.rb

Thegamewillrun,buteverythingwillbetentimesslowerasusual,becauseeverycalltoeveryfunctionisbeingrecorded,andafteryouexittheprogram,profilingoutputwillbedumpeddirectlytoyourconsole.

Downsideofthisapproachisthatwearegoingtoprofileeverythingthereis,includingthesuper-slowmapgenerationthatusesPerlinNoise.Wedon’twanttooptimizethat,soinordertofindbottlenecksinourplaystateratherthanmapgeneration,wehavetokeepplayingatdreadful2FPSforatleast30seconds.

Thiswastheoutputoffirst“naive”profilingsession:

Page 64: Developing Games With Ruby: For those who write code for living

Initialprofilingresults

It’sobvious,thatCamera#viewportandCamera#can_view?aretopCPUburners.Thismeanseitherthatourimplementationiseitherverybad,ortheassumptionthatcheckingifcameracanviewobjectisslowerthandrawingtheobjectoffscreen.

Herearethoseslowmethods:classCamera

#...

defcan_view?(x,y,obj)

x0,x1,y0,y1=viewport

(x0-obj.width..x1).include?(x)&&

(y0-obj.height..y1).include?(y)

end

#...

defviewport

x0=@x-($window.width/2)/@zoom

x1=@x+($window.width/2)/@zoom

y0=@y-($window.height/2)/@zoom

y1=@y+($window.height/2)/@zoom

[x0,x1,y0,y1]

end

#...

end

Itdoesn’tlookfundamentallybroken,sowewilltryour“checkingisslowerthanrendering”hypothesisbyshort-circuitingcan_view?toreturntrueeverytime:classCamera

#...

defcan_view?(x,y,obj)

returntrue#shortcircuiting

x0,x1,y0,y1=viewport

(x0-obj.width..x1).include?(x)&&

(y0-obj.height..y1).include?(y)

Page 65: Developing Games With Ruby: For those who write code for living

end

#...

end

Aftersavingcamera.rbandrunningthegamewithoutprofiling,youwillnoticeasignificantspeedup.Hypothesiswascorrect,checkingvisibilityismoreexpensivethansimplyrenderingit.ThatmeanswecanthrowawayCamera#can_view?andcallstoit.

Butbeforedoingthat,let’sprofileonceagain:

Page 66: Developing Games With Ruby: For those who write code for living

Profilingresultsaftershort-circuitingCamera#can_view?

WecanseeCamera#can_view?isstillintop3,sowewillremoveifcamera.can_view?(map_x,map_y,tile)fromMap#drawandfornowkeepitlikethis:classMap

#...

defdraw(camera)

@map.eachdo|x,row|

row.eachdo|y,val|

tile=@map[x][y]

map_x=x*TILE_SIZE

map_y=y*TILE_SIZE

tile.draw(map_x,map_y,0)

end

end

end

#...

end

AftercompletelyremovingCamera#can_view?,profilingsessionlookslikedead-end-nomorelowhangingfruitsontop:

Page 67: Developing Games With Ruby: For those who write code for living

ProfilingresultsafterremovingCamera#can_view?

Thegamestilldoesn’tfeelfastenough,FPSoccasionallykeepsdroppingdownto~45,sowewillhavetodoprofileourcodeinsmarterway.

AdvancedProfilingTechniquesWewouldgetmoreaccuracywhenprofilingonlywhatwewanttooptimize.InourcaseitiseverythingthathappensinPlayState,exceptforMapgeneration.Thistimewewillhavetouseruby-profAPItohookintoplacesweneed.

MapgenerationhappensinPlayStateinitializer,sowewillleverageGameState#enterandGameState#leavetostartandstopprofiling,sinceithappensafterstateisinitialized.Hereishowwehookin:require'ruby-prof'

classPlayState<GameState

#...

defenter

RubyProf.start

end

defleave

result=RubyProf.stop

printer=RubyProf::FlatPrinter.new(result)

printer.print(STDOUT)

end

#...

end

Thenwerunthegameasusual:$ruby04-prototype-optimized/main.rb

Page 68: Developing Games With Ruby: For those who write code for living

Now,afterwepressNtostartnewgame,Mapgenerationhappensrelativelyfast,andthenprofilingkicksin,FPSdropsto15.AftermovingaroundandshootingforawhilewehitEsctoreturntothemenu,andatthatpointPlayState#leavespitsprofilingresultsouttotheconsole:

Page 69: Developing Games With Ruby: For those who write code for living

ProfilingresultsforPlayState

WecanseethatGosu::Image#drawtakesupto20%ofallexecutiontime.ThengoesGosu::Window#caption,butweneedittomeasureFPS,sowewillleaveitalone,andfinallywecanseeHash#each,whichisguaranteedtobetheonefromMap#draw,andittriggersallthoseGosu::Image#drawcalls.

OptimizingInefficientCodeAccordingtoprofilingresults,weneedtooptimizethismethod:classMap

#...

defdraw(camera)

@map.eachdo|x,row|

row.eachdo|y,val|

tile=@map[x][y]

map_x=x*TILE_SIZE

map_y=y*TILE_SIZE

tile.draw(map_x,map_y,0)

end

end

end

#...

end

Butwehavetooptimizeitinmorecleverwaythanwedidbefore.Ifinsteadofloopingthroughallmaprowsandcolumnsandblindlyrenderingeverytileorcheckingiftileisvisiblewecouldcalculatetheexactmapcellsthatneedtobedisplayed,wewouldreducemethodcomplexityandgetmajorperformanceboost.Let’sdothat.

Page 70: Developing Games With Ruby: For those who write code for living

WewilluseCamera#viewporttoreturnmapboundariesthatarevisiblebycamera,thendividethoseboundariesbyMap#TILE_SIZEtogettilenumbersinsteadofpixels,andretrievethemfromthemap.classMap

#...

defdraw(camera)

viewport=camera.viewport

viewport.map!{|p|p/TILE_SIZE}

x0,x1,y0,y1=viewport.map(&:to_i)

(x0..x1).eachdo|x|

(y0..y1).eachdo|y|

row=@map[x]

ifrow

tile=@map[x][y]

map_x=x*TILE_SIZE

map_y=y*TILE_SIZE

tile.draw(map_x,map_y,0)

end

end

end

end

Thisoptimizationyieldedastoundingresults.Wearenowgettingnearlystable60FPSevenwhenprofilingthecode!Comparethatto2FPSwhileprofilingwhenwestarted.

Page 71: Developing Games With Ruby: For those who write code for living

ProfilingresultsforPlayStateafterMap#drawoptimization

NowwejusthavetodosomethingaboutthatGosu::Window#caption,becauseitisconsuming1/3ofourCPUcycles!Eventhoughgameisalreadyflyingsofastthatwewillhavetoreducetankandbulletspeedstomakeitlookmorerealistic,wecannotletourselvesleavethislowhangingfruitremainunpicked.

Wewillupdatethecaptiononcepersecond,itshouldremovethebottleneck:classPlayState<GameState

#...

defupdate

#...

update_caption

end

#...

private

defupdate_caption

now=Gosu.milliseconds

ifnow-(@caption_updated_at||0)>1000

$window.caption='TanksPrototype.'<<

"[FPS:#{Gosu.fps}."<<

"Tank@#{@tank.x.round}:#{@tank.y.round}]"

@caption_updated_at=now

end

end

end

Nowit’sgettinghardtogetFPStodropbelow58,andprofilingresultsshowthattherearenomorebottlenecks:

Page 72: Developing Games With Ruby: For those who write code for living

ProfilingresultsforPlayStateafterintroducingGosu::Window#captioncache

Wecannowsleepwellatnight.

ProfilingOnDemandWhenyoudevelopagame,youmaywanttoturnonprofilingnowandthen.Toavoidcommentingoutoraddingandremovingprofilingeverytimeyouwanttodoso,usethistrick:#...

require'ruby-prof'ifENV['ENABLE_PROFILING']

classPlayState<GameState

#...

defenter

RubyProf.startifENV['ENABLE_PROFILING']

end

defleave

ifENV['ENABLE_PROFILING']

result=RubyProf.stop

printer=RubyProf::FlatPrinter.new(result)

printer.print(STDOUT)

end

end

defbutton_down(id)

#...

ifid==Gosu::KbQ

leave

$window.close

end

end

#...

end

Page 73: Developing Games With Ruby: For those who write code for living

Now,toenableprofiling,simplystartyourgamewithENABLE_PROFILING=1environmentalvariable,likethis:$ENABLE_PROFILING=1ruby-prof03-prototype/main.rb

AdjustingGameSpeedForVariablePerformanceYoushouldhavenoticedthatouroptimizedTanksprototyperunswaytoofast.Tanksandbulletsshouldtravelsamedistancenomatterhowfastorslowthecodeis.

OnewouldexpectGosu::Window#update_intervaltobedesignedexactlyforthatpurpose,butitreturns16.6666inbothoriginalandoptimizedversionoftheprototype,soyoucanguessitisthedesiredinterval,nottheactualone.

Tofindoutactualupdateinterval,wewilluseGosu.millisecondsandcalculateitourselves.Todothat,wewillintroduceGame#track_update_intervalthatwillbecalledinGameWindow#update,andGame#update_intervalwhichwillretrieveactualupdateinterval,sowecanuseittoadjustourrunspeed.

WewillalsoaddGame#adjust_speedmethodthatwilltakearbitraryspeedvalueandshiftitsoisasfastasitwaswhenthegamewasrunningat30FPS.Theformulaissimple,if60FPSexpectstocallGosu::Window#updateevery16.66ms,ourspeedadjustmentwilldivideactualupdateratefrom33.33,whichroughlyequalsto16.66*2.So,ifbulletwouldfly100pixelsperupdatein30FPS,adjustedspeedwillchangeitto50pixelsat60FPS.

Hereistheimplementation:#04-prototype-optimized/main.rb

moduleGame

#...

defself.track_update_interval

now=Gosu.milliseconds

@update_interval=(now-(@last_update||=0)).to_f

@last_update=now

end

defself.update_interval

@update_interval||=$window.update_interval

end

defself.adjust_speed(speed)

speed*update_interval/33.33

end

end

#04-prototype-optimized/game_window.rb

classGameWindow<Gosu::Window

#...

defupdate

Game.track_update_interval

@state.update

end

#...

end

Now,tofixthatspeedproblem,wewillneedtoapplyGame.adjust_speedtotank,bulletandcameramovements.

Hereareallthechangesneededtomakeourgamerunatroughlysamespeedindifferentconditions:

Page 74: Developing Games With Ruby: For those who write code for living

#04-prototype-optimized/entities/tank.rb

classTank

#...

defupdate(camera)

#...

shift=Game.adjust_speed(speed)

new_x-=shiftif$window.button_down?(Gosu::KbA)

new_x+=shiftif$window.button_down?(Gosu::KbD)

new_y-=shiftif$window.button_down?(Gosu::KbW)

new_y+=shiftif$window.button_down?(Gosu::KbS)

#...

end

#...

end

#04-prototype-optimized/entities/bullet.rb

classBullet

#...

defupdate

#...

fly_speed=Game.adjust_speed(@speed)

fly_distance=(Gosu.milliseconds-@fired_at)*0.001*fly_speed

@x,@y=point_at_distance(fly_distance)

#...

end

#...

end

#04-prototype-optimized/entities/camera.rb

classCamera

#...

defupdate

shift=Game.adjust_speed(@target.speed)

@x+=shiftif@x<@target.x-$window.width/4

@x-=shiftif@x>@target.x+$window.width/4

@y+=shiftif@y<@target.y-$window.height/4

@y-=shiftif@y>@target.y+$window.height/4

zoom_delta=@zoom>0?0.01:1.0

zoom_delta=Game.adjust_speed(zoom_delta)

#...

end

#...

end

ThereisonemoretricktomakethegameplayableevenatverylowFPS.Youcansimulatesuchconditionsbyaddingsleep0.3toGameWindow#drawmethod.Atthatframerategamecursorisveryunresponsive,soyoumaywanttostartshowingnativemousecursorwhenthingsgetugly,i.e.whenupdateintervalexceeds200milliseconds:#04-prototype-optimized/game_window.rb

classGameWindow<Gosu::Window

#...

defneeds_cursor?

Game.update_interval>200

end

#...

end

FrameSkippingYouwillseestrangethingshappeningatverylowframerates.Forexample,bulletexplosionsareshowingupframebyframe,soexplosionspeedseemswaytooslowandunrealistic.Toavoidthat,wewillmodifyourExplosionclasstoemployframeskippingifupdaterateistooslow:#04-prototype-optimized/explosion.rb

classExplosion

FRAME_DELAY=16.66#ms

#...

Page 75: Developing Games With Ruby: For those who write code for living

defupdate

advance_frame

end

defdone?

@done||=@current_frame>=animation.size

end

#...

private

#...

defadvance_frame

now=Gosu.milliseconds

delta=now-(@last_frame||=now)

ifdelta>FRAME_DELAY

@last_frame=now

end

@current_frame+=(delta/FRAME_DELAY).floor

end

end

Nowourprototypeisplayableevenatlowerframerates.

Page 76: Developing Games With Ruby: For those who write code for living

RefactoringThePrototype

Atthispointyoumaybethinkingwheretogonext.Wewanttoimplementenemies,collisiondetectionandAI,butdesignofcurrentprototypeisalreadylimiting.Codeisbecomingtightlycoupled,thereisnocleanseparationbetweendifferentdomains.

Ifweweretocontinuebuildingontopofourprototype,thingswouldgetuglyquickly.Thuswewilluntanglethespaghettiandrewritesomepartsfromscratchtoachieveelegance.

GameProgrammingPatternsIwouldliketotipmyhattoRobertNystrom,whowrotethisamazingbookcalledGameProgrammingPatterns.Thebookisavailableonlineforfree,itisarelativelyquickread-I’vedevoureditwithpleasureinroughly4hours.Ifyouareguessingthatthischapterisinspiredbythatbook,youareabsolutelyright.

Componentpatternisespeciallynoteworthy.Wewillbeusingittodomajorhousekeeping,anditisgreattimetodoso,becausewehaven’timplementedmuchofthegameyet.

WhatIsWrongWithCurrentDesignUntilthispointwehavebeenbuildingthecodeinmonolithicfashion.Tankclassholdsthecodethat:

1. Loadsallgroundunitsprites.Ifsomeotherclasshandledit,wecouldreusethecodetoloadotherunits.

2. Handlessoundeffects.3. UsesGosu::Songformovingsounds.Thatlimitsonlyonetankmovementsoundper

wholegame.Basically,weabusedGosuhere.4. Handleskeyboardandmouse.IfweweretocreateAIthatcontrolsthetank,we

wouldnotbeabletoreuseTankclassbecauseofthis.5. Drawsgraphicsonscreen.6. Calculatesphysicalproperties,likespeed,acceleration.7. Detectsmovementcollisions.

Bulletisnotperfecteither:

1. Itrendersit’sgraphics.2. Ithandlesit’smovementtrajectoriesandotherphysics.3. IttreatsExplosionaspartofit’sownlifecycle.4. Drawsgraphicsonscreen.5. Handlessoundeffects.

Page 77: Developing Games With Ruby: For those who write code for living

EventherelativelysmallExplosionclassistoomonolithic:

1. Itloadsit’sgraphics.2. Ithandlesrendering,animationandframeskipping3. Itloadsandplaysit’ssoundeffects.

DecouplingUsingComponentPatternBestdesignseparatesconcernsincodesothateverythinghasit’sownplace,andeveryclasshandlesonlyonething.Let’strysplittingupTankclassintocomponentsthathandlespecificdomains:

DecoupledTank

WewillintroduceGameObjectclasswillcontainsharedfunctionalityforallgameobjects(Tank,Bullet,Explosion),eachofthemwouldhaveit’sownsetofcomponents.Everycomponentwillhaveit’sparentobject,soitwillbeabletointeractwithit,changeit’sattributes,orpossiblyinvokeothercomponentsifitcomestothat.

Page 78: Developing Games With Ruby: For those who write code for living

Gameobjectsandtheircomponents

AlltheseobjectswillbeheldwithinObjectPool,whichwouldnotcaretoknowifobjectisatankorabullet.PurposeofObjectPoolisalittledifferentinRuby,sinceGCwilltakecareofmemoryfragmentationforus,butwestillneedasingleplacethatknowsabouteveryobjectinthegame.

ObjectPool

PlayStatewouldtheniteratethrough@object_pool.objectsandinvokeupdateanddrawmethods.

Now,let’sbeginbyimplementingbaseclassforGameObject:05-refactor/entities/game_object.rb

1classGameObject

2definitialize(object_pool)

3@components=[]

4@object_pool=object_pool

5@object_pool.objects<<self

Page 79: Developing Games With Ruby: For those who write code for living

6end

7

8defcomponents

9@components

10end

11

12defupdate

[email protected](&:update)

14end

15

16defdraw(viewport)

[email protected]{|c|c.draw(viewport)}

18end

19

20defremovable?

21@removable

22end

23

24defmark_for_removal

25@removable=true

26end

27

28protected

29

30defobject_pool

31@object_pool

32end

33end

WhenGameObjectisinitialized,itregistersitselfwithObjectPoolandpreparesempty@componentsarray.ConcreteGameObjectclassesshouldinitializeComponentssothatarraywouldnotbeempty.

updateanddrawmethodswouldcyclethrough@componentsanddelegatethosecallstoeachoftheminasequence.Itisimportanttoupdateallcomponentsfirst,andonlythendrawthem.Keepinmindthat@componentsarrayorderhassignificance.Firstelementswillalwaysbeupdatedanddrawnbeforelastones.

Wewillalsoprovideremovable?methodthatwouldreturntrueforobjectsthatmark_for_removalwasinvokedon.ThiswaywewillbeabletoweedoutoldbulletsandexplosionsandfeedthemtoGC.

Nextup,baseComponentclass:05-refactor/entities/components/component.rb

1classComponent

2definitialize(game_object=nil)

3self.object=game_object

4end

5

6defupdate

7#override

8end

9

10defdraw(viewport)

11#override

12end

13

14protected

15

16defobject=(obj)

17ifobj

18@object=obj

19obj.components<<self

20end

21end

22

23defx

Page 80: Developing Games With Ruby: For those who write code for living

[email protected]

25end

26

27defy

[email protected]

29end

30

31defobject

32@object

33end

34end

ItregistersitselfwithGameObject#components,providessomeprotectedmethodstoaccessparentobjectandit’smostoftencalledproperties-xandy.

RefactoringExplosion

Explosionwasprobablythesmallestclass,sowewillextractit’scomponentsfirst.05-refactor/entities/explosion.rb

1classExplosion<GameObject

2attr_accessor:x,:y

3

4definitialize(object_pool,x,y)

5super(object_pool)

6@x,@y=x,y

7ExplosionGraphics.new(self)

8ExplosionSounds.play

9end

10end

Itismuchcleanerthanbefore.ExplosionGraphicswillbeaComponentthathandlesanimation,andExplosionSoundswillplayasound.05-refactor/entities/components/explosion_graphics.rb

1classExplosionGraphics<Component

2FRAME_DELAY=16.66#ms

3

4definitialize(game_object)

5super

6@current_frame=0

7end

8

9defdraw(viewport)

10image=current_frame

11image.draw(

12x-image.width/2+3,

13y-image.height/2-35,

1420)

15end

16

17defupdate

18now=Gosu.milliseconds

19delta=now-(@last_frame||=now)

20ifdelta>FRAME_DELAY

21@last_frame=now

22end

23@current_frame+=(delta/FRAME_DELAY).floor

24object.mark_for_removalifdone?

25end

26

27private

28

29defcurrent_frame

30animation[@current_frame%animation.size]

31end

32

33defdone?

Page 81: Developing Games With Ruby: For those who write code for living

34@done||=@current_frame>=animation.size

35end

36

37defanimation

38@@animation||=

39Gosu::Image.load_tiles(

40$window,Utils.media_path('explosion.png'),

41128,128,false)

42end

43end

Everythingthatisrelatedtoanimatingtheexplosionisnowclearlyseparated.mark_for_removaliscalledontheexplosionafterit’sanimationisdone.05-refactor/entities/components/explosion_sounds.rb

1classExplosionSounds

2class<<self

3defplay

4sound.play

5end

6

7private

8

9defsound

10@@sound||=Gosu::Sample.new(

11$window,Utils.media_path('explosion.mp3'))

12end

13end

14end

Sinceexplosionsoundsaretriggeredonlyonce,whenitstartstoexplode,ExplosionSoundsisastaticclasswithplaymethod.

RefactoringBullet

Now,let’sgoupalittleandreimplementourBullet:05-refactor/entities/bullet.rb

1classBullet<GameObject

2attr_accessor:x,:y,:target_x,:target_y,:speed,:fired_at

3

4definitialize(object_pool,source_x,source_y,target_x,target_y)

5super(object_pool)

6@x,@y=source_x,source_y

7@target_x,@target_y=target_x,target_y

8BulletPhysics.new(self)

9BulletGraphics.new(self)

10BulletSounds.play

11end

12

13defexplode

14Explosion.new(object_pool,@x,@y)

15mark_for_removal

16end

17

18deffire(speed)

19@speed=speed

20@fired_at=Gosu.milliseconds

21end

22end

Allphysics,graphicsandsoundsareextractedintoindividualcomponents,andinsteadofmanagingExplosion,itjustregistersanewExplosionwithObjectPoolandmarksitselfforremovalinexplodemethod.05-refactor/entities/components/bullet_physics.rb

Page 82: Developing Games With Ruby: For those who write code for living

1classBulletPhysics<Component

2START_DIST=20

3MAX_DIST=300

4

5definitialize(game_object)

6super

7object.x,object.y=point_at_distance(START_DIST)

8iftrajectory_length>MAX_DIST

9object.target_x,object.target_y=point_at_distance(MAX_DIST)

10end

11end

12

13defupdate

14fly_speed=Utils.adjust_speed(object.speed)

15fly_distance=(Gosu.milliseconds-object.fired_at)*0.001*fly_speed

16object.x,object.y=point_at_distance(fly_distance)

17object.explodeifarrived?

18end

19

20deftrajectory_length

21d_x=object.target_x-x

22d_y=object.target_y-y

23Math.sqrt(d_x*d_x+d_y*d_y)

24end

25

26defpoint_at_distance(distance)

27ifdistance>trajectory_length

28return[object.target_x,object.target_y]

29end

30distance_factor=distance.to_f/trajectory_length

31p_x=x+(object.target_x-x)*distance_factor

32p_y=y+(object.target_y-y)*distance_factor

33[p_x,p_y]

34end

35

36private

37

38defarrived?

39x==object.target_x&&y==object.target_y

40end

41end

BulletPhysicsiswherethemostofBulletendedupat.ItdoesallthecalculationsandtriggersBullet#explodewhenready.Whenwewillbeimplementingcollisiondetection,theimplementationwillgosomewherehere.05-refactor/entities/components/bullet_graphics.rb

1classBulletGraphics<Component

2COLOR=Gosu::Color::BLACK

3

4defdraw(viewport)

5$window.draw_quad(x-2,y-2,COLOR,

6x+2,y-2,COLOR,

7x-2,y+2,COLOR,

8x+2,y+2,COLOR,

91)

10end

11

12end

AfterpullingawayBulletgraphicscode,itlooksverysmallandelegant.Wewillprobablyneverhavetoeditanythinghereagain.05-refactor/entities/components/bullet_sounds.rb

1classBulletSounds

2class<<self

3defplay

4sound.play

Page 83: Developing Games With Ruby: For those who write code for living

5end

6

7private

8

9defsound

10@@sound||=Gosu::Sample.new(

11$window,Utils.media_path('fire.mp3'))

12end

13end

14end

JustlikeExplosionSounds,BulletSoundsarestatelessandstatic.Wecouldmakeitjustlikearegularcomponent,butconsideritourlittleoptimization.

RefactoringTank

TimetotakealookatfreshlydecoupledTank:05-refactor/entities/tank.rb

1classTank<GameObject

2SHOOT_DELAY=500

3attr_accessor:x,:y,:throttle_down,:direction,:gun_angle,:sounds,:physics

4

5definitialize(object_pool,input)

6super(object_pool)

7@input=input

[email protected](self)

9@physics=TankPhysics.new(self,object_pool)

10@graphics=TankGraphics.new(self)

11@sounds=TankSounds.new(self)

12@direction=@gun_angle=0.0

13end

14

15defshoot(target_x,target_y)

16ifGosu.milliseconds-(@last_shot||0)>SHOOT_DELAY

17@last_shot=Gosu.milliseconds

18Bullet.new(object_pool,@x,@y,target_x,target_y).fire(100)

19end

20end

21end

Tankclasswasreducedover5times.WecouldgofurtherandextractGuncomponent,butfornowit’ssimpleenoughalready.Now,thecomponents.05-refactor/entities/components/tank_physics.rb

1classTankPhysics<Component

2attr_accessor:speed

3

4definitialize(game_object,object_pool)

5super(game_object)

6@object_pool=object_pool

7@map=object_pool.map

8game_object.x,[email protected]_spawn_point

9@speed=0.0

10end

11

12defcan_move_to?(x,y)

[email protected]_move_to?(x,y)

14end

15

16defmoving?

17@speed>0

18end

19

20defupdate

21ifobject.throttle_down

22accelerate

23else

Page 84: Developing Games With Ruby: For those who write code for living

24decelerate

25end

26if@speed>0

27new_x,new_y=x,y

28shift=Utils.adjust_speed(@speed)

[email protected]_i

30when0

31new_y-=shift

32when45

33new_x+=shift

34new_y-=shift

35when90

36new_x+=shift

37when135

38new_x+=shift

39new_y+=shift

40when180

41new_y+=shift

42when225

43new_y+=shift

44new_x-=shift

45when270

46new_x-=shift

47when315

48new_x-=shift

49new_y-=shift

50end

51ifcan_move_to?(new_x,new_y)

52object.x,object.y=new_x,new_y

53else

54object.sounds.collideif@speed>1

55@speed=0.0

56end

57end

58end

59

60private

61

62defaccelerate

63@speed+=0.08if@speed<5

64end

65

66defdecelerate

67@speed-=0.5if@speed>0

68@speed=0.0if@speed<0.01#damp

69end

70end

Whilewehadtoripplayerinputawayfromit’smovement,wegotourselvesabenefit-tanknowbothacceleratesanddecelerates.Whendirectionalbuttonsarenolongerpressed,tankkeepsmovinginlastdirection,butquicklydeceleratesandstops.AnotheradditionthatwouldhavebeenmoredifficulttoimplementonpreviousTankiscollisionsound.WhenTankabruptlystopsbyhittingsomething(fornowit’sonlywater),collisionsoundisplayed.Wewillhavetofixthat,becausemetalbangisnotappropriatewhenyoustopontheedgeofariver,butwenowdiditforthesakeofscience.05-refactor/entities/components/tank_graphics.rb

1classTankGraphics<Component

2definitialize(game_object)

3super(game_object)

4@body=units.frame('tank1_body.png')

5@shadow=units.frame('tank1_body_shadow.png')

6@gun=units.frame('tank1_dualgun.png')

7end

8

9defdraw(viewport)

[email protected]_rot(x-1,y-1,0,object.direction)

[email protected]_rot(x,y,1,object.direction)

[email protected]_rot(x,y,2,object.gun_angle)

13end

Page 85: Developing Games With Ruby: For those who write code for living

14

15private

16

17defunits

18@@units=Gosu::TexturePacker.load_json(

19$window,Utils.media_path('ground_units.json'),:precise)

20end

21end

Again,graphicsareneatlypackedandseparatedfromeverythingelse.Eventuallyweshouldoptimizedrawtotakeviewportintoconsideration,butit’sgoodenoughfornow,especiallywhenwehaveonlyonetankinthegame.05-refactor/entities/components/tank_sounds.rb

1classTankSounds<Component

2defupdate

3ifobject.physics.moving?

4if@driving&&@driving.paused?

[email protected]

[email protected]?

7@driving=driving_sound.play(1,1,true)

8end

9else

10if@driving&&@driving.playing?

[email protected]

12end

13end

14end

15

16defcollide

17crash_sound.play(1,0.25,false)

18end

19

20private

21

22defdriving_sound

23@@driving_sound||=Gosu::Sample.new(

24$window,Utils.media_path('tank_driving.mp3'))

25end

26

27defcrash_sound

28@@crash_sound||=Gosu::Sample.new(

29$window,Utils.media_path('crash.ogg'))

30end

31end

UnlikeExplosionandBullet,Tanksoundsarestateful.Wehavetokeeptrackoftank_driving.mp3,whichisnolongerGosu::Song,butGosu::Sample,likeitshouldhavebeen.

WhenGosu::Sample#playisinvoked,Gosu::SampleInstanceisreturned,andwehavefullcontroloverit.Nowwearereadytoplaysoundsformorethanonetankatonce.05-refactor/entities/components/player_input.rb

1classPlayerInput<Component

2definitialize(camera)

3super(nil)

4@camera=camera

5end

6

7defcontrol(obj)

8self.object=obj

9end

10

11defupdate

12d_x,[email protected]_delta_on_screen

13atan=Math.atan2(($window.width/2)-d_x-$window.mouse_x,

Page 86: Developing Games With Ruby: For those who write code for living

14($window.height/2)-d_y-$window.mouse_y)

15object.gun_angle=-atan*180/Math::PI

16motion_buttons=[Gosu::KbW,Gosu::KbS,Gosu::KbA,Gosu::KbD]

17

18ifany_button_down?(*motion_buttons)

19object.throttle_down=true

20object.direction=change_angle(object.direction,*motion_buttons)

21else

22object.throttle_down=false

23end

24

25ifUtils.button_down?(Gosu::MsLeft)

26object.shoot(*@camera.mouse_coords)

27end

28end

29

30private

31

32defany_button_down?(*buttons)

33buttons.eachdo|b|

34returntrueifUtils.button_down?(b)

35end

36false

37end

38

39defchange_angle(previous_angle,up,down,right,left)

40ifUtils.button_down?(up)

41angle=0.0

42angle+=45.0ifUtils.button_down?(left)

43angle-=45.0ifUtils.button_down?(right)

44elsifUtils.button_down?(down)

45angle=180.0

46angle-=45.0ifUtils.button_down?(left)

47angle+=45.0ifUtils.button_down?(right)

48elsifUtils.button_down?(left)

49angle=90.0

50angle+=45.0ifUtils.button_down?(up)

51angle-=45.0ifUtils.button_down?(down)

52elsifUtils.button_down?(right)

53angle=270.0

54angle-=45.0ifUtils.button_down?(up)

55angle+=45.0ifUtils.button_down?(down)

56end

57angle=(angle+360)%360ifangle&&angle<0

58(angle||previous_angle)

59end

60end

WefinallycometoaplacewherekeyboardandmouseinputishandledandconvertedtoTankcommands.WecouldhaveusedCommandpatterntodecoupleeverythingevenfurther.

RefactoringPlayState05-refactor/game_states/play_state.rb

1require'ruby-prof'ifENV['ENABLE_PROFILING']

2classPlayState<GameState

3attr_accessor:update_interval

4

5definitialize

6@map=Map.new

7@camera=Camera.new

8@object_pool=ObjectPool.new(@map)

9@tank=Tank.new(@object_pool,PlayerInput.new(@camera))

[email protected]=@tank

11end

12

13defenter

14RubyProf.startifENV['ENABLE_PROFILING']

15end

16

17defleave

18ifENV['ENABLE_PROFILING']

Page 87: Developing Games With Ruby: For those who write code for living

19result=RubyProf.stop

20printer=RubyProf::FlatPrinter.new(result)

21printer.print(STDOUT)

22end

23end

24

25defupdate

26@object_pool.objects.map(&:update)

27@object_pool.objects.reject!(&:removable?)

[email protected]

29update_caption

30end

31

32defdraw

[email protected]

[email protected]

35off_x=$window.width/2-cam_x

36off_y=$window.height/2-cam_y

[email protected]

38$window.translate(off_x,off_y)do

[email protected]

40$window.scale(zoom,zoom,cam_x,cam_y)do

[email protected](viewport)

42@object_pool.objects.map{|o|o.draw(viewport)}

43end

44end

[email protected]_crosshair

46end

47

48defbutton_down(id)

49ifid==Gosu::KbQ

50leave

51$window.close

52end

53ifid==Gosu::KbEscape

54GameState.switch(MenuState.instance)

55end

56end

57

58private

59

60defupdate_caption

61now=Gosu.milliseconds

62ifnow-(@caption_updated_at||0)>1000

63$window.caption='TanksPrototype.'<<

64"[FPS:#{Gosu.fps}."<<

65"Tank@#{@tank.x.round}:#{@tank.y.round}]"

66@caption_updated_at=now

67end

68end

69end

ImplementationofPlayStateisnowalsoalittlesimpler.Itdoesn’tupdate@[email protected],itusesObjectPoolanddoesallobjectoperationsinbulk.

OtherImprovements05-refactor/main.rb

1#!/usr/bin/envruby

2

3require'gosu'

4

5root_dir=File.dirname(__FILE__)

6require_pattern=File.join(root_dir,'**/*.rb')

7@failed=[]

8

9#Dynamicallyrequireeverything

10Dir.glob(require_pattern).eachdo|f|

11nextiff.end_with?('/main.rb')

12begin

Page 88: Developing Games With Ruby: For those who write code for living

13require_relativef.gsub("#{root_dir}/",'')

14rescue

15#Mayfailifparentclassnotrequiredyet

16@failed<<f

17end

18end

19

20#Retryunresolvedrequires

[email protected]|f|

22require_relativef.gsub("#{root_dir}/",'')

23end

24

25$window=GameWindow.new

26GameState.switch(MenuState.instance)

27$window.show

Finally,wemadesomeimprovementstomain.rb-itnowrecursivelyrequiresall*.rbfileswithinsamedirectory,sowedon’thavetoworryaboutitinotherclasses.05-refactor/utils.rb

1moduleUtils

2defself.media_path(file)

3File.join(File.dirname(File.dirname(

4__FILE__)),'media',file)

5end

6

7defself.track_update_interval

8now=Gosu.milliseconds

9@update_interval=(now-(@last_update||=0)).to_f

10@last_update=now

11end

12

13defself.update_interval

14@update_interval||=$window.update_interval

15end

16

17defself.adjust_speed(speed)

18speed*update_interval/33.33

19end

20

21defself.button_down?(button)

22@buttons||={}

23now=Gosu.milliseconds

24now=now-(now%150)

25if$window.button_down?(button)

26@buttons[button]=now

27true

28elsif@buttons[button]

29ifnow==@buttons[button]

30true

31else

[email protected](button)

33false

34end

35end

36end

37end

AnothernotablechangeisrenamingGamemoduleintoUtils.Thenamefinallymakesmoresense,IhavenoideawhyIpututilitymethodsintoGamemoduleinthefirstplace.Also,Utilsreceivedbutton_down?method,thatsolvestheissueofchangingtankdirectionwhenbuttonisimmediatelyreleased.Itmadeverydifficulttostopatdiagonalangle,becausewhenyoudepressedtwobuttons,16mswasenoughforGosutothink“hereleasedW,andSisstillpressed,solet’schangedirectiontoS”.Utils#button_down?givesasoft150mswindowtosynchronizebuttonrelease.Nowcontrolsfeelmorenatural.

Page 89: Developing Games With Ruby: For those who write code for living

SimulatingPhysics

Tomakethegamemorerealistic,wewillspicethingsupwithsomephysics.Thisisthefeaturesetwearegoingtoimplement:

1. Collisiondetection.Tankwillbumpintootherobjects-stationarytanks.Bulletswillnotgothroughthemeither.

2. Terraineffects.Tankwillgofastongrass,sloweronsand.

AddingEnemyObjectsIt’sboringtoplayalone,sowewillmakeaquickchangeandspawnsomestationarytanksthatwillbedeployedrandomlyaroundthemap.Theywillbestationaryinthebeginning,butwewillstillneedadummyAIclasstoreplacePlayerInput:06-physics/entities/components/ai_input.rb

1classAiInput<Component

2defcontrol(obj)

3self.object=obj

4end

5end

AquickanddirtywaytospawnsometankswouldbewheninitializingPlayState:classPlayState<GameState

#...

definitialize

@map=Map.new

@camera=Camera.new

@object_pool=ObjectPool.new(@map)

@tank=Tank.new(@object_pool,PlayerInput.new(@camera))

@camera.target=@tank

#...

50.timesdo

Tank.new(@object_pool,AiInput.new)

end

end

#...

end

Andunlesswewantallstationarytanksfacesamedirection,wewillrandomizeit:classTank<GameObject

#...

definitialize(object_pool,input)

#...

@direction=rand(0..7)*45

@gun_angle=rand(0..360)

end

#...

end

Fireupthegame,andwanderaroundfrozentanks.Youcanpassthroughthemasiftheywereghosts,butwewillfixthatinamoment.

Page 90: Developing Games With Ruby: For those who write code for living

Braindeadenemies

AddingBoundingBoxesAndDetectingCollisionsWewantourcollisiondetectiontobepixelperfect,thatmeansweneedtohaveaboundingboxandcheckcolisionsagainstit.Getreadyforsomemath!

First,weneedtofindacorrectwaytoconstructaboundingbox.Tankhasit’sbodyimage,solet’sseehowit’sboundarieslooklike.WewilladdsomecodetoTankGraphicscomponenttoseeit:classTankGraphics<Component

defdraw(viewport)

#...

draw_bounding_box

end

defdraw_bounding_box

$window.rotate(object.direction,x,y)do

[email protected]

[email protected]

$window.draw_quad(

x-w/2,y-h/2,Gosu::Color::RED,

x+w/2,y-h/2,Gosu::Color::RED,

x+w/2,y+h/2,Gosu::Color::RED,

x-w/2,y+h/2,Gosu::Color::RED,

100)

end

end

Page 91: Developing Games With Ruby: For those who write code for living

#...

end

Resultisprettygood,wehavetankshapedbox,sowewillbeusingbodyimagedimensionstodetermineourboundingboxcorners:

Page 92: Developing Games With Ruby: For those who write code for living

Tank’sboundingboxvisualized

Thereisoneproblemherethough.Gosu::Window#rotatedoestherotationmathforus,andweneedtoperformthesecalculationsonourown.Wehavefourpointsthatwewanttorotatearoundacenterpoint.It’snotverydifficulttofindhowtodothis.HereisaRubymethodforyou:moduleUtils

#...

defself.rotate(angle,around_x,around_y,*points)

result=[]

points.each_slice(2)do|x,y|

r_x=Math.cos(angle)*(x-around_x)-

Math.sin(angle)*(y-around_y)+around_x

r_y=Math.sin(angle)*(x-around_x)+

Math.cos(angle)*(y-around_y)+around_y

result<<r_x

result<<r_y

end

result

end

#...

end

Wecannowcalculateedgesofourboundingbox,butweneedonemorefunctionwhichtellsifpointisinsideapolygon.Thisproblemhasbeensolvedmilliontimesbefore,sojustpoketheinternetforitanddrinkfromtheinformationfirehoseuntilyouunderstandhowtodothis.

Page 93: Developing Games With Ruby: For those who write code for living

Ifyouwasn’tfamiliarwiththetermyet,bynowyoushoulddiscoverwhatvertexis.Ingeometry,avertex(pluralvertices)isaspecialkindofpointthatdescribesthecornersorintersectionsofgeometricshapes.

Here’swhatIendedupwriting:moduleUtils

#...

#http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html

defself.point_in_poly(testx,testy,*poly)

nvert=poly.size/2#Numberofverticesinpoly

vertx=[]

verty=[]

poly.each_slice(2)do|x,y|

vertx<<x

verty<<y

end

inside=false

j=nvert-1

(0..nvert-1).eachdo|i|

if(((verty[i]>testy)!=(verty[j]>testy))&&

(testx<(vertx[j]-vertx[i])*(testy-verty[i])/

(verty[j]-verty[i])+vertx[i]))

inside=!inside

end

j=i

end

inside

end

#...

ItisJordancurvetheoremreimplementedinRuby.Looksugly,butitactuallyworks,andisprettyfasttoo.

Also,thisworksonmoresophisticatedpolygons,andourtankisshapedmorelikeanHratherthanarectangle,sowecoulddefineapixelperfectpolygon.Somepenandpaperwillhelp.classTankPhysics<Component

#...

#TankboxlookslikeH.Vertices:

#1256

#34

#

#109

#121187

defbox

w=box_width/2-1

h=box_height/2-1

tw=8#trackwidth

fd=8#frontdepth

rd=6#reardepth

Utils.rotate(object.direction,x,y,

x+w,y+h,#1

x+w-tw,y+h,#2

x+w-tw,y+h-fd,#3

x-w+tw,y+h-fd,#4

x-w+tw,y+h,#5

x-w,y+h,#6

x-w,y-h,#7

x-w+tw,y-h,#8

x-w+tw,y-h+rd,#9

x+w-tw,y-h+rd,#10

x+w-tw,y-h,#11

x+w,y-h,#12

)

end

Page 94: Developing Games With Ruby: For those who write code for living

#...

end

Tovisuallyseeit,wewillimproveourdraw_bounding_boxmethod:classTankGraphics<Component

#...

DEBUG_COLORS=[

Gosu::Color::RED,

Gosu::Color::BLUE,

Gosu::Color::YELLOW,

Gosu::Color::WHITE

]

#...

defdraw_bounding_box

i=0

object.box.each_slice(2)do|x,y|

color=DEBUG_COLORS[i]

$window.draw_triangle(

x-3,y-3,color,

x,y,color,

x+3,y-3,color,

100)

i=(i+1)%4

end

end

#...

Nowwecanvisuallytestboundingboxedgesandseethattheyactuallyarewheretheybelong.

Page 95: Developing Games With Ruby: For those who write code for living

Highprecisionboundingboxes

TimetopimpourTankPhysicstodetectthosecollisions.Whileouralgorithmisprettyfast,itdoesn’tmakesensetocheckcollisionsforobjectsthatareprettyfarapart.ThisiswhyweneedourObjectPooltoknowhowtoqueryobjectsincloseproximity.classObjectPool

#...

defnearby(object,max_distance)

@objects.selectdo|obj|

distance=Utils.distance_between(

obj.x,obj.y,object.x,object.y)

obj!=object&&distance<max_distance

end

end

end

BacktoTankPhysics:classTankPhysics<Component

#...

defcan_move_to?(x,y)

old_x,old_y=object.x,object.y

object.x=x

object.y=y

[email protected]_move_to?(x,y)

@object_pool.nearby(object,100).eachdo|obj|

ifcollides_with_poly?(obj.box)

#Allowtogetunstuck

old_distance=Utils.distance_between(

obj.x,obj.y,old_x,old_y)

new_distance=Utils.distance_between(

Page 96: Developing Games With Ruby: For those who write code for living

obj.x,obj.y,x,y)

returnfalseifnew_distance<old_distance

end

end

true

ensure

object.x=old_x

object.y=old_y

end

#...

private

defcollides_with_poly?(poly)

ifpoly

poly.each_slice(2)do|x,y|

returntrueifUtils.point_in_poly(x,y,*box)

end

box.each_slice(2)do|x,y|

returntrueifUtils.point_in_poly(x,y,*poly)

end

end

false

end

#...

end

It’sprobablynotthemostelegantsolutionyoucouldcomeupwith,butcan_move_to?temporarilychangesTanklocationtomakeacollisiontest,andthenrevertsoldcoordinatesjustbeforereturningtheresult.Nowourtanksstopwithbangingsoundwhentheyhiteachother.

Page 97: Developing Games With Ruby: For those who write code for living

Tankscolliding

CatchingBulletsRightnowbulletsflyrightthroughourtanks,andwewantthemtocollide.It’saprettysimplechange,whichmostlyaffectsBulletPhysicsclass:#06-physics/entities/components/bullet_physics.rb

classBulletPhysics<Component

#...

defupdate

#...

check_hit

object.explodeifarrived?

end

#...

private

defcheck_hit

@object_pool.nearby(object,50).eachdo|obj|

nextifobj==object.source#Don'thitsourcetank

ifUtils.point_in_poly(x,y,*obj.box)

object.target_x=x

object.target_y=y

return

end

end

end

#...

end

Page 98: Developing Games With Ruby: For those who write code for living

Nowbulletsfinallyhit,butdon’tdoanydamageyet.Wewillcomebacktothatsoon.

Page 99: Developing Games With Ruby: For those who write code for living

Bullethittingenemytank

ImplementingTurnSpeedPenaltiesTankscannotmaketurnsandgointoreverseatfullspeedwhilekeepingit’sinertia,right?Itiseasytoimplement.Sinceit’srelatedtophysics,wewilldelegatechangingTank’s@directiontoourTankPhysicsclass:#06-physics/entities/components/player_input.rb

classPlayerInput<Component

#...

defupdate

#...

motion_buttons=[Gosu::KbW,Gosu::KbS,Gosu::KbA,Gosu::KbD]

ifany_button_down?(*motion_buttons)

object.throttle_down=true

object.physics.change_direction(

change_angle(object.direction,*motion_buttons))

else

object.throttle_down=false

end

#...

end

#...

end

#06-physics/entities/components/tank_physics.rb

classTankPhysics<Component

#...

Page 100: Developing Games With Ruby: For those who write code for living

defchange_direction(new_direction)

change=(new_direction-object.direction+360)%360

change=360-changeifchange>180

ifchange>90

@speed=0

elsifchange>45

@speed*=0.33

elsifchange>0

@speed*=0.66

end

object.direction=new_direction

end

#...

end

ImplementingTerrainSpeedPenaltiesNow,let’sseehowcanwemaketerraininfluenceourmovement.ItsoundsreasonableforTankPhysicstoconsultwithMapaboutspeedpenaltyofcurrenttile:#06-physics/entities/map.rb

classMap

#...

defmovement_penalty(x,y)

tile=tile_at(x,y)

casetile

when@sand

0.33

else

0

end

end

#...

end

#06-physics/entities/components/tank_physics.rb

classTankPhysics<Component

#...

defupdate

#...

speed=apply_movement_penalty(@speed)

shift=Utils.adjust_speed(speed)

#...

end

#...

private

defapply_movement_penalty(speed)

speed*([email protected]_penalty(x,y))

end

#...

end

Thismakesalltanksmove33%sloweronsand.

Page 101: Developing Games With Ruby: For those who write code for living

ImplementingHealthAndDamage

Iknowyouhavebeenwaitingforthis.Wewillbeimplementinghealthsystemandmostimportantly,damage.Soowewillbereadytoblowthingsup.

Toimplementthis,weneedto:

1. AddTankHealthcomponent.Startwith100health.2. Rendertankhealthnexttotankitself.3. Inflictdamagetotankwhenitisinexplosionzone4. Renderdifferentspritefordeadtank.5. Cutoffplayerinputwhentankisdead.

AddingHealthComponentIfwedidn’thaveComponentsysteminplace,itwouldbewaymoredifficult.Nowwejustkickinanewclass:07-damage/entities/components/tank_health.rb

1classTankHealth<Component

2attr_accessor:health

3

4definitialize(object,object_pool)

5super(object)

6@object_pool=object_pool

7@health=100

8@health_updated=true

9@last_damage=Gosu.milliseconds

10end

11

12defupdate

13update_image

14end

15

16defupdate_image

17if@health_updated

18ifdead?

19text='✝'20font_size=25

21else

[email protected]_s

23font_size=18

24end

25@image=Gosu::Image.from_text(

26$window,text,

27Gosu.default_font_name,font_size)

28@health_updated=false

29end

30end

31

32defdead?

33@health<1

34end

35

36definflict_damage(amount)

37if@health>0

38@health_updated=true

39@health=[@health-amount.to_i,0].max

40if@health<1

Page 102: Developing Games With Ruby: For those who write code for living

41Explosion.new(@object_pool,x,y)

42end

43end

44end

45

46defdraw(viewport)

[email protected](

[email protected]/2,

49y-object.graphics.height/2-

[email protected],100)

51end

52end

Ithooksitselfintothegamerightaway,afterweinitializeitinTankclass:classTank<GameObject

attr_accessor:health

#...

definitialize(object_pool,input)

#...

@health=TankHealth.new(self,object_pool)

#..

end

#..

end

InflictingDamageWithBulletsTherearetwowaystoinflictdamage-directlyandindirectly.Whenbullethitsenemytank(collideswithtankboundingbox),weshouldinflictdirectdamage.ItcanbedoneinBulletPhysics#check_hitmethodthatwealreadyhad:classBulletPhysics<Component

#...

defcheck_hit

@object_pool.nearby(object,50).eachdo|obj|

nextifobj==object.source#Don'thitsourcetank

ifUtils.point_in_poly(x,y,*obj.box)

#Directhit-extradamage

obj.health.inflict_damage(20)

object.target_x=x

object.target_y=y

return

end

end

end

#...

end

Finally,Explosionitselfshouldinflictadditionaldamagetoanythingthatarenearby.Theeffectwillbediminishinganditwillbedeterminedbyobjectdistance.classExplosion<GameObject

#...

definitialize(object_pool,x,y)

#...

inflict_damage

end

private

definflict_damage

object_pool.nearby(self,100).eachdo|obj|

ifobj.class==Tank

obj.health.inflict_damage(

Math.sqrt(3*100-Utils.distance_between(

obj.x,obj.y,x,y)))

end

end

end

end

Page 103: Developing Games With Ruby: For those who write code for living

Thisisit,wearereadytodealdamage.Butwewanttoseeifweactuallykilledsomebody,soTankGraphicsshouldbeawareofhealthandshoulddrawdifferentsetofspriteswhentankisdead.HereiswhatweneedtochangeinourcurrentTankGraphicstoachievetheresult:classTankGraphics<Component

#...

definitialize(game_object)

super(game_object)

@body_normal=units.frame('tank1_body.png')

@shadow_normal=units.frame('tank1_body_shadow.png')

@gun_normal=units.frame('tank1_dualgun.png')

@body_dead=units.frame('tank1_body_destroyed.png')

@shadow_dead=units.frame('tank1_body_destroyed_shadow.png')

@gun_dead=nil

end

defupdate

ifobject.health.dead?

@body=@body_dead

@gun=@gun_dead

@shadow=@shadow_dead

else

@body=@body_normal

@gun=@gun_normal

@shadow=@shadow_normal

end

end

defdraw(viewport)

@shadow.draw_rot(x-1,y-1,0,object.direction)

@body.draw_rot(x,y,1,object.direction)

@gun.draw_rot(x,y,2,object.gun_angle)if@gun

end

#...

end

Nowwecanblowthemupandenjoytheview:

Page 104: Developing Games With Ruby: For those who write code for living

Targetpractice

Butwhatifweblowourselvesupbyshootingnearby?Wewouldstillbeabletomovearound.Tofixthis,wewillsimplycutoutplayerinputwhenwearedead:classPlayerInput<Component

#...

defupdate

returnifobject.health.dead?

#...

end

#...

end

Andtopreventtankfromthrottlingforeverifthepedalwasdownbeforeitgotkilled:classTankPhysics<Component

#...

defupdate

ifobject.throttle_down&&!object.health.dead?

accelerate

else

decelerate

end

#...

end

#...

end

That’sit.Allweneedrightnowissomeresistancefromthosebraindeadenemies.Wewillsparksomelifeintotheminnextchapter.

Page 105: Developing Games With Ruby: For those who write code for living

CreatingArtificialIntelligence

ArtificialIntelligenceisasubjectsovastthatwewillbarelyscratchthesurface.AIinVideoGamesisusuallyheavilysimplifiedandthereforeeasiertoimplement.

ThereisthiswonderfulseriesofarticlescalledDesigningArtificialIntelligenceforGamesthatIhighlyrecommendreadingtogetafeelinghowgameAIshouldbedone.Wewillbecontinuingourworkontopofwhatwealreadyhave,examplecodeforthischapterwillbein08-ai.

DesigningAIUsingFiniteStateMachineNonplayertanksinourgamewillbelonerangers,huntingeverythingthatmoveswhiletryingtosurvive.WewilluseFiniteStateMachinetoimplementtankbehavior.

First,weneedtothink“whatwouldatankdo?”Howaboutthisscenario:

1. Tankwandersaround,mindingit’sownbusiness.2. Tankencountersanothertank.Itthenstartsdoingevasivemovesandtrieshittingthe

enemy.3. Enemytooksomedamageandstarteddrivingaway.Tankstartschasingtheenemy

tryingtofinishit.4. Anothertankappearsandfiresacoupleofaccurateshots,dealingseriousdamage.

Ourtankstartsrunningaway,becauseifitkeptreceivingdamageatsuchrate,itwoulddieverysoon.

5. Tankkeepsfleeingandlookingforsafetyuntilitgetscorneredortheopponentlooksdamagedtoo.Thentankgoesintoit’sfinalbattle.

WecannowdrawaFiniteStateMachineusingthisscenario:

VigilanteTankFSM

Page 106: Developing Games With Ruby: For those who write code for living

Ifyouareonapathtobecomeagamedeveloper,FSMshouldnotstandforFlyingSpaghettiMonsterforyouanymore.

ImplementingAIVisionTomakeopponentsrealistic,wehavetogivethemsenses.Let’screateaclassforthat:08-ai/entities/components/ai/vision.rb

1classAiVision

2CACHE_TIMEOUT=500

3attr_reader:in_sight

4

5definitialize(viewer,object_pool,distance)

6@viewer=viewer

7@object_pool=object_pool

8@distance=distance

9end

10

11defupdate

12@in_sight=@object_pool.nearby(@viewer,@distance)

13end

14

15defclosest_tank

16now=Gosu.milliseconds

17@closest_tank=nil

18ifnow-(@cache_updated_at||=0)>CACHE_TIMEOUT

19@closest_tank=nil

20@cache_updated_at=now

21end

22@closest_tank||=find_closest_tank

23end

24

25private

26

27deffind_closest_tank

28@in_sight.selectdo|o|

29o.class==Tank&&!o.health.dead?

30end.sortdo|a,b|

31x,[email protected],@viewer.y

32d1=Utils.distance_between(x,y,a.x,a.y)

33d2=Utils.distance_between(x,y,b.x,b.y)

34d1<=>d2

35end.first

36end

37end

ItusesObjectPooltoputnearbyobjectsinsight,andgetsashorttermfocusononeclosesttank.Closesttankiscachedfor500millisecondsfortworeasons:

1. Performance.UncachedversionwoulddoArray#selectandArray#sort60timespersecond,nowitwilldo2times.

2. Focus.Whenyouchooseatarget,youshouldkeepitalittlelonger.Thisshouldalsoavoid“jitters”,whentankwouldshakebetweentwonearbytargetsthatarewithinsamedistance.

ControllingTankGunAfterwemadeAiVision,wecannowuseittoautomaticallyaimandshootatclosesttank.Itshouldworklikethis:

1. Everyinstanceofthegunhasit’sownuniquecombinationofspeed,accuracyandaggressiveness.

Page 107: Developing Games With Ruby: For those who write code for living

2. Gunwillautomaticallytargetclosesttankinsight.3. Ifnoothertankisinsight,gunwilltargetinsamedirectionastank’sbody.4. Ifothertankisaimedatandwithinshootingdistance,gunwillmakeadecisiononce

inawhilewhetheritshouldshootornot,basedonaggressivenesslevel.Aggressivetankswillbetriggerhappyallthetime,whilelessaggressiveoneswillmakesmallrandompausesbetweenshots.

5. Gunwillhavea“desired”anglethatitwillbeautomaticallyadjustingto,accordingtoit’sspeed.

Hereistheimplementation:08-ai/entities/components/ai/gun.rb

1classAiGun

2DECISION_DELAY=1000

3attr_reader:target,:desired_gun_angle

4

5definitialize(object,vision)

6@object=object

7@vision=vision

8@desired_gun_angle=rand(0..360)

9@retarget_speed=rand(1..5)

10@accuracy=rand(0..10)

11@aggressiveness=rand(1..5)

12end

13

14defadjust_angle

15adjust_desired_angle

16adjust_gun_angle

17end

18

19defupdate

[email protected]_sight.any?

[email protected]_tank!=@target

22change_target(@vision.closest_tank)

23end

24else

25@target=nil

26end

27

28if@target

29if(0..10-rand(0..@accuracy)).include?(

30(@[email protected]_angle).abs.round)

31distance=distance_to_target

32ifdistance-50<=BulletPhysics::MAX_DIST

33target_x,target_y=Utils.point_at_distance(

[email protected],@object.y,@object.gun_angle,

35distance+10-rand(0..@accuracy))

36ifcan_make_new_decision?&&@object.can_shoot?&&

37should_shoot?

[email protected](target_x,target_y)

39end

40end

41end

42end

43end

44

45defdistance_to_target

46Utils.distance_between(

[email protected],@object.y,@target.x,@target.y)

48end

49

50

51defshould_shoot?

52rand*@aggressiveness>0.5

53end

54

55defcan_make_new_decision?

56now=Gosu.milliseconds

Page 108: Developing Games With Ruby: For those who write code for living

57ifnow-(@last_decision||=0)>DECISION_DELAY

58@last_decision=now

59true

60end

61end

62

63defadjust_desired_angle

64@desired_gun_angle=if@target

65Utils.angle_between(

[email protected],@object.y,@target.x,@target.y)

67else

[email protected]

69end

70end

71

72defchange_target(new_target)

73@target=new_target

74adjust_desired_angle

75end

76

77defadjust_gun_angle

[email protected]_angle

79desired=@desired_gun_angle

80ifactual>desired

81ifactual-desired>180#0->360fix

[email protected]_angle=(actual+@retarget_speed)%360

[email protected]_angle<desired

[email protected]_angle=desired#damp

85end

86else

[email protected]_angle=[actual-@retarget_speed,desired].max

88end

89elsifactual<desired

90ifdesired-actual>180#360->0fix

[email protected]_angle=(360+actual-@retarget_speed)%360

[email protected]_angle>desired

[email protected]_angle=desired#damp

94end

95else

[email protected]_angle=[actual+@retarget_speed,desired].min

97end

98end

99end

100end

Thereissomemathinvolved,butitisprettystraightforward.Weneedtofindoutananglebetweentwopoints,toknowwhereourgunshouldpoint,andtheotherthingweneediscoordinatesofpointwhichisinsomedistanceawayfromsourceatgivenangle.Herearethosefunctions:moduleUtils

#...

defself.angle_between(x,y,target_x,target_y)

dx=target_x-x

dy=target_y-y

(180-Math.atan2(dx,dy)*180/Math::PI)+360%360

end

defself.point_at_distance(source_x,source_y,angle,distance)

angle=(90-angle)*Math::PI/180

x=source_x+Math.cos(angle)*distance

y=source_y-Math.sin(angle)*distance

[x,y]

end

#...

end

ImplementingAIInputAtthispointourtankscanalreadydefendthemselves,eventhroughmotionisnotyetimplemented.Let’swireeverythingwehaveinAiInputclassthatwehadpreparedearlier.

Page 109: Developing Games With Ruby: For those who write code for living

WewillneedablankTankMotionFSMclasswith3argumentinitializerandemptyupdate,on_collision(with)andon_damage(amount)methodsforittowork:08-ai/entities/components/ai_input.rb

1classAiInput<Component

2UPDATE_RATE=200#ms

3

4definitialize(object_pool)

5@object_pool=object_pool

6super(nil)

7@last_update=Gosu.milliseconds

8end

9

10defcontrol(obj)

11self.object=obj

12@vision=AiVision.new(obj,@object_pool,

13rand(700..1200))

14@gun=AiGun.new(obj,@vision)

15@motion=TankMotionFSM.new(obj,@vision,@gun)

16end

17

18defon_collision(with)

[email protected]_collision(with)

20end

21

22defon_damage(amount)

[email protected]_damage(amount)

24end

25

26defupdate

27returnifobject.health.dead?

[email protected]_angle

29now=Gosu.milliseconds

30returnifnow-@last_update<UPDATE_RATE

31@last_update=now

[email protected]

[email protected]

[email protected]

35end

36end

Itadjustgunangleallthetime,butdoesupdatesatUPDATE_RATEtosaveCPUpower.AIisusuallyoneofthemostCPUintensivethingsingames,soit’sacommonpracticetoexecuteitlessoften.Refreshingenemybrains5persecondisenoughtomakethemdeadly.

MakesureyouspawnsomeAIcontrolledtanksinPlayStateandtrykillingthemnow.Ibettheywilleventuallygetyouevenwhilestandingstill.YoucanalsomaketanksspawnbelowmousecursorwhenyoupressTkey:classPlayState<GameState

#...

definitialize

#...

10.timesdo|i|

Tank.new(@object_pool,AiInput.new(@object_pool))

end

end

#...

defbutton_down(id)

#...

ifid==Gosu::KbT

t=Tank.new(@object_pool,

AiInput.new(@object_pool))

t.x,[email protected]_coords

end

#...

end

Page 110: Developing Games With Ruby: For those who write code for living

#...

end

ImplementingTankMotionStatesThisistheplacewherewewillneedFiniteStateMachinetogetthingsright.Wewilldesignitlikethis:

1. TankMotionFSMwilldecidewhichmotionstatetankshouldbein,consideringvariousparameters,e.g.existenceoftargetorlackthereof,health,etc.

2. TherewillbeTankMotionStatebaseclassthatwilloffercommonmethodslikedrive,waitandon_collision.

3. Concretemotionclasseswillimplementupdate,change_directionandothermethods,thatwillfiddlewithTank#throttle_downandTank#directiontomakeitmoveandturn.

WewillbeginwithTankMotionState:08-ai/entities/components/ai/tank_motion_state.rb

1classTankMotionState

2definitialize(object,vision)

3@object=object

4@vision=vision

5end

6

7defenter

8#Overrideifnecessary

9end

10

11defchange_direction

12#Override

13end

14

15defwait_time

16#Overrideandreturnanumber

17end

18

19defdrive_time

20#Overrideandreturnanumber

21end

22

23defturn_time

24#Overrideandreturnanumber

25end

26

27defupdate

28#Override

29end

30

31defwait

32@sub_state=:waiting

33@started_waiting=Gosu.milliseconds

34@will_wait_for=wait_time

[email protected]_down=false

36end

37

38defdrive

39@sub_state=:driving

40@started_driving=Gosu.milliseconds

41@will_drive_for=drive_time

[email protected]_down=true

43end

44

45defshould_change_direction?

46returntrueunless@changed_direction_at

47Gosu.milliseconds-@changed_direction_at>

Page 111: Developing Games With Ruby: For those who write code for living

48@will_keep_direction_for

49end

50

51defsubstate_expired?

52now=Gosu.milliseconds

53case@sub_state

54when:waiting

55trueifnow-@started_waiting>@will_wait_for

56when:driving

57trueifnow-@started_driving>@will_drive_for

58else

59true

60end

61end

62

63defon_collision(with)

64change=caserand(0..100)

65when0..30

66-90

67when30..60

6890

69when60..70

70135

71when80..90

72-135

73else

74180

75end

[email protected]_direction(

[email protected]+change)

78end

79end

Nothingextraordinaryhere,andweneedaconcreteimplementationtogetafeelinghowitwouldwork,thereforelet’sexamineTankRoamingState.Itwillbethedefaultstatewhichtankwouldbeiniftherewerenoenemiesaround.

TankRoamingState08-ai/entities/components/ai/tank_roaming_state.rb

1classTankRoamingState<TankMotionState

2definitialize(object,vision)

3super

4@object=object

5@vision=vision

6end

7

8defupdate

9change_directionifshould_change_direction?

10ifsubstate_expired?

11rand>0.3?drive:wait

12end

13end

14

15defchange_direction

16change=caserand(0..100)

17when0..30

18-45

19when30..60

2045

21when60..70

2290

23when80..90

24-90

25else

260

27end

28ifchange!=0

[email protected]_direction(

[email protected]+change)

31end

Page 112: Developing Games With Ruby: For those who write code for living

32@changed_direction_at=Gosu.milliseconds

33@will_keep_direction_for=turn_time

34end

35

36defwait_time

37rand(500..2000)

38end

39

40defdrive_time

41rand(1000..5000)

42end

43

44defturn_time

45rand(2000..5000)

46end

47end

Thelogichere:

1. Tankwillrandomlychangedirectioneveryturn_timeinterval,whichisbetween2and5seconds.

2. Tankwillchoosetodrive(80%chance)ortostandstill(20%chance).3. Iftankchosetodrive,itwillkeepdrivingfordrive_time,whichisbetween1and5

seconds.4. Samegoeswithwaiting,butwait_time(0.5-2seconds)willbeusedforduration.5. Directionchangesanddriving/waitingareindependent.

Thiswillmakeanimpressionthatourtankisdrivingaroundlookingforenemies.

TankFightingState

Whentankfinallyseesanopponent,itwillstartfighting.Fightingmotionshouldbemoreenergeticthanroaming,wewillneedasharpersetofchoicesinchange_directionamongotherthings.08-ai/entities/components/ai/tank_fighting_state.rb

1classTankFightingState<TankMotionState

2definitialize(object,vision)

3super

4@object=object

5@vision=vision

6end

7

8defupdate

9change_directionifshould_change_direction?

10ifsubstate_expired?

11rand>0.2?drive:wait

12end

13end

14

15defchange_direction

16change=caserand(0..100)

17when0..20

18-45

19when20..40

2045

21when40..60

2290

23when60..80

24-90

25when80..90

26135

27when90..100

28-135

29end

Page 113: Developing Games With Ruby: For those who write code for living

[email protected]_direction(

[email protected]+change)

32@changed_direction_at=Gosu.milliseconds

33@will_keep_direction_for=turn_time

34end

35

36defwait_time

37rand(300..1000)

38end

39

40defdrive_time

41rand(2000..5000)

42end

43

44defturn_time

45rand(500..2500)

46end

47end

Wewillhavemuchlesswaitingandmuchmoredrivingandturning.

TankChasingState

Ifopponentisfleeing,wewillwanttosetourdirectiontowardstheopponentandhitpedaltothemetal.Nowaitinghere.AiGun#desired_gun_anglewillpointdirectlytoourenemy.08-ai/entities/components/ai/tank_chasing_state.rb

1classTankChasingState<TankMotionState

2definitialize(object,vision,gun)

3super(object,vision)

4@object=object

5@vision=vision

6@gun=gun

7end

8

9defupdate

10change_directionifshould_change_direction?

11drive

12end

13

14defchange_direction

[email protected]_direction(

[email protected]_gun_angle-

[email protected]_gun_angle%45)

18

19@changed_direction_at=Gosu.milliseconds

20@will_keep_direction_for=turn_time

21end

22

23defdrive_time

2410000

25end

26

27defturn_time

28rand(300..600)

29end

30end

TankFleeingState

Now,ifourhealthislow,wewilldotheoppositeofchasing.Gunwillbepointingandshootingattheopponent,butwewantbodytomoveaway,sowewon’tgetourselveskilled.ItisverysimilartoTankChasingStatewherechange_directionaddsextra180degreestotheequation,butthereisonemorething.Tankcanonlyfleeforawhile.Thenitgetsitselftogetherandgoesintofinalbattle.That’swhyweprovidecan_flee?methodthatTankMotionFSMwillconsultwithbeforeenteringfleeingstate.

Page 114: Developing Games With Ruby: For those who write code for living

Wehaveimplementedallthestates,thatmeanswearemomentsawayfromactuallyplayableprototypewithtankbotsrunningaroundandfightingwithyouandeachother.

WiringTankMotionStatesIntoFiniteStateMachineImplementingTankMotionFSMafterwehaveallmotionstatesreadyissurprisinglyeasy:08-ai/entities/components/ai/tank_motion_fsm.rb

1classTankMotionFSM

2STATE_CHANGE_DELAY=500

3

4definitialize(object,vision,gun)

5@object=object

6@vision=vision

7@gun=gun

8@roaming_state=TankRoamingState.new(object,vision)

9@fighting_state=TankFightingState.new(object,vision)

10@fleeing_state=TankFleeingState.new(object,vision,gun)

11@chasing_state=TankChasingState.new(object,vision,gun)

12set_state(@roaming_state)

13end

14

15defon_collision(with)

16@current_state.on_collision(with)

17end

18

19defon_damage(amount)

20if@current_state==@roaming_state

21set_state(@fighting_state)

22end

23end

24

25defupdate

26choose_state

27@current_state.update

28end

29

30defset_state(state)

31returnunlessstate

32returnifstate==@current_state

33@last_state_change=Gosu.milliseconds

34@current_state=state

35state.enter

36end

37

38defchoose_state

39returnunlessGosu.milliseconds-

40(@last_state_change)>STATE_CHANGE_DELAY

[email protected]

[email protected]>40

[email protected]_to_target>BulletPhysics::MAX_DIST

44new_state=@chasing_state

45else

46new_state=@fighting_state

47end

48else

49if@fleeing_state.can_flee?

50new_state=@fleeing_state

51else

52new_state=@fighting_state

53end

54end

55else

56new_state=@roaming_state

57end

58set_state(new_state)

59end

60end

Page 115: Developing Games With Ruby: For those who write code for living

Allthelogicisinchoose_statemethod,whichisprettyuglyandprocedural,butitdoesthejob.Thecodeshouldbeeasytounderstand,soinsteadofdescribingit,hereisapictureworththousandwords:

Page 116: Developing Games With Ruby: For those who write code for living

Firstrealbattle

Youmaynoticeanewcrosshair,whichreplacedtheoldonethatwasnevervisible:classCamera

#...

defdraw_crosshair

factor=0.5

x=$window.mouse_x

y=$window.mouse_y

c=crosshair

c.draw(x-c.width*factor/2,

y-c.height*factor/2,

1000,factor,factor)

end

#...

private

defcrosshair

@crosshair||=Gosu::Image.new(

$window,Utils.media_path('c_dot.png'),false)

end

end

Howeverthisnewcrosshairdidn’thelpmewin,Igotmyasskickedbadly.Increasinggamewindowsizehelped,butweobviouslyneedtofinetunemanythingsinthisAI,tomakeitsmartandchallengingratherthandumbanddeadlyaccurate.

Page 117: Developing Games With Ruby: For those who write code for living

MakingThePrototypePlayable

Rightnowwehaveasomewhatplayable,butboringprototypewithoutanyscoresorwinningconditions.Youcanjustrunaroundandshootothertanks.Nobodywouldplayagamelikethis,henceweneedtotoaddthemissingparts.Thereisacrazyamountofthem.Itistimetogiveitathoroughplaythroughandwritedownalltheideasandpainpointsabouttheprototype.

Hereismylist:

1. Enemytanksdonotrespawn.2. Enemytanksshootatmycurrentlocation,notatwhereIwillbewhenbullethitsme.3. Enemytanksdon’tavoidcollisions.4. Randommapsareboringandlackdetail,couldusemoretilesorrandom

environmentobjects.5. Bulletsarehardtoseeongreensurface.6. Hardtotellwhereenemiesarecomingfrom,radarwouldhelp.7. Soundsplayatfullvolumeevenwhensomethinghappensacrossthewholemap.8. Mytankshouldrespawnafterit’sdead.9. Motionandfiringmechanicsseemclumsy.10. Mapboundariesarevisiblewhenyoucometotheedge.11. Enemytankmovementpatternsneedpolishingandimprovement.12. Bothmytankandenemiesdon’thaveanyidentity.Sometimeshardtodistinguish

whoiswho.13. Noideawhohasmostkills.HUDwithscoreandsomestatethatdisplaysscore

detailswouldhelp.14. Wouldbegreattohaverandompowerupslikehealth,extradamage.15. Explosionsdon’tleaveatrace.16. Tankscouldleavetrails.17. Deadtankskeeppilingupandclutteringthemap.18. Camerashouldbescoutingaheadofyouwhenyoumove,notdraggingbehind.19. Bulletsseemtoaccelerate.

Thiswillkeepusbusyforawhile,butintheendwewillprobablyhavesomethingthatwillhopefullybeabletoentertainpeopleformorethan3minutes.

Someitemsonthislistareeasyfixes.AfterplayingaroundwithPixelmatorfor15minutes,Iendedupwithabulletthatisvisibleonbothlightanddarkbackgrounds:

Page 118: Developing Games With Ruby: For those who write code for living

Highlyvisiblebullet

Motionandfiringmechanicswilleitherhavetobetunedsettingbysetting,orrewrittenfromscratch.Implementingscoresystem,powerupsandimprovingenemyAIdeservetohavechaptersoftheirown.Therestcanbetakencareofrightaway.

DrawingWaterBeyondMapBoundariesWedon’twanttoseedarknesswhenwecometotheedgeofgameworld.Luckily,itisatrivialfix.InMap#drawwecheckiftileexistsinmapbeforedrawingit.Whentiledoesnotexist,wecandrawwaterinstead.AndwecanalwaysfallbacktowatertileinMap#tile_at:classMap

#...

defdraw(viewport)

viewport.map!{|p|p/TILE_SIZE}

x0,x1,y0,y1=viewport.map(&:to_i)

(x0..x1).eachdo|x|

(y0..y1).eachdo|y|

row=@map[x]

map_x=x*TILE_SIZE

map_y=y*TILE_SIZE

ifrow

tile=@map[x][y]

iftile

tile.draw(map_x,map_y,0)

else

@water.draw(map_x,map_y,0)

Page 119: Developing Games With Ruby: For those who write code for living

end

else

@water.draw(map_x,map_y,0)

end

end

end

end

#...

private

#...

deftile_at(x,y)

t_x=((x/TILE_SIZE)%TILE_SIZE).floor

t_y=((y/TILE_SIZE)%TILE_SIZE).floor

row=@map[t_x]

row?row[t_y]:@water

end

#...

end

Nowtheedgelooksmuchbetter:

Page 120: Developing Games With Ruby: For those who write code for living

Mapedge

GeneratingTreeClustersTomakethemapmorefuntoplayat,wewillgeneratesometrees.Let’sstartwithTreeclass:09-polishing/entities/tree.rb

1classTree<GameObject

2attr_reader:x,:y,:health,:graphics

3

4definitialize(object_pool,x,y,seed)

5super(object_pool)

6@x,@y=x,y

7@graphics=TreeGraphics.new(self,seed)

8@health=Health.new(self,object_pool,30,false)

9@angle=rand(-15..15)

10end

11

12defon_collision(object)

[email protected](object.direction)

14end

15

16defbox

17[x,y]

18end

19end

Page 121: Developing Games With Ruby: For those who write code for living

Nothingfancyhere,wewantittoshakeoncollision,andithasgraphicsandhealth.seedwillusedtogenerateclustersofsimilartrees.Let’stakealookatTreeGraphics:09-polishing/entities/components/tree_graphics.rb

1classTreeGraphics<Component

2SHAKE_TIME=100

3SHAKE_COOLDOWN=200

4SHAKE_DISTANCE=[2,1,0,-1,-2,-1,0,1,0,-1,0]

5definitialize(object,seed)

6super(object)

7load_sprite(seed)

8end

9

10defshake(direction)

11now=Gosu.milliseconds

12returnif@shake_start&&

13now-@shake_start<SHAKE_TIME+SHAKE_COOLDOWN

14@shake_start=now

15@shake_direction=direction

16@shaking=true

17end

18

19defadjust_shake(x,y,shaking_for)

20elapsed=[shaking_for,SHAKE_TIME].min/SHAKE_TIME.to_f

21frame=((SHAKE_DISTANCE.length-1)*elapsed).floor

22distance=SHAKE_DISTANCE[frame]

23Utils.point_at_distance(x,y,@shake_direction,distance)

24end

25

26defdraw(viewport)

27if@shaking

28shaking_for=Gosu.milliseconds-@shake_start

29shaking_x,shaking_y=adjust_shake(

30center_x,center_y,shaking_for)

[email protected](shaking_x,shaking_y,5)

32ifshaking_for>=SHAKE_TIME

33@shaking=false

34end

35else

[email protected](center_x,center_y,5)

37end

38Utils.mark_corners(object.box)if$debug

39end

40

41defheight

[email protected]

43end

44

45defwidth

[email protected]

47end

48

49private

50

51defload_sprite(seed)

52frame_list=trees.frame_list

53frame=frame_list[(frame_list.size*seed).round]

54@tree=trees.frame(frame)

55end

56

57defcenter_x

58@center_x||[email protected]/2

59end

60

61defcenter_y

62@center_y||[email protected]/2

63end

64

65deftrees

66@@trees||=Gosu::TexturePacker.load_json($window,

67Utils.media_path('trees_packed.json'))

68end

69end

Page 122: Developing Games With Ruby: For those who write code for living

Shakingisprobablythemostinterestingparthere.Whenshakeiscalled,graphicswillstartdrawingtreeshiftedingivendirectionbyamountdefinedinSHAKE_DISTANCEarray.drawwillbesteppingthroughSHAKE_DISTANCEdependingonSHAKE_TIME,anditwillnotbeshakenagainforSHAKE_COOLDOWNperiod,toavoidinfiniteshakingwhiledrivingintoit.

WealsoneedsomeadjustmentstoTankPhysicsandTanktobeabletohittrees.First,wewanttocreateanemptyon_collision(object)methodinGameObjectclass,soallgameobjectswillbeabletocollide.

Then,TankPhysicsstartscallingTank#on_collisionwhencollisionisdetected:classTank<GameObject

#...

defon_collision(object)

returnunlessobject

#Avoidrecursion

ifobject.class==Tank

#InformAIabouthit

object.input.on_collision(object)

else

#Callonlyonnon-tankstoavoidrecursion

object.on_collision(self)

end

#BulletsshouldnotslowTanksdown

ifobject.class!=Bullet

@[email protected]>1

end

end

#...

end

ThefinalingredienttoourTreeisHealth,whichisextractedfromTankHealthtoreduceduplication.TankHealthnowextendsit:09-polishing/entities/components/health.rb

1classHealth<Component

2attr_accessor:health

3

4definitialize(object,object_pool,health,explodes)

5super(object)

6@explodes=explodes

7@object_pool=object_pool

8@initial_health=@health=health

9@health_updated=true

10end

11

12defrestore

13@health=@initial_health

14@health_updated=true

15end

16

17defdead?

18@health<1

19end

20

21defupdate

22update_image

23end

24

25definflict_damage(amount)

26if@health>0

27@health_updated=true

28@health=[@health-amount.to_i,0].max

29after_deathifdead?

30end

31end

32

33defdraw(viewport)

34returnunlessdraw?

Page 123: Developing Games With Ruby: For those who write code for living

35@image&&@image.draw(

[email protected]/2,

37y-object.graphics.height/2-

[email protected],100)

39end

40

41protected

42

43defdraw?

44$debug

45end

46

47defupdate_image

48returnunlessdraw?

49if@health_updated

[email protected]_s

51font_size=18

52@image=Gosu::Image.from_text(

53$window,text,

54Gosu.default_font_name,font_size)

55@health_updated=false

56end

57end

58

59defafter_death

60if@explodes

61ifThread.list.count<8

62Thread.newdo

63sleep(rand(0.1..0.3))

64Explosion.new(@object_pool,x,y)

65sleep0.3

66object.mark_for_removal

67end

68else

69Explosion.new(@object_pool,x,y)

70object.mark_for_removal

71end

72else

73object.mark_for_removal

74end

75end

76end

Yes,youcanmaketreeexplodewhenit’sdestroyed.Anditcausescoolchainreactionsblowingupwholetreeclusters.Butlet’snotdothat,becausewewilladdsomethingmoreappropriateforexplosions.

OurTreeisreadytofillthelandscape.WewilldoitinMapclass,whichwillnowneedtoknowaboutObjectPool,becausetreeswillgothere.classMap

#...

definitialize(object_pool)

load_tiles

@object_pool=object_pool

object_pool.map=self

@map=generate_map

generate_trees

end

#...

defgenerate_trees

noises=Perlin::Noise.new(2)

contrast=Perlin::Curve.contrast(

Perlin::Curve::CUBIC,2)

trees=0

target_trees=rand(300..500)

whiletrees<target_treesdo

x=rand(0..MAP_WIDTH*TILE_SIZE)

y=rand(0..MAP_HEIGHT*TILE_SIZE)

n=noises[x*0.001,y*0.001]

n=contrast.call(n)

iftile_at(x,y)==@grass&&n>0.5

Tree.new(@object_pool,x,y,n*2-1)

Page 124: Developing Games With Ruby: For those who write code for living

trees+=1

end

end

end

#...

end

Perlinnoiseisusedinsimilarfashionasitwaswhenwegeneratedmaptiles.Weallowcreatingtreesonlyifnoiselevelisabove0.5,andusenoiselevelasseedvalue-n*2-1willbeanumberbetween0and1whennisin0.5..1range.Andweonlyallowcreatingtreesongrasstiles.

Nowourmaplooksalittlebetter:

Page 125: Developing Games With Ruby: For those who write code for living

Hidingamongprocedurallygeneratedtrees

GeneratingRandomObjectsTreesaregreat,butwewantmoredetail.Let’sspicethingsupwithexplosiveboxesandbarrels.Theywillbeusingthesameclasswithsinglespritesheet,sowhilethespritewillbechosenrandomly,behaviorwillbethesame.ThisnewclasswillbecalledBox:09-polishing/entities/box.rb

1classBox<GameObject

2attr_reader:x,:y,:health,:graphics,:angle

3

4definitialize(object_pool,x,y)

5super(object_pool)

6@x,@y=x,y

7@graphics=BoxGraphics.new(self)

8@health=Health.new(self,object_pool,10,true)

9@angle=rand(-15..15)

10end

11

12defon_collision(object)

13returnunlessobject.physics.speed>1.0

14@x,@y=Utils.point_at_distance(@x,@y,object.direction,2)

15@box=nil

16end

17

18defbox

19return@boxif@box

[email protected]/2

Page 126: Developing Games With Ruby: For those who write code for living

[email protected]/2

22#Boundingboxadjustedtotrimshadows

23@box=[x-w+4,y-h+8,

24x+w,y-h+8,

25x+w,y+h,

26x-w+4,y+h]

27@box=Utils.rotate(@angle,@x,@y,*@box)

28end

29end

Itwillbegeneratedwithslightrandomangle,topreserverealisticshadowsbutgiveanimpressionofchaoticplacement.Tankswillalsobeabletopushboxesalittleoncollision,butonlywhengoingfastenough.HealthcomponentisthesameonethatTreehas,butinitializedwithlesshealthandexplosiveflagistrue,sotheboxwillblowupafteronehitanddealextradamagetothesurroundings.

BoxGraphicsisnothingfancy,itjustloadsrandomspriteuponinitialization:09-polishing/entities/components/box_graphics.rb

1classBoxGraphics<Component

2definitialize(object)

3super(object)

4load_sprite

5end

6

7defdraw(viewport)

[email protected]_rot(x,y,0,object.angle)

9Utils.mark_corners(object.box)if$debug

10end

11

12defheight

[email protected]

14end

15

16defwidth

[email protected]

18end

19

20private

21

22defload_sprite

23frame=boxes.frame_list.sample

24@box=boxes.frame(frame)

25end

26

27defcenter_x

28@center_x||=x-width/2

29end

30

31defcenter_y

32@center_y||=y-height/2

33end

34

35defboxes

36@@boxes||=Gosu::TexturePacker.load_json($window,

37Utils.media_path('boxes_barrels.json'))

38end

39end

TimetogenerateboxesinourMap.Itwillbesimilartotrees,butwewon’tneedPerlinnoise,sincetherewillbewayfewerboxesthantrees,sowedon’tneedtoformpatterns.Allweneedtodoistocheckifwe’renotgeneratingboxonwater.classMap

#...

definitialize(object_pool)

#...

generate_boxes

Page 127: Developing Games With Ruby: For those who write code for living

end

#...

defgenerate_boxes

boxes=0

target_boxes=rand(10..30)

whileboxes<target_boxesdo

x=rand(0..MAP_WIDTH*TILE_SIZE)

y=rand(0..MAP_HEIGHT*TILE_SIZE)

iftile_at(x,y)!=@water

Box.new(@object_pool,x,y)

boxes+=1

end

end

end

#...

end

Nowgiveitago.Beautiful,isn’tit?

Page 128: Developing Games With Ruby: For those who write code for living

Boxesandbarrelsinthejungle

ImplementingARadarWithallthevisualnoiseitisgettingincreasinglydifficulttoseeenemytanks.That’swhywewillimplementaRadartohelpourselves.09-polishing/entities/radar.rb

1classRadar

2UPDATE_FREQUENCY=1000

3WIDTH=150

4HEIGHT=100

5PADDING=10

6#Blackwith33%transparency

7BACKGROUND=Gosu::Color.new(255*0.33,0,0,0)

8attr_accessor:target

9

10definitialize(object_pool,target)

11@object_pool=object_pool

12@target=target

13@last_update=0

14end

15

16defupdate

17ifGosu.milliseconds-@last_update>UPDATE_FREQUENCY

18@nearby=nil

19end

20@nearby||=@object_pool.nearby(@target,2000).selectdo|o|

21o.class==Tank&&!o.health.dead?

Page 129: Developing Games With Ruby: For those who write code for living

22end

23end

24

25defdraw

26x1,x2,y1,y2=radar_coords

27$window.draw_quad(

28x1,y1,BACKGROUND,

29x2,y1,BACKGROUND,

30x2,y2,BACKGROUND,

31x1,y2,BACKGROUND,

32200)

33draw_tank(@target,Gosu::Color::GREEN)

34@nearby&&@nearby.eachdo|t|

35draw_tank(t,Gosu::Color::RED)

36end

37end

38

39private

40

41defdraw_tank(tank,color)

42x1,x2,y1,y2=radar_coords

43tx=x1+WIDTH/2+([email protected])/20

44ty=y1+HEIGHT/2+([email protected])/20

45if(x1..x2).include?(tx)&&(y1..y2).include?(ty)

46$window.draw_quad(

47tx-2,ty-2,color,

48tx+2,ty-2,color,

49tx+2,ty+2,color,

50tx-2,ty+2,color,

51300)

52end

53end

54

55defradar_coords

56x1=$window.width-WIDTH-PADDING

57x2=$window.width-PADDING

58y1=$window.height-HEIGHT-PADDING

59y2=$window.height-PADDING

60[x1,x2,y1,y2]

61end

62end

Radar,likeCamera,alsohasatarget.ItusesObjectPooltoquerynearbyobjectsandfiltersoutinstancesofaliveTank.Thenitdrawsatransparentblackbackgroundandsmalldotsforeachtank,greenfortarget,redfortherest.

ToavoidqueryingObjectPooltoooften,Radarupdatesitselfonlyonceeverysecond.

Itisinitialized,updatedanddrawninPlayState,rightafterCamera:classPlayState<GameState

#...

definitialize

#...

@camera.target=@tank

@radar=Radar.new(@object_pool,@tank)

#...

end

#...

defupdate

#...

@camera.update

@radar.update

#...

end

#...

defdraw

#...

@camera.draw_crosshair

@radar.draw

end

#...

end

Page 130: Developing Games With Ruby: For those who write code for living

Timetoenjoytheresults.

Page 131: Developing Games With Ruby: For those who write code for living

Radarinaction

DynamicSoundVolumeAndPanningWehaveimprovedthevisuals,butsoundisstillterrible.Likesomesuperhero,youcanheareverythingthathappensinthemap,anditcandriveyouinsane.Wewillfixthatinamoment.

Theideaistomakeeverythingthathappensfurtherawayfromcameratargetsoundlessloud,untilthesoundfadesawaycompletely.Tomakeplayer’sexperiencemoreimmersive,wewillalsotakeadvantageofstereospeakers-soundsshouldappeartobecomingfromtherightdirection.

Unfortunately,Gosu::Sample#play_pandoesnotworkasonewouldexpectitto.Ifyouplaythesamplewithjustalittlepanning,itcompletelycutsofftheoppositechannel,meaningthatifyouplayasamplewithpanlevelof0.1(10%totheright),youwouldexpecttohearsomethinginleftspeakeraswell.Theactualbehavioristhatsoundplaysthroughtherightspeakerprettyloudly,andifyouincreasepanlevelto,say,0.7,youwillhearthesoundthroughrightspeakeragain,butitwillbewaymoresilent.

Toimplementrealisticstereosoundsthatcomethroughbothspeakerswhenpanned,weneedtoplaytwosampleswithoppositepanlevel.Aftersomeexperimenting,Idiscovered

Page 132: Developing Games With Ruby: For those who write code for living

thatfiddlingwithpanlevelmakesthingssoundweird,whileplayingwithvolumeproducessofter,moresubtleeffect.ThisiswhatIendeduphaving:09-polishing/misc/stereo_sample.rb

1classStereoSample

2@@all_instances=[]

3

4defself.register_instances(instances)

5@@all_instances<<instances

6end

7

8defself.cleanup

9@@all_instances.eachdo|instances|

10instances.eachdo|key,instance|

11unlessinstance.playing?||instance.paused?

12instances.delete(key)

13end

14end

15end

16end

17

18definitialize(window,sound_l,sound_r=sound_l)

19@sound_l=Gosu::Sample.new(window,sound_l)

20#Usesamesampleinmono->stereo

21ifsound_l==sound_r

22@sound_r=@sound_l

23else

24@sound_r=Gosu::Sample.new(window,sound_r)

25end

26@instances={}

27self.class.register_instances(@instances)

28end

29

30defpaused?(id=:default)

31i=@instances["#{id}_l"]

32i&&i.paused?

33end

34

35defplaying?(id=:default)

36i=@instances["#{id}_l"]

37i&&i.playing?

38end

39

40defstopped?(id=:default)

41@instances["#{id}_l"].nil?

42end

43

44defplay(id=:default,pan=0,

45volume=1,speed=1,looping=false)

46@instances["#{id}_l"]=@sound_l.play_pan(

47-0.2,0,speed,looping)

48@instances["#{id}_r"]=@sound_r.play_pan(

490.2,0,speed,looping)

50volume_and_pan(id,volume,pan)

51end

52

53defpause(id=:default)

54@instances["#{id}_l"].pause

55@instances["#{id}_r"].pause

56end

57

58defresume(id=:default)

59@instances["#{id}_l"].resume

60@instances["#{id}_r"].resume

61end

62

63defstop

[email protected]("#{id}_l").stop

[email protected]("#{id}_r").stop

66end

67

68defvolume_and_pan(id,volume,pan)

69ifpan>0

Page 133: Developing Games With Ruby: For those who write code for living

70pan_l=1-pan*2

71pan_r=1

72else

73pan_l=1

74pan_r=1+pan*2

75end

76pan_l*=volume

77pan_r*=volume

78@instances["#{id}_l"].volume=[pan_l,0.05].max

79@instances["#{id}_r"].volume=[pan_r,0.05].max

80end

81end

StereoSamplemanagesstereoplaybackofsampleinstances,andtoavoidmemoryleaks,ithascleanupthatscansallsampleinstancesandremovessamplesthathavefinishedplaying.Forthisremovaltowork,weneedtoplaceacalltoStereoSample.cleanupinsidePlayState#updatemethod.

Todeterminecorrectpanandvolume,wewillcreatesomehelpermethodsinUtilsmodule:moduleUtils

HEARING_DISTANCE=1000.0

#...

defself.volume(object,camera)

return1ifobject==camera.target

distance=Utils.distance_between(

camera.target.x,camera.target.y,

object.x,object.y)

distance=[(HEARING_DISTANCE-distance),0].max

distance/HEARING_DISTANCE

end

defself.pan(object,camera)

return0ifobject==camera.target

pan=object.x-camera.target.x

sig=pan>0?1:-1

pan=(pan%HEARING_DISTANCE)/HEARING_DISTANCE

ifsig>0

pan

else

-1+pan

end

end

defself.volume_and_pan(object,camera)

[volume(object,camera),pan(object,camera)]

end

end

Apparently,havingaccesstoCameraisnecessaryforcalculatingsoundvolumeandpan,sowewilladdattr_accessor:cameratoObjectPoolclassandassignitinPlayStateconstructor.Youmaywonderwhywedidn’tuseCamera#targetrightaway.Theansweristhatcameracanchangeit’starget.E.g.whenyourtankdies,newinstancewillbegeneratedwhenyourespawn,soifallotherobjectswouldstillhavethereferencetoyouroldtank,guesswhatyouwouldhear?

RemasteredTankSoundscomponentisprobablythemostelaborateexampleofhowStereoSampleshouldbeused:09-polishing/entities/components/tank_sounds.rb

1classTankSounds<Component

2definitialize(object,object_pool)

3super(object)

4@object_pool=object_pool

5end

Page 134: Developing Games With Ruby: For those who write code for living

6

7defupdate

8id=object.object_id

9ifobject.physics.moving?

10move_volume=Utils.volume(

11object,@object_pool.camera)

12pan=Utils.pan(object,@object_pool.camera)

13ifdriving_sound.paused?(id)

14driving_sound.resume(id)

15elsifdriving_sound.stopped?(id)

16driving_sound.play(id,pan,0.5,1,true)

17end

18driving_sound.volume_and_pan(id,move_volume*0.5,pan)

19else

20ifdriving_sound.playing?(id)

21driving_sound.pause(id)

22end

23end

24end

25

26defcollide

27vol,pan=Utils.volume_and_pan(

28object,@object_pool.camera)

29crash_sound.play(self.object_id,pan,vol,1,false)

30end

31

32private

33

34defdriving_sound

35@@driving_sound||=StereoSample.new(

36$window,Utils.media_path('tank_driving.mp3'))

37end

38

39defcrash_sound

40@@crash_sound||=StereoSample.new(

41$window,Utils.media_path('metal_interaction2.wav'))

42end

43end

AndthisishowstaticExplosionSoundslookslike:09-polishing/entities/components/explosion_sounds.rb

1classExplosionSounds

2class<<self

3defplay(object,camera)

4volume,pan=Utils.volume_and_pan(object,camera)

5sound.play(object.object_id,pan,volume)

6end

7

8private

9

10defsound

11@@sound||=StereoSample.new(

12$window,Utils.media_path('explosion.mp3'))

13end

14end

15end

AfterwiringeverythingsothatsoundcomponentshaveaccesstoObjectPool,therestisstraightforward.

GivingEnemiesIdentityWouldn’titbegreatifyoucouldtellyourselfapartfromtheenemies.Moreover,enemiescouldhavenames,soyouwouldknowwhichoneismoreaggressiveorhave,youknow,personalissueswithsomeone.

Page 135: Developing Games With Ruby: For those who write code for living

Todothatweneedtoasktheplayertoinputanickname,andchoosesomefunnynamesforeachenemyAI.Hereisanicelistwewillgrab:http://www.paulandstorm.com/wha/clown-names/

Wefirstcompileeverythingintoatextfiledcallednames.txt,thatlookslikethis:media/names.txt

Strippy

Boffo

Buffo

Drips

...

Nowweneedaclasstoparsethelistandgiveoutrandomnamesfromit.Wealsowanttolimitnamelengthtosomethingthatdisplaysnicely.09-polishing/misc/names.rb

1classNames

2definitialize(file)

3@names=File.read(file).split("\n").rejectdo|n|

4n.size>12

5end

6end

7

8defrandom

[email protected]

[email protected](name)

11name

12end

13end

Thenweneedtoplacethosenamessomewhere.Wecouldassignthemtotanks,butthinkahead-ifourplayerandAIenemieswillrespawn,weshouldgivenamestoinputs,becauseTankisreplaceable,driverisnot.Well,itis,butlet’snotgettoodeepintoit.

FornowwejustaddnameparametertoPlayerInputandAiInputinitializers,saveitin@nameinstancevariable,andthenadddraw(viewport)methodtomakeitrenderbelowthetank:#09-polishing/entities/components/player_input.rb

classPlayerInput<Component

#Darkgreen

NAME_COLOR=Gosu::Color.argb(0xee084408)

definitialize(name,camera)

super(nil)

@name=name

@camera=camera

end

#...

defdraw(viewport)

@name_image||=Gosu::Image.from_text(

$window,@name,Gosu.default_font_name,20)

@name_image.draw(

x-@name_image.width/2-1,

y+object.graphics.height/2,100,

1,1,Gosu::Color::WHITE)

@name_image.draw(

x-@name_image.width/2,

y+object.graphics.height/2,100,

1,1,NAME_COLOR)

end

#...

end

Page 136: Developing Games With Ruby: For those who write code for living

#09-polishing/entities/components/ai_input.rb

classAiInput<Component

#Darkred

NAME_COLOR=Gosu::Color.argb(0xeeb10000)

definitialize(name,object_pool)

super(nil)

@object_pool=object_pool

@name=name

@last_update=Gosu.milliseconds

end

#...

defdraw(viewport)

@motion.draw(viewport)

@gun.draw(viewport)

@name_image||=Gosu::Image.from_text(

$window,@name,Gosu.default_font_name,20)

@name_image.draw(

x-@name_image.width/2-1,

y+object.graphics.height/2,100,

1,1,Gosu::Color::WHITE)

@name_image.draw(

x-@name_image.width/2,

y+object.graphics.height/2,100,

1,1,NAME_COLOR)

end

#...

end

WecanseethatgenericInputclasscanbeeasilyextracted,butlet’sfollowtheRuleofthreeandnotdoprematurerefactoring.

Instead,runthegameandenjoydyingfromabunchofmadclowns.

Page 137: Developing Games With Ruby: For those who write code for living

Identitymakesadifference

RespawningTanksAndRemovingDeadOnesToimplementrespawningwecoulduseMap#find_spawn_pointeverytimewewantedtorespawn,butitmaygetslow,becauseitbruteforcesthemapforrandomspotsthatarenotwater.Wedon’twantourgametostartfreezingwhentanksarerespawning,sowewillchangehowtankspawningworks.Insteadoflookingforanewrespawnpointallthetime,wewillpre-generateseveralofthemforreuse.classMap

#...

defspawn_points(max)

@spawn_points=(0..max).mapdo

find_spawn_point

end

@spawn_points_pointer=0

end

defspawn_point

@spawn_points[(@spawn_points_pointer+=1)%@spawn_points.size]

end

#...

end

Herewehavespawn_pointsmethodthatpreparesanumberofspawnpointsandstoresthemin@spawn_pointsinstancevariable,andspawn_pointmethodthatcyclesthroughall

Page 138: Developing Games With Ruby: For those who write code for living

@spawn_pointsandreturnsthemonebyone.find_spawn_pointcannowbecomeprivate.

WewilluseMap#spawn_pointswheninitializingPlayStateandpassObjectPooltoPlayerInput(AiInputalreadyhasit),sothatwewillbeabletocall@object_pool.map.spawn_pointwhenneeded.classPlayState<GameState

#...

definitialize

#...

@map=Map.new(@object_pool)

@map.spawn_points(15)

@tank=Tank.new(@object_pool,

PlayerInput.new('Player',@camera,@object_pool))

#...

10.timesdo|i|

Tank.new(@object_pool,AiInput.new(

@names.random,@object_pool))

end

end

#...

end

Whentankdies,wewantittostaydeadfor5secondsandthenrespawninoneofourpredefinedspawnpoints.WewillachievethatbyaddingrespawnmethodandcallingitinPlayerInput#updateandAiInput#updateiftankisdead.#09-polishing/entities/components/player_input.rb

classPlayerInput<Component

#...

defupdate

returnrespawnifobject.health.dead?

#...

end

#...

private

defrespawn

ifobject.health.should_respawn?

object.health.restore

object.x,object.y=@object_pool.map.spawn_point

@camera.x,@camera.y=x,y

PlayerSounds.respawn(object,@camera)

end

end

#...

end

#09-polishing/entities/components/ai_input.rb

classAiInput<Component

#...

defupdate

returnrespawnifobject.health.dead?

#...

end

#...

private

defrespawn

ifobject.health.should_respawn?

object.health.restore

object.x,object.y=@object_pool.map.spawn_point

PlayerSounds.respawn(object,@object_pool.camera)

end

end

end

WeneedsomechangesinTankHealthclasstoo:classTankHealth<Health

RESPAWN_DELAY=5000

Page 139: Developing Games With Ruby: For those who write code for living

#...

defshould_respawn?

Gosu.milliseconds-@death_time>RESPAWN_DELAY

end

#...

defafter_death

@death_time=Gosu.milliseconds

#...

end

end

classHealth<Component

#...

defrestore

@health=@initial_health

@health_updated=true

end

#...

end

Itshouldn’tbehardtoputeverythingtogetherandenjoytheneverendinggameplay.

Youmayhavenoticedthatwealsoaddedasoundthatwillbeplayedwhenplayerrespawns.Anice“whoosh”.09-polishing/entities/components/player_sounds.rb

1classPlayerSounds

2class<<self

3defrespawn(object,camera)

4volume,pan=Utils.volume_and_pan(object,camera)

5respawn_sound.play(object.object_id,pan,volume*0.5)

6end

7

8private

9

10defrespawn_sound

11@@respawn||=StereoSample.new(

12$window,Utils.media_path('respawn.wav'))

13end

14end

15end

DisplayingExplosionDamageTrailsWhensomethingblowsup,youexpectittoleaveatrail,right?Inourcaseexplosionsdisappearasifnothinghaseverhappened,andwejustcan’tleaveitlikethis.Let’sintroduceDamagegameobjectthatwillberesponsiblefordisplayingexplosionresidueonsandandgrass:09-polishing/entities/damage.rb

1classDamage<GameObject

2MAX_INSTANCES=100

3attr_accessor:x,:y

4@@instances=[]

5

6definitialize(object_pool,x,y)

7super(object_pool)

8DamageGraphics.new(self)

9@x,@y=x,y

10track(self)

11end

12

13defeffect?

14true

15end

16

17private

Page 140: Developing Games With Ruby: For those who write code for living

18

19deftrack(instance)

20if@@instances.size<MAX_INSTANCES

21@@instances<<instance

22else

23out=@@instances.shift

24out.mark_for_removal

25@@instances<<instance

26end

27end

28end

Damagetracksit’sinstancesandstartsremovingoldoneswhenMAX_INSTANCESarereached.Withoutthisoptimization,thegamewouldgetincreasinglyslowereverytimesomebodyshoots.

Wehavealsoaddedanewgameobjecttrait-effect?returnstrueonDamageandExplosion,falseonTank,Tree,BoxandBullet.ThatwaywecanfilterouteffectswhenqueryingObjectPool#nearbyforcollisionsorenemies.09-polishing/entities/object_pool.rb

1classObjectPool

2attr_accessor:objects,:map,:camera

3

4definitialize

5@objects=[]

6end

7

8defnearby(object,max_distance)

9non_effects.selectdo|obj|

10obj!=object&&

11(obj.x-object.x).abs<max_distance&&

12(obj.y-object.y).abs<max_distance&&

13Utils.distance_between(

14obj.x,obj.y,object.x,object.y)<max_distance

15end

16end

17

18defnon_effects

[email protected](&:effect?)

20end

21end

Whenitcomestorenderinggraphics,tomakeanimpressionofrandomness,wewillcyclethroughseveraldifferentdamageimagesanddrawthemrotated:09-polishing/entities/components/damage_graphics.rb

1classDamageGraphics<Component

2definitialize(object_pool)

3super

4@image=images.sample

5@angle=rand(0..360)

6end

7

8defdraw(viewport)

[email protected]_rot(x,y,0,@angle)

10end

11

12private

13

14defimages

15@@images||=(1..4).mapdo|i|

16Gosu::Image.new($window,

17Utils.media_path("damage#{i}.png"),false)

18end

19end

20end

Page 141: Developing Games With Ruby: For those who write code for living

ExplosionwillberesponsibleforcreatingDamageinstancesonsolidground,justbeforeexplosionanimationstarts:classExplosion<GameObject

definitialize(object_pool,x,y)

#...

if@object_pool.map.can_move_to?(x,y)

Damage.new(@object_pool,@x,@y)

end

#...

end

#...

end

Andthisishowtheresultlookslike:

Page 142: Developing Games With Ruby: For those who write code for living

Damagedbattlefield

DebuggingBulletPhysicsWhenplayingthegame,thereisafeelingthatbulletsstartoutslowwhenfiredandgainspeedastimegoes.Let’sreviewBulletPhysics#updateandthinkwhythisishappening:classBulletPhysics<Component

#...

defupdate

fly_speed=Utils.adjust_speed(object.speed)

fly_distance=(Gosu.milliseconds-object.fired_at)*

0.001*fly_speed/2

object.x,object.y=point_at_distance(fly_distance)

check_hit

object.explodeifarrived?

end

#...

end

Flawhereisveryobvious.Gosu.milliseconds-object.fired_atwillbeincreasinglybiggerastimegoes,thusincreasingfly_distance.Thefixisstraightforward-wewanttocalculatefly_distanceusingtimepassedbetweencallstoBulletPhysics#update,likethis:classBulletPhysics<Component

#...

defupdate

fly_speed=Utils.adjust_speed(object.speed)

Page 143: Developing Games With Ruby: For those who write code for living

now=Gosu.milliseconds

@last_update||=object.fired_at

fly_distance=(now-@last_update)*0.001*fly_speed

object.x,object.y=point_at_distance(fly_distance)

@last_update=now

check_hit

object.explodeifarrived?

end

#...

end

Butifyouwouldrunthegamenow,bulletswouldflysoslow,thatyouwouldfeellikeNeoinTheMatrix.Tofixthat,wewillhavetotellourtanktofirebulletsalittlefaster.classTank<GameObject

#...

defshoot(target_x,target_y)

ifcan_shoot?

@last_shot=Gosu.milliseconds

Bullet.new(object_pool,@x,@y,target_x,target_y)

.fire(self,1500)#Oldvaluewas100

end

end

#...

end

Nowbulletsflyliketheyaresupposedto.Icanonlywonderwhyhaven’tInoticedthisbugintheverybeginning.

MakingCameraLookAheadOneofthemostannoyingthingswithcurrentstateofprototypeisthatCameraisdraggingbehindinsteadofshowingwhatisinthedirectionyouaremoving.Tofixtheissue,weneedtochangethewayhowCameramovesaround.FirstweneedtoknowwhereCamerawantstobe.WewilluseUtils.point_at_distancetochooseaspotaheadoftheTank.Then,Camera#updateneedstoberewritten,soCameracandynamicallyadjusttoit’sdesiredspot.Herearethechanges:classCamera

#...

defdesired_spot

[email protected]?

Utils.point_at_distance(

@target.x,@target.y,

@target.direction,

@target.physics.speed.ceil*25)

else

[@target.x,@target.y]

end

end

#...

defupdate

des_x,des_y=desired_spot

shift=Utils.adjust_speed(

@target.physics.speed).floor+1

if@x<des_x

ifdes_x-@x<shift

@x=des_x

else

@x+=shift

end

elsif@x>des_x

if@x-des_x<shift

@x=des_x

else

@x-=shift

end

end

if@y<des_y

Page 144: Developing Games With Ruby: For those who write code for living

ifdes_y-@y<shift

@y=des_y

else

@y+=shift

end

elsif@y>des_y

if@y-des_y<shift

@y=des_y

else

@y-=shift

end

end

#...

end

#...

end

Itwouldn’twincodestyleawards,butitdoesthejob.Gameisnowmuchmoreplayable.

ReviewingTheChangesLet’sgetbacktoourlistofimprovementstoseewhatwehavedone:

1. Enemytanksdonotrespawn.2. Randommapsareboringandlackdetail,couldusemoretilesorrandom

environmentobjects.3. Bulletsarehardtoseeongreensurface.4. Hardtotellwhereenemiesarecomingfrom,radarwouldhelp.5. SoundsplayatfullvolumeevenwhensomethinghappensacrossThewholemap.6. Mytankshouldrespawnafterit’sdead.7. Mapboundariesarevisiblewhenyoucometotheedge.8. Bothmytankandenemiesdon’thaveanyidentity.Sometimeshardtodistinguish

whoiswho.9. Explosionsdon’tleaveatrace.10. Deadtankskeeppilingupandclutteringthemap.11. Camerashouldbescoutingaheadofyouwhenyoumove,notdraggingbehind.12. Bulletsseemtoaccelerate.

Notbadforastart.Thisiswhatwestillneedtocoverinnextcoupleofchapters:

1. Enemytanksshootatmycurrentlocation,notatwhereIwillbewhenbullethitsme.2. Enemytanksdon’tavoidcollisions.3. Enemytankmovementpatternsneedpolishingandimprovement.4. Noideawhohasmostkills.HUDwithscoreandsomestatethatdisplaysscore

detailswould5. Wouldbegreattohaverandompowerupslikehealth,extradamage.6. Motionandfiringmechanicsseemclumsy.help.7. Tankscouldleavetrails.

Iwilladd“OptimizeObjectPoolperformance”,becausegamestartsslowingdownwhentoomanyobjectsareaddedtothepool,andprofilingshowsthatArray#select,whichistheheartofObjectPool#nearby,isthemaincause.Speedisoneofmostimportantfeaturesofanygame,solet’snothesitatetoimproveit.

Page 145: Developing Games With Ruby: For those who write code for living

DealingWithThousandsOfGameObjects

Gosuisblazingfastwhenitcomestodrawing,buttherearemorethingsgoingon.Namely,weuseObjectPool#nearbyquiteoftentoloopthroughthousandsofobjects60timespersecondtomeasuredistancesamongthem.Thisslowseverythingdownwhenobjectpoolgrows.

Todemonstratetheeffect,wewillgenerate1500trees,30tanks,~100boxesandleave1000damagetrailsfromexplosions.ItwasenoughtodropFPSbelow30:

Page 146: Developing Games With Ruby: For those who write code for living

Runningslowwiththousandsofgameobjects

SpatialPartitioningThereisasolutionforthisparticularproblemis“SpatialPartitioning”,andtheessenceofitisthatyouhavetouseatree-likedatastructurethatdividesspaceintoregions,placesobjectsthereandletsyouqueryitselfinlogarithmictime,omittingobjectsthatfalloutofqueryregion.SpatialPartitioningisexplainedwellinGameProgrammingPatterns.

Probablythemostappropriatedatastructureforour2Dgameisquadtree.ToquoteWikipedia,“quadtreesaremostoftenusedtopartitionatwo-dimensionalspacebyrecursivelysubdividingitintofourquadrantsorregions.”Hereishowitlookslike:

Page 147: Developing Games With Ruby: For those who write code for living

Visualrepresentationofquadtree

ImplementingAQuadtreeTherearesomeimplementationsofquadtreeavailableforRuby-rquad,rubyquadtreeandrubyquad,butitseemseasytoimplement,sowewillbuildonetailored(read:closelycoupled)toourgameusingthepseudocodefromWikipedia.

AxisAlignedBoundingBox

OneofprerequisitesofquadtreeisAxisalignedboundingbox,sometimesreferredtoas“AABB”.Itissimplyaboxthatsurroundstheshapebuthasedgesthatareinparallelwiththeaxesofunderlyingcoordinatesystem.Theadvantageofthisboxisthatitgivesaroughestimatewheretheshapeisandisveryefficientwhenitcomestoqueryingifapointisinsideoroutsideit.

Page 148: Developing Games With Ruby: For those who write code for living

Axisalignedboundingboxwithcenterpointandhalfdimension

Todefineaxisalignedboundingbox,weneedit’scenterpointandhalfdimensionvector,whichpointsfromcenterpointtooneofthecornersofthebox,andtwomethods,onethattellsifAABBcontainsapoint,andonethattellsifAABBintersectswithanotherAABB.Thisishowourimplementationlookslike:10-partitioning/misc/axis_aligned_bounding_box.rb

1classAxisAlignedBoundingBox

2attr_reader:center,:half_dimension

3definitialize(center,half_dimension)

4@center=center

5@half_dimension=half_dimension

6@dhx=(@half_dimension[0]-@center[0]).abs

7@dhy=(@half_dimension[1]-@center[1]).abs

8end

9

10defcontains?(point)

11returnfalseunless(@center[0]+@dhx)>=point[0]

12returnfalseunless(@center[0]-@dhx)<=point[0]

13returnfalseunless(@center[1]+@dhy)>=point[1]

14returnfalseunless(@center[1]-@dhy)<=point[1]

15true

16end

17

18defintersects?(other)

19ocx,ocy=other.center

20ohx,ohy=other.half_dimension

21odhx=(ohx-ocx).abs

22returnfalseunless(@center[0]+@dhx)>=(ocx-odhx)

23returnfalseunless(@center[0]-@dhx)<=(ocx+odhx)

24odhy=(ohy-ocy).abs

25returnfalseunless(@center[1]+@dhy)>=(ocy-odhy)

Page 149: Developing Games With Ruby: For those who write code for living

26returnfalseunless(@center[1]-@dhy)<=(ocy+odhy)

27true

28end

29

30defto_s

31"c:#{@center},h:#{@half_dimension}"

32end

33end

Ifyoudigin10-partitioning/specs,youwillfindtestsforthisimplementationtoo.

ThemathusedinAxisAlignedBoundingBox#contains?andAxisAlignedBoundingBox#intersects?isfairlysimpleandhopefullyveryfast,becausethesemethodswillbecalledbillionsoftimesthroughoutthegame.

QuadTreeForGameObjects

ToimplementthegloriousQuadTreeitself,weneedtoinitializeitwithboundary,thatisdefinedbyaninstanceofAxisAlignedBoundingBoxandprovidemethodsforinserting,removingandqueryingthetree.PrivateQuadTree#subdividemethodwillbecalledwhenwetrytoinsertanobjectintoatreethathasmoreobjectsthanit’sNODE_CAPACITY.10-partitioning/misc/quad_tree.rb

1classQuadTree

2NODE_CAPACITY=12

3attr_accessor:ne,:nw,:se,:sw,:objects

4

5definitialize(boundary)

6@boundary=boundary

7@objects=[]

8end

9

10definsert(game_object)

[email protected]?(

12game_object.location)

13

[email protected]<NODE_CAPACITY

15@objects<<game_object

16returntrue

17end

18

19subdivideunless@nw

20

[email protected](game_object)

[email protected](game_object)

[email protected](game_object)

[email protected](game_object)

25

26#shouldneverhappen

27raise"Failedtoinsert#{game_object}"

28end

29

30defremove(game_object)

[email protected]?(

32game_object.location)

[email protected](game_object)

34returntrue

35end

36returnfalseunless@nw

[email protected](game_object)

[email protected](game_object)

[email protected](game_object)

[email protected](game_object)

41false

42end

43

44defquery_range(range)

45result=[]

Page 150: Developing Games With Ruby: For those who write code for living

[email protected]?(range)

47returnresult

48end

49

[email protected]|o|

51ifrange.contains?(o.location)

52result<<o

53end

54end

55

56#Notsubdivided

57returnresultunless@ne

58

[email protected]_range(range)

[email protected]_range(range)

[email protected]_range(range)

[email protected]_range(range)

63

64result

65end

66

67private

68

69defsubdivide

70cx,[email protected]

71hx,[email protected]_dimension

72hhx=(cx-hx).abs/2.0

73hhy=(cy-hy).abs/2.0

74@nw=QuadTree.new(

75AxisAlignedBoundingBox.new(

76[cx-hhx,cy-hhy],

77[cx,cy]))

78@ne=QuadTree.new(

79AxisAlignedBoundingBox.new(

80[cx+hhx,cy-hhy],

81[cx,cy]))

82@sw=QuadTree.new(

83AxisAlignedBoundingBox.new(

84[cx-hhx,cy+hhy],

85[cx,cy]))

86@se=QuadTree.new(

87AxisAlignedBoundingBox.new(

88[cx+hhx,cy+hhy],

89[cx,cy]))

90end

91end

ThisisavanillaquadtreethatstoresinstancesofGameObjectandusesGameObject#locationforindexingobjectsinspace.Italsohasspecsthatyoucanfindincodesamples.

YoucanexperimentwithQuadTree#NODE_CAPACITY,butIfoundthatvaluesbetween8and16worksbest,soIsettledwith12.

IntegratingObjectPoolWithQuadTreeWehaveimplementedaQuadTree,butitisnotyetincorporatedintoourgame.Todothat,wewillhookitintoObjectPoolandtrytokeeptheoldinterfaceintact,sothatObjectPool#nearbywillstillworkasusual,butwillbeabletocopewithwaymoreobjectsthanbefore.10-partitioning/entities/object_pool.rb

1classObjectPool

2attr_accessor:map,:camera,:objects

3

4defsize

[email protected]

Page 151: Developing Games With Ruby: For those who write code for living

6end

7

8definitialize(box)

9@tree=QuadTree.new(box)

10@objects=[]

11end

12

13defadd(object)

14@objects<<object

[email protected](object)

16end

17

18deftree_remove(object)

[email protected](object)

20end

21

22deftree_insert(object)

[email protected](object)

24end

25

26defupdate_all

[email protected](&:update)

[email protected]!do|o|

29ifo.removable?

[email protected](o)

31true

32end

33end

34end

35

36defnearby(object,max_distance)

37cx,cy=object.location

38hx,hy=cx+max_distance,cy+max_distance

39#Fast,roughresults

[email protected]_range(

41AxisAlignedBoundingBox.new([cx,cy],[hx,hy]))

42#Siftthroughtoselectfine-grainedresults

43results.selectdo|o|

44o!=object&&

45Utils.distance_between(

46o.x,o.y,object.x,object.y)<=max_distance

47end

48end

49

50defquery_range(box)

[email protected]_range(box)

52end

53end

Anoldfashionedarrayofallobjectsisstillused,becausewestillneedtoloopthrougheverythingandinvokeGameObject#update.ObjectPool#query_rangewasintroducedtoquicklygrabobjectsthathavetoberenderedonscreen,andObjectPool#nearbynowqueriestreeandmeasuresdistancesonlyonroughresultset.

Thisishowwewillrenderthingsfromnowon:classPlayState<GameState

#...

defdraw

[email protected]

[email protected]

off_x=$window.width/2-cam_x

off_y=$window.height/2-cam_y

[email protected]

x1,x2,y1,y2=viewport

box=AxisAlignedBoundingBox.new(

[x1+(x2-x1)/2,y1+(y2-y1)/2],

[x1-Map::TILE_SIZE,y1-Map::TILE_SIZE])

$window.translate(off_x,off_y)do

[email protected]

$window.scale(zoom,zoom,cam_x,cam_y)do

@map.draw(viewport)

@object_pool.query_range(box).mapdo|o|

Page 152: Developing Games With Ruby: For those who write code for living

o.draw(viewport)

end

end

end

@camera.draw_crosshair

@radar.draw

end

#...

end

MovingObjectsInQuadTreeThereisonemoreerrandwenowhavetotakecareof.Everythingworksfinewhenthingsarestatic,butwhentanksandbulletsmove,weneedtoupdatetheminourQuadTree.That’swhyObjectPoolhastree_removeandtree_insert,whicharecalledfromGameObject#move.Fromnowon,theonlywaytochangeobject’slocationwillbebyusingGameObject#move:classGameObject

attr_reader:x,:y,:location,:components

definitialize(object_pool,x,y)

@x,@y=x,y

@location=[x,y]

@components=[]

@object_pool=object_pool

@object_pool.add(self)

end

defmove(new_x,new_y)

returnifnew_x==@x&&new_y==@y

@object_pool.tree_remove(self)

@x=new_x

@y=new_y

@location=[new_x,new_y]

@object_pool.tree_insert(self)

end

#...

end

Atthispointwehavetogothroughallthegameobjectsandchangehowtheyinitializetheirbaseclassandupdatexandycoordinates,butwewon’tcoverthathere.Ifindoubt,refertocodeat10-partitioning.

Finally,FPSisbacktostable60andwecanfocusongameplayagain.

Page 153: Developing Games With Ruby: For those who write code for living

Spatialpartitioningsavestheday

Page 154: Developing Games With Ruby: For those who write code for living

ImplementingPowerups

Gamewouldbecomemorestrategiciftherewerewaystorepairyourdamagedtank,boostit’sspeedorincreaserateoffirebypickingupvariouspowerups.Thisshouldnotbetoodifficulttoimplement.Wewillusesomeoftheseimages:

Powerups

Fornow,therewillbefourkindsofpowerups:

1. Repairdamage.Wrenchbadgewillrestoredamagedtank’shealthbackto100whenpickedup.

2. Healthboost.Green+1badgewilladd25health,upto200total,ifyoukeeppickingthemup.

3. Fireboost.Doublebulletbadgewillincreasereloadspeedby25%,upto200%ifyoukeeppickingthemup.

4. Speedboost.Airplanebadgewillincreasemovementspeedby10%,upto150%ifyoukeeppickingthemup

Thesepowerupswillbeplacedrandomlyaroundthemap,andwillautomaticallyrespawn30secondsafterpickup.

ImplementingBasePowerupBeforerushingforwardtoimplementthis,wehavetodosomeresearchandthinkhowtoelegantlyintegratethisintothewholegame.First,let’sagreethatPowerupisaGameObject.Itwillhavegraphics,soundsandit’scoordinates.EffectscanbyappliedbyharnessingGameObject#on_collision-whenTankcollideswithPowerup,itgetsit.11-powerups/entities/powerups/powerup.rb

1classPowerup<GameObject

2definitialize(object_pool,x,y)

3super

4PowerupGraphics.new(self,graphics)

5end

6

7defbox

8[x-8,y-8,

9x+8,y-8,

10x+8,y+8,

11x-8,y+8]

12end

13

14defon_collision(object)

Page 155: Developing Games With Ruby: For those who write code for living

15ifpickup(object)

16PowerupSounds.play(object,object_pool.camera)

17remove

18end

19end

20

21defpickup(object)

22#overrideandimplementapplication

23end

24

25defremove

26object_pool.powerup_respawn_queue.enqueue(

27respawn_delay,

28self.class,x,y)

29mark_for_removal

30end

31

32defrespawn_delay

3330

34end

35end

IgnorePowerup#remove,wewillgettoitwhenimplementingPowerupRespawnQueue.Therestshouldbestraightforward.

ImplementingPowerupGraphicsAllpowerupswillusethesamespritesheet,sotherecouldbeasinglePowerupGraphicsclassthatwillberenderinggivenspritetype.Wewillusegosu-texture-packergem,sincespritesheetisconvenientlypackedalready.11-powerups/entities/components/powerup_graphics.rb

1classPowerupGraphics<Component

2definitialize(object,type)

3super(object)

4@type=type

5end

6

7defdraw(viewport)

8image.draw(x-12,y-12,1)

9Utils.mark_corners(object.box)if$debug

10end

11

12private

13

14defimage

15@image||=images.frame("#{@type}.png")

16end

17

18defimages

19@@images||=Gosu::TexturePacker.load_json(

20$window,Utils.media_path('pickups.json'))

21end

22end

ImplementingPowerupSoundsIt’sevensimplerwithsounds.Allpowerupswillemitamellow“bleep”whenpickedup,soPowerupSoundscanbecompletelystatic,likeExplosionSoundsorBulletSounds:11-powerups/entities/components/powerup_sounds.rb

1classPowerupSounds

2class<<self

3defplay(object,camera)

4volume,pan=Utils.volume_and_pan(object,camera)

Page 156: Developing Games With Ruby: For those who write code for living

5sound.play(object.object_id,pan,volume)

6end

7

8private

9

10defsound

11@@sound||=StereoSample.new(

12$window,Utils.media_path('powerup.mp3'))

13end

14end

15end

ImplementingRepairDamagePowerupRepairingbrokentankisprobablythemostimportantpowerupofthemall,solet’simplementitfirst:11-powerups/entities/powerups/repair_powerup.rb

1classRepairPowerup<Powerup

2defpickup(object)

3ifobject.class==Tank

4ifobject.health.health<100

5object.health.restore

6end

7true

8end

9end

10

11defgraphics

12:repair

13end

14end

Thiswasincrediblysimple.Health#restorealreadyexistedsincewehadtorespawnourtanks.Wecanonlyhopeotherpowerupsareassimpletoimplementasthisone.

ImplementingHealthBoostRepairingdamageisgreat,buthowaboutboostingsomeextrahealthforupcomingbattles?Healthboosttotherescue:11-powerups/entities/powerups/health_powerup.rb

1classHealthPowerup<Powerup

2defpickup(object)

3ifobject.class==Tank

4object.health.increase(25)

5true

6end

7end

8

9defgraphics

10:life_up

11end

12end

ThistimewehavetoimplementHealth#increase,butitisprettysimple:classHealth<Component

#...

defincrease(amount)

@health=[@health+25,@initial_health*2].min

@health_updated=true

end

#...

end

Page 157: Developing Games With Ruby: For those who write code for living

SinceTankhas@initial_healthequalto100,increasinghealthwon’tgoover200,whichisexactlywhatwewant.

ImplementingFireRateBoostHowaboutboostingtank’sfirerate?11-powerups/entities/powerups/fire_rate_powerup.rb

1classFireRatePowerup<Powerup

2defpickup(object)

3ifobject.class==Tank

4ifobject.fire_rate_modifier<2

5object.fire_rate_modifier+=0.25

6end

7true

8end

9end

10

11defgraphics

12:straight_gun

13end

14end

Weneedtointroduce@fire_rate_modifierinTankclassanduseitwhencallingTank#can_shoot?:classTank<GameObject

#...

attr_accessor:fire_rate_modifier

#...

defcan_shoot?

Gosu.milliseconds-(@last_shot||0)>

(SHOOT_DELAY/@fire_rate_modifier)

end

#...

defreset_modifiers

@fire_rate_modifier=1

end

#...

end

Tank#reset_modifiershouldbecalledwhenrespawning,sincewewanttankstolosetheirpowerupswhentheydie.ItcanbedoneinTankHealth#after_death:classTankHealth<Health

#...

defafter_death

object.reset_modifiers

#...

end

end

ImplementingTankSpeedBoostTankspeedboostisverysimilartofireratepowerup:11-powerups/entities/powerups/tank_speed_powerup.rb

1classTankSpeedPowerup<Powerup

2defpickup(object)

3ifobject.class==Tank

4ifobject.speed_modifier<1.5

5object.speed_modifier+=0.10

6end

7true

8end

9end

Page 158: Developing Games With Ruby: For those who write code for living

10

11defgraphics

12:wingman

13end

14end

Wehavetoadd@speed_modifiertoTankclassanduseitinTankPhysics#updatewhencalculatingmovementdistance.#11-powerups/entities/tank.rb

classTank<GameObject

#...

attr_accessor:speed_modifier

#...

defreset_modifiers

#...

@speed_modifier=1

end

#...

end

#11-powerups/entities/components/tank_physics.rb

classTankPhysics<Component

#...

defupdate

#...

new_x,new_y=x,y

speed=apply_movement_penalty(@speed)

shift=Utils.adjust_speed(speed)*object.speed_modifier

#...

end

#...

end

Camera#updatehasalsorefertoTank#speed_modifier,otherwisetheoperatorwillfailtocatchupandcamerawillbelaggingbehind.classCamera

#...

defupdate

#...

shift=Utils.adjust_speed(

@target.physics.speed).floor*

@target.speed_modifier+1

#...

end

#...

end

SpawningPowerupsOnMapPowerupsareimplemented,butnotyetspawned.Wewillspawn20-30randompowerupswhengeneratingthemap:classMap

#...

definitialize(object_pool)

#...

generate_powerups

end

#...

defgenerate_powerups

pups=0

target_pups=rand(20..30)

whilepups<target_pupsdo

x=rand(0..MAP_WIDTH*TILE_SIZE)

y=rand(0..MAP_HEIGHT*TILE_SIZE)

iftile_at(x,y)!=@water

random_powerup.new(@object_pool,x,y)

pups+=1

end

end

Page 159: Developing Games With Ruby: For those who write code for living

end

defrandom_powerup

[HealthPowerup,

RepairPowerup,

FireRatePowerup,

TankSpeedPowerup].sample

end

#...

end

Thecodeisverysimilartogeneratingboxes.It’sprobablynotthebestwaytodistributepowerupsonmap,butitwillhavetodofornow.

RespawningPowerupsAfterPickupWhenwepickupapowerup,wewantittoreappearinsamespot30secondslater.Athought“wecanstartanewThreadwithsleepandinitializethesamepowerupthere”soundsverybad,butIhaditforafewseconds.ThenPowerupRespawnQueuewasborn.

First,let’srecallhowPowerup#removemethodlookslike:classPowerup<GameObject

#...

defremove

object_pool.powerup_respawn_queue.enqueue(

respawn_delay,

self.class,x,y)

mark_for_removal

end

#...

end

Powerupenqueuesitselfforrespawnwhenpickedup,providingit’sclassandcoordinates.PowerupRespawnQueueholdsthisdataandrespawnspowerupsatrighttimewithhelpofObjectPool:11-powerups/entities/powerups/powerup_respawn_queue.rb

1classPowerupRespawnQueue

2RESPAWN_DELAY=1000

3definitialize

4@respawn_queue={}

5@last_respawn=Gosu.milliseconds

6end

7

8defenqueue(delay_seconds,type,x,y)

9respawn_at=Gosu.milliseconds+delay_seconds*1000

10@respawn_queue[respawn_at.to_i]=[type,x,y]

11end

12

13defrespawn(object_pool)

14now=Gosu.milliseconds

15returnifnow-@last_respawn<RESPAWN_DELAY

16@respawn_queue.keys.eachdo|k|

17nextifk>now#notyet

18type,x,y=@respawn_queue.delete(k)

19type.new(object_pool,x,y)

20end

21@last_respawn=now

22end

23end

PowerupRespawnQeueue#respawniscalledfromObjectPool#update_all,butisthrottledtorunoncepersecondforbetterperformance.classObjectPool

#...

Page 160: Developing Games With Ruby: For those who write code for living

attr_accessor:powerup_respawn_queue

#...

defupdate_all

#...

@powerup_respawn_queue.respawn(self)

end

#...

end

Thisisit,thegameshouldnowcontainrandomlyplacedpowerupsthatrespawn30secondsafterpickedup.Timetoenjoytheresult.

Page 161: Developing Games With Ruby: For those who write code for living

Playingwithpowerups

Wehaven’tdoneanychangestoAIthough,thatmeansenemieswillonlybepickingthosepowerupsbyaccident,sonowyouhaveasignificantadvantageandthegamehassuddenlybecametooeasytoplay.Don’tworry,wewillbefixingthatwhenoverhaulingtheAI.

Page 162: Developing Games With Ruby: For those who write code for living

ImplementingHeadsUpDisplay

Inordertoknowwhat’shappening,weneedsomesortofHUD.Wealreadyhavecrosshairandradar,buttheyarescatteredaroundincode.Nowwewanttodisplayactivepowerupmodifiers,soyouwouldknowwhatisyourfirerateandspeed,andifit’sworthgettingonemorepowerupbeforegoingintothenextfight.

DesignConsiderationsWhilecreatingourHUDclass,wewillhavetostartbuildinggamestats,becausewewanttodisplaynumberofkillsourtankmade.Statstopicwillbecoveredindepthlater,butfornowlet’[email protected],whichwewanttodrawintop-leftcornerofthescreen,alongwithplayerhealthandmodifiervalues.

HUDwillalsoberesponsiblefordrawingcrosshairandradar.

RenderingTextWithCustomFontPreviously,alltextwererenderedwithGosu.default_font_name,andwewantsomethingmorefancyandmorethematic,probablyadirtystencilbasedfontlikethisone:

ArmaliteRiflefont

Andonemorefancyfontwillmakeourgametitlelookgood.Toobadwedon’thaveatitleyet,but“TanksPrototype”writeninathematicwaystilllooksprettygood.

Tohaveconvenientaccesstothesefonts,wewilladdahelpermethodsinUtils:moduleUtils

#...

defself.title_font

media_path('top_secret.ttf')

end

defself.main_font

media_path('armalite_rifle.ttf')

Page 163: Developing Games With Ruby: For those who write code for living

end

#...

end

UseitinsteadofGosu.default_font_name:size=20

Gosu::Image.from_text($window,"Yourtext",Utils.main_font,size)

ImplementingHUDClassAfterwehaveputeverythingtogether,wewillgetHUDclass:12-stats/entities/hud.rb

1classHUD

2attr_accessor:active

3definitialize(object_pool,tank)

4@object_pool=object_pool

5@tank=tank

6@radar=Radar.new(@object_pool,tank)

7end

8

9defplayer=(tank)

10@tank=tank

[email protected]=tank

12end

13

14defupdate

[email protected]

16end

17

18defhealth_image

[email protected]?||@tank.health.health!=@health

20@[email protected]

21@health_image=Gosu::Image.from_text(

22$window,"Health:#{@health}",Utils.main_font,20)

23end

24@health_image

25end

26

27defstats_image

[email protected]

29if@stats_image.nil?||stats.changed_at<=Gosu.milliseconds

30@stats_image=Gosu::Image.from_text(

31$window,"Kills:#{stats.kills}",Utils.main_font,20)

32end

33@stats_image

34end

35

36deffire_rate_image

[email protected]_rate_modifier>1

38if@[email protected]_rate_modifier

39@[email protected]_rate_modifier

40@fire_rate_image=Gosu::Image.from_text(

41$window,"Firerate:#{@fire_rate.round(2)}X",

42Utils.main_font,20)

43end

44else

45@fire_rate_image=nil

46end

47@fire_rate_image

48end

49

50defspeed_image

[email protected]_modifier>1

52if@[email protected]_modifier

53@[email protected]_modifier

54@speed_image=Gosu::Image.from_text(

55$window,"Speed:#{@speed.round(2)}X",

56Utils.main_font,20)

57end

58else

59@speed_image=nil

Page 164: Developing Games With Ruby: For those who write code for living

60end

61@speed_image

62end

63

64defdraw

65if@active

66@object_pool.camera.draw_crosshair

67end

[email protected]

69offset=20

70health_image.draw(20,offset,1000)

71stats_image.draw(20,offset+=30,1000)

72iffire_rate_image

73fire_rate_image.draw(20,offset+=30,1000)

74end

75ifspeed_image

76speed_image.draw(20,offset+=30,1000)

77end

78end

79end

Touseit,weneedtohookintoPlayState:classPlayState<GameState

#...

definitialize

#...

@hud=HUD.new(@object_pool,@tank)

end

defupdate

#...

@hud.update

end

defdraw

#...

@hud.draw

end

#...

end

[email protected],youshouldgetaneatviewshowinginterestingthingsintop-leftcornerofthescreen:

Page 165: Developing Games With Ruby: For those who write code for living

ShinynewHUD

Page 166: Developing Games With Ruby: For those who write code for living

ImplementingGameStatistics

Gameslikeonewearebuildingareallaboutcompetition,andyoucannotcompeteifyoudon’tknowthescore.Letusintroduceaclassthatwillberesponsibleforkeepingtabsonvariousstatisticsofeverytank.12-stats/misc/stats.rb

1classStats

2attr_reader:name,:kills,:deaths,:shots,:changed_at

3definitialize(name)

4@name=name

5@kills=@deaths=@shots=@damage=@damage_dealt=0

6changed

7end

8

9defadd_kill(amount=1)

10@kills+=amount

11changed

12end

13

14defadd_death

15@deaths+=1

16changed

17end

18

19defadd_shot

20@shots+=1

21changed

22end

23

24defadd_damage(amount)

25@damage+=amount

26changed

27end

28

29defdamage

[email protected]

31end

32

33defadd_damage_dealt(amount)

34@damage_dealt+=amount

35changed

36end

37

38defdamage_dealt

39@damage_dealt.round

40end

41

42defto_s

43"[kills:#{@kills},"\

44"deaths:#{@deaths},"\

45"shots:#{@shots},"\

46"damage:#{damage},"\

47"damage_dealt:#{damage_dealt}]"

48end

49

50private

51

52defchanged

53@changed_at=Gosu.milliseconds

54end

55end

Page 167: Developing Games With Ruby: For those who write code for living

WhilebuildingtheHUD,weestablishedthatStatsshouldbelongtoTank#input,becauseitdefineswhoiscontrollingthetank.So,everyinstanceofPlayerInputandAiInputhastohaveit’sownStats:#12-stats/entities/components/player_input.rb

classPlayerInput<Component

#...

attr_reader:stats

definitialize(name,camera,object_pool)

#...

@stats=Stats.new(name)

end

#...

defon_damage(amount)

@stats.add_damage(amount)

end

#...

end

#12-stats/entities/components/ai_input.rb

classAiInput<Component

#...

attr_reader:stats

definitialize(name,object_pool)

#...

@stats=Stats.new(name)

end

defon_damage(amount)

#...

@stats.add_damage(amount)

end

end

ThatitchtoextractabaseclassfromPlayerInputandAiInputisgettingstronger,butwewillhavetoresisttheurge,fornow.

TrackingKills,DeathsandDamageTobegintrackingkills,weneedtoknowwhomdoeseverybulletbelongto.Bulletalreadyhassourceattribute,whichcontainsthetankthatfiredit,therewillbenotroubletofindoutwhowastheshooterwhenbulletgetsadirecthit.Buthowaboutexplosions?Bulletsthathitthegroundnearbyatankdealsindirectdamagefromtheexplosion.

Solutionissimple,weneedtopassthesourceoftheBullettotheExplosionwhenit’sbeinginitialized.classBullet<GameObject

#...

defexplode

Explosion.new(object_pool,@x,@y,@source)

#...

end

#...

end

MakingDamagePersonalNowthatwehavethesourceofeveryBulletandExplosiontheytrigger,wecanstartpassingthecauseofdamagetoHealth#inflict_damageandincrementingtheappropriatestats.#12-stats/entities/components/health.rb

classHealth<Component

Page 168: Developing Games With Ruby: For those who write code for living

#...

definflict_damage(amount,cause)

if@health>0

@health_updated=true

ifobject.respond_to?(:input)

object.input.stats.add_damage(amount)

#Don'tcountdamagetotreesandboxes

ifcause.respond_to?(:input)&&cause!=object

cause.input.stats.add_damage_dealt(amount)

end

end

@health=[@health-amount.to_i,0].max

after_death(cause)ifdead?

end

end

#...

end

#12-stats/entities/components/tank_health.rb

classTankHealth<Health

#...

defafter_death(cause)

#...

object.input.stats.add_death

kill=object!=cause?1:-1

cause.input.stats.add_kill(kill)

#...

end

#...

end

TrackingDamageFromChainReactionsThereisonemorewaytocausedamage.Whenyoushootatree,boxorbarrel,itexplodes,probablytriggeringachainreactionofexplosionsaroundit.Ifthoseexplosionskillsomebody,itwouldonlybefairtoaccountthatkillforthetankthattriggeredthischainreaction.

Tosolvethis,simplypassthecauseofdeathtotheExplosionthatgetstriggeredafterwards.#12-stats/entities/components/health.rb

classHealth<Component

#...

defafter_death(cause)

if@explodes

Thread.newdo

#...

Explosion.new(@object_pool,x,y,cause)

#...

end

#...

end

end

end

#12-stats/entities/components/tank_health.rb

classTankHealth<Health

#...

defafter_death(cause)

#...

Thread.newdo

#...

Explosion.new(@object_pool,x,y,cause)

end

end

end

Noweverybitofdamagegetsaccountedfor.

Page 169: Developing Games With Ruby: For those who write code for living

DisplayingGameScoreHavingallthedataisuselessunlesswedisplayitsomehow.Forthis,let’srethinkourgamestates.NowwehaveMenuStateandPlayState.Bothofthemcanswitchoneintoanother.WhatifweintroducedaPauseState,whichwouldfreezethegameanddisplaythelistofalltanksalongwiththeirkills.ThenMenuStatewouldswitchtoPlayState,andfromPlayStateyouwouldbeabletogettoPauseState.

Let’sbeginbyimplementingScoreDisplay,thatwouldprintasortedlistoftankkillsalongwiththeirnames.12-stats/entities/score_display.rb

1classScoreDisplay

2definitialize(object_pool)

3tanks=object_pool.objects.selectdo|o|

4o.class==Tank

5end

6stats=tanks.map(&:input).map(&:stats)

7stats.sort!do|stat1,stat2|

8stat2.kills<=>stat1.kills

9end

10create_stats_image(stats)

11end

12

13defcreate_stats_image(stats)

14text=stats.mapdo|stat|

15"#{stat.kills}:#{stat.name}"

16end.join("\n")

17@stats_image=Gosu::Image.from_text(

18$window,text,Utils.main_font,30)

19end

20

21defdraw

22@stats_image.draw(

23$window.width/2-@stats_image.width/2,

24$window.height/4+30,

251000)

26end

27end

WewillhavetoinitializeScoreDisplayeverytimewhenwewanttoshowtheupdatedscore.TimetocreatethePauseStatethatwouldshowthescore.12-stats/game_states/pause_state.rb

1require'singleton'

2classPauseState<GameState

3includeSingleton

4attr_accessor:play_state

5

6definitialize

7@message=Gosu::Image.from_text(

8$window,"GamePaused",

9Utils.title_font,60)

10end

11

12defenter

13music.play(true)

14music.volume=1

15@score_display=ScoreDisplay.new(@play_state.object_pool)

16@mouse_coords=[$window.mouse_x,$window.mouse_y]

17end

18

19defleave

20music.volume=0

21music.stop

22$window.mouse_x,$window.mouse_y=@mouse_coords

23end

Page 170: Developing Games With Ruby: For those who write code for living

24

25defmusic

26@@music||=Gosu::Song.new(

27$window,Utils.media_path('menu_music.mp3'))

28end

29

30defdraw

31@play_state.draw

[email protected](

33$window.width/[email protected]/2,

34$window.height/[email protected],

351000)

36@score_display.draw

37end

38

39defbutton_down(id)

40$window.closeifid==Gosu::KbQ

41ifid==Gosu::KbC&&@play_state

42GameState.switch(@play_state)

43end

44ifid==Gosu::KbEscape

45GameState.switch(@play_state)

46end

47end

48end

YouwillnoticethatPauseStateinvokesPlayState#draw,butwithoutPlayState#updatethiswillbeastillimage.Wemakesurewehidethecrosshairandrestorepreviousmouselocationwhenresumingplaystate.Thatwayplayerwouldnotbeabletocheatbypausingthegame,targetingthetankwhilenothingmovesandthenunpausingreadytodealdamage.OurHUDhadattr_accessor:activeexactlyforthisreason,butweneedtoswitchitonandoffinPlayState#enterandPlayState#leave.classPlayState<GameState

#...

defbutton_down(id)

#...

ifid==Gosu::KbEscape

pause=PauseState.instance

pause.play_state=self

GameState.switch(pause)

end

#...

end

#...

defleave

StereoSample.stop_all

@hud.active=false

end

defenter

@hud.active=true

end

#...

end

Timeforatestdrive.

Page 171: Developing Games With Ruby: For those who write code for living

Pausingthegametoseethescore

Fornow,scoringmostkillsisrelativelysimple.ThisshouldchangewhenwewilltellenemyAItocollectpowerupswhenappropriate.

Page 172: Developing Games With Ruby: For those who write code for living

BuildingAdvancedAI

TheAIwehaverightnowcankicksomeass,butitistoodumbforanyseasonedgamertocompetewith.Thisisthelistofcurrentflaws:

1. Itdoesnotnavigatewell,getsstuckamongtreesorsomewherenearwater.2. Itisnotawareofpowerups.3. Itcoulddobetterjobatshooting.4. It’sfieldofvisionistoosmall,comparedtoplayer’s,whoisequippedwithradar.

Wewilltackletheseissuesincurrentchapter.

ImprovingTankNavigationTanksshouldn’tbehavelikeRoombas,randomlydrivingaroundandbumpingintothings.Theycouldbenavigatinglikethis:

1. ConsultwithcurrentAIstateandfindorupdatedestinationpoint.2. Ifdestinationhaschanged,calculateshortestpathtodestination.3. Movealongthecalculatedpath.4. Repeat.

Ifthislookseasy,letmeassureyou,itwouldprobablyrequirerewritingthemajorityofAIandMapcodewehaveatthispoint,anditisprettytrickytoimplementwithprocedurallygeneratedmaps,becausenormallyyouwoulduseamapeditortosetupwaypoints,navigationmeshorotherhintsforAIsoitdoesn’tgetstuck.Sometimesitisbettertohavesomethingworkingimperfectlyoveraperfectsolutionthatneverhappens,thuswewillusesimplethingsthatwillmakeasmuchimpactaspossiblewithoutrewritinghalfofthecode.

GeneratingFriendlierMaps

Oneofmainreasonswhytanksgetstuckisbadplacementofspawnpoints.Theydon’ttaketreesandboxesintoaccount,soenemytankcanspawninthemiddleofaforest,withnochanceofgettingoutwithoutblowingthingsup.AsimplefixwouldbetoconsultwithObjectPoolbeforeplacingaspawnpointonlywheretherearenoothergameobjectsaroundin,say,150pixelradius:classMap

#...

deffind_spawn_point

whiletrue

x=rand(0..MAP_WIDTH*TILE_SIZE)

y=rand(0..MAP_HEIGHT*TILE_SIZE)

ifcan_move_to?(x,y)&&

@object_pool.nearby_point(x,y,150).empty?

return[x,y]

end

end

Page 173: Developing Games With Ruby: For those who write code for living

end

#...

end

Howaboutpowerups?Theycanalsospawninthemiddleofaforest,andwhiletanksarenotseekingthemyet,wewillbeimplementingthisbehavior,andleadingtanksintowildernessoftreesisnotthebestideaever.Let’sfixittoo:classMap

#...

defgenerate_powerups

pups=0

target_pups=rand(20..30)

whilepups<target_pupsdo

x=rand(0..MAP_WIDTH*TILE_SIZE)

y=rand(0..MAP_HEIGHT*TILE_SIZE)

iftile_at(x,y)!=@water&&

@object_pool.nearby_point(x,y,150).empty?

random_powerup.new(@object_pool,x,y)

pups+=1

end

end

end

#...

end

Wecouldalsoreducetreecount,butthatwouldmakethemaplookworse,sowearegoingtokeepthisinourpocketasameanoflastresort.

ImplementingDemoStateToObserveAIProbablythebestwaytofigureoutifourAIisanygoodistotargetoneofAItankswithourgamecameraandseehowitplays.ItwillgiveusagreatvisualtestingtoolthatwillallowtweakingAIsettingsandseeingiftheyperformbetterorworse.ForthatwewillintroduceDemoStatewhereonlyAItankswillbepresentinthemap,andwewillbeabletoswitchcamerafromonetanktoanother.

DemoStateisverysimilartoPlayState,themaindifferenceisthatthereisnoplayer.Wewillextractcreate_tanksmethodthatwillbeoverriddeninDemoState.classPlayState<GameState

attr_accessor:update_interval,:object_pool,:tank

definitialize

#...

@camera=Camera.new

@object_pool.camera=@camera

create_tanks(4)

end

#...

private

defcreate_tanks(amount)

@map.spawn_points(amount*3)

@tank=Tank.new(@object_pool,

PlayerInput.new('Player',@camera,@object_pool))

amount.timesdo|i|

Tank.new(@object_pool,AiInput.new(

@names.random,@object_pool))

end

@camera.target=@tank

@hud=HUD.new(@object_pool,@tank)

end

#...

end

Page 174: Developing Games With Ruby: For those who write code for living

Wewillalsowanttodisplayasmallerversionofscoreintop-rightcornerofthescreen,solet’saddsomeadjustmentstoScoreDisplay:classScoreDisplay

definitialize(object_pool,font_size=30)

@font_size=font_size

#...

end

defcreate_stats_image(stats)

#...

@stats_image=Gosu::Image.from_text(

$window,text,Utils.main_font,@font_size)

end

#...

defdraw_top_right

@stats_image.draw(

$window.width-@stats_image.width-20,

20,

1000)

end

end

AndhereistheextendedDemoState:13-advanced-ai/game_states/demo_state.rb

1classDemoState<PlayState

2attr_accessor:tank

3

4defenter

5#PreventreactivatingHUD

6end

7

8defupdate

9super

10@score_display=ScoreDisplay.new(

11object_pool,20)

12end

13

14defdraw

15super

16@score_display.draw_top_right

17end

18

19defbutton_down(id)

20super

21ifid==Gosu::KbSpace

[email protected]|t|

[email protected]

24end.sample

25switch_to_tank(target_tank)

26end

27end

28

29private

30

31defcreate_tanks(amount)

[email protected]_points(amount*3)

33@tanks=[]

34amount.timesdo|i|

35@tanks<<Tank.new(@object_pool,AiInput.new(

[email protected],@object_pool))

37end

[email protected]

39@hud=HUD.new(@object_pool,target_tank)

[email protected]=false

41switch_to_tank(target_tank)

42end

43

44defswitch_to_tank(tank)

[email protected]=tank

[email protected]=tank

47self.tank=tank

Page 175: Developing Games With Ruby: For those who write code for living

48end

49end

TohaveapossibilitytoenterDemoState,weneedtochangeMenuStatealittle:classMenuState<GameState

#...

defupdate

text="Q:Quit\nN:NewGame\nD:Demo"

#...

end

#...

defbutton_down(id)

#...

ifid==Gosu::KbD

@play_state=DemoState.new

GameState.switch(@play_state)

end

end

end

Now,mainmenuhastheoptiontoenterdemostate:

Page 176: Developing Games With Ruby: For those who write code for living

Overhauledmainmenu

Page 177: Developing Games With Ruby: For those who write code for living

ObservingAIindemostate

VisualAIDebuggingAfterwatchingAIbehaviorindemomodeforawhile,Iwasterrified.Whenplayinggamenormally,youusuallyseetanksin“fighting”state,whichworksprettywell,butwhentanksgoroaming,it’sacompletedisaster.Theygetstuckeasily,theydon’tgotoofarfromtheoriginallocation,theywaittoomuch.

Somethingscouldbeimprovedjustbychangingwait_time,turn_timeanddrive_timetodifferentvalues,butwecertainlyhavetodobiggerchangesthanthat.

Ontheotherhand,“observeAIinaction,tweak,repeat”cycleprovedtobeveryeffective,Iwilldefinitelyusethistechniqueinallmyfuturegames.

Tomakevisualdebuggingeasier,buildyourselfsometooling.Onewaytodoitistohaveglobal$debugvariablewhichyoucantogglebypressingsomebutton:classPlayState<GameState

#...

defbutton_down(id)

#...

ifid==Gosu::KbF1

$debug=!$debug

end

#...

end

Page 178: Developing Games With Ruby: For those who write code for living

#...

end

Thenaddextradrawinginstructionstoyourobjectsandtheircomponents.Forexample,thiswillmakeTankdisplayit’scurrentTankMotionStateimplementationclassbeneathit:classTankMotionFSM

#...

defset_state(state)

#...

if$debug

@image=Gosu::Image.from_text(

$window,state.class.to_s,

Gosu.default_font_name,18)

end

end

#...

defdraw(viewport)

if$debug

@image&&@image.draw(

@[email protected]/2,

@[email protected]/2-

@image.height,100)

end

end

#...

end

Tomarktank’sdesiredgunangleasbluelineandactualgunangleasredline,youcandothis:classAiGun

#...

defdraw(viewport)

if$debug

color=Gosu::Color::BLUE

x,[email protected],@object.y

t_x,t_y=Utils.point_at_distance(x,y,@desired_gun_angle,

BulletPhysics::MAX_DIST)

$window.draw_line(x,y,color,t_x,t_y,color,1001)

color=Gosu::Color::RED

t_x,t_y=Utils.point_at_distance(x,y,@object.gun_angle,

BulletPhysics::MAX_DIST)

$window.draw_line(x,y,color,t_x,t_y,color,1000)

end

end

#...

end

Finally,youcanautomaticallymarkcollisionboxcornersonyourgraphicscomponents.Let’stakeBoxGraphicsforexample:#13-advanced-ai/misc/utils.rb

moduleUtils

#...

defself.mark_corners(box)

i=0

box.each_slice(2)do|x,y|

color=DEBUG_COLORS[i]

$window.draw_triangle(

x-3,y-3,color,

x,y,color,

x+3,y-3,color,

100)

i=(i+1)%4

end

end

#...

end

#13-advanced-ai/entities/components/box_graphics.rb

classBoxGraphics<Component

#..

defdraw(viewport)

Page 179: Developing Games With Ruby: For those who write code for living

@box.draw_rot(x,y,0,object.angle)

Utils.mark_corners(object.box)if$debug

end

#...

end

Asadeveloper,youcanmakeyourselfseenearlyeverythingyouwant,makeuseofit.

Page 180: Developing Games With Ruby: For those who write code for living

VisualdebuggingofAIbehavior

Althoughithurtstheframeratealittle,itisveryusefulwhenbuildingnotonlyAI,buttherestofthegametoo.UsingthisvisualdebuggingtogetherwithDemomode,youcantweakalltheAIvaluestomakeitshootmoreoften,fightbetter,andbemoreagile.Wewon’tgothroughthisminortuning,butyoucanfindthechangesbyviewingchangesintroducedin13-advanced-ai.

MakingAICollectPowerupsToevenouttheodds,wehavetomakeAIseekpowerupswhentheyarerequired.Thelogicbehinditcanbeimplementedusingacoupleofsimplesteps:

1. AIwouldknowwhatpowerupsarecurrentlyneeded.Thismayvaryfromstatetostate,i.e.speedandfireratepowerupsarenicetohavewhenroaming,butnotthatimportantwhenfleeingaftertakingheavydamage.Andwedon’twantAItowastetimeandcollectspeedpowerupswhenspeedmodifierisalreadymaxedout.

2. AiVisionwouldreturnclosestvisiblepowerup,filteredbyacceptablepoweruptypes.3. SomeTankMotionStateimplementationwouldadjusttankdirectiontowardsclosest

visiblepowerupinchange_directionmethod.

FindingPowerupsInSight

Page 181: Developing Games With Ruby: For those who write code for living

ToimplementchangesinAiVision,wewillintroduceclosest_powerupmethod.Itwillqueryobjectsinsightandfilterthemoutbytheirclassanddistance.classAiVision

#...

POWERUP_CACHE_TIMEOUT=50

#...

defclosest_powerup(*suitable)

now=Gosu.milliseconds

@closest_powerup=nil

ifnow-(@powerup_cache_updated_at||=0)>POWERUP_CACHE_TIMEOUT

@closest_powerup=nil

@powerup_cache_updated_at=now

end

@closest_powerup||=find_closest_powerup(*suitable)

end

private

deffind_closest_powerup(*suitable)

ifsuitable.empty?

suitable=[FireRatePowerup,

HealthPowerup,

RepairPowerup,

TankSpeedPowerup]

end

@in_sight.selectdo|o|

suitable.include?(o.class)

end.sortdo|a,b|

x,[email protected],@viewer.y

d1=Utils.distance_between(x,y,a.x,a.y)

d2=Utils.distance_between(x,y,b.x,b.y)

d1<=>d2

end.first

end

#...

end

ItisverysimilartoAiVision#closest_tank,andpartsshouldprobablybeextractedtokeepthecodedry,butwewillnotbother.

SeekingPowerupsWhileRoaming

Roamingiswhenmostpickingshouldhappen,becauseTankseesnoenemiesinsightandneedstoprepareforupcomingbattles.Let’sseehowcanweimplementthisbehaviorwhileleveragingthenewlymadeAiVision#closest_powerup:classTankRoamingState<TankMotionState

#...

defrequired_powerups

required=[]

[email protected]

[email protected]_rate_modifier<2&&health>50

required<<FireRatePowerup

end

[email protected]_modifier<1.5&&health>50

required<<TankSpeedPowerup

end

ifhealth<100

required<<RepairPowerup

end

ifhealth<190

required<<HealthPowerup

end

required

end

defchange_direction

[email protected]_powerup(

*required_powerups)

ifclosest_powerup

@seeking_powerup=true

Page 182: Developing Games With Ruby: For those who write code for living

angle=Utils.angle_between(

@object.x,@object.y,

closest_powerup.x,closest_powerup.y)

@object.physics.change_direction(

angle-angle%45)

else

@seeking_powerup=false

#...chooserandomdirection

end

@changed_direction_at=Gosu.milliseconds

@will_keep_direction_for=turn_time

end

#...

defturn_time

if@seeking_powerup

rand(100..300)

else

rand(1000..3000)

end

end

end

Itissimpleasthat,andourAItanksarenowgettingbuffedontheirsparetime.

SeekingHealthPowerupsAfterHeavyDamageToseekhealthwhendamaged,weneedtochangeTankFleeingState#change_direction:classTankFleeingState<TankMotionState

#...

defchange_direction

[email protected]_powerup(

RepairPowerup,HealthPowerup)

ifclosest_powerup

angle=Utils.angle_between(

@object.x,@object.y,

closest_powerup.x,closest_powerup.y)

@object.physics.change_direction(

angle-angle%45)

else

#...reversefromenemy

end

@changed_direction_at=Gosu.milliseconds

@will_keep_direction_for=turn_time

end

#...

end

ThissmallchangetellsAItopickuphealthwhilefleeing.TheinterestingpartisthatwhentankpicksupRepairPowerup,it’shealthgetsfullyrestoredandAIshouldswitchbacktoTankFightingState.ThissimplethingisamajorimprovementinAIbehavior.

EvadingCollisionsAndGettingUnstuckWhileobservingAInavigation,itwasnoticeablethattanksoftengotstuck,eveninsimplesituations,likedrivingintoatreeandhittingitrepeatedlyforadozenofseconds.Toreducethenumberofsuchoccasions,wewillintroduceTankNavigatingState,whichwouldhelpavoidcollisions,andTankStuckState,whichwouldberesponsiblefordrivingoutofdeadendsasquicklyaspossible.

Toimplementthesestates,weneedtohaveawaytotelliftankcangoforwardandawayofgettingadirectionwhichisnotblockedbyotherobjects.Let’saddacoupleofmethodstoAiVision:classAiVision

#...

defcan_go_forward?

Page 183: Developing Games With Ruby: For those who write code for living

in_front=Utils.point_at_distance(

*@viewer.location,@viewer.direction,40)

@object_pool.map.can_move_to?(*in_front)&&

@object_pool.nearby_point(*in_front,40,@viewer)

.reject{|o|o.is_a?Powerup}.empty?

end

defclosest_free_path(away_from=nil)

paths=[]

5.timesdo|i|

ifpaths.any?

returnfarthest_from(paths,away_from)

end

radius=55-i*5

range_x=range_y=[-radius,0,radius]

range_x.shuffle.eachdo|x|

range_y.shuffle.eachdo|y|

[email protected]+x

[email protected]+y

if@object_pool.map.can_move_to?(x,y)&&

@object_pool.nearby_point(x,y,radius,@viewer)

.reject{|o|o.is_a?Powerup}.empty?

ifaway_from

paths<<[x,y]

else

return[x,y]

end

end

end

end

end

false

end

alias:closest_free_path_away_from:closest_free_path

#...

private

deffarthest_from(paths,away_from)

paths.sortdo|p1,p2|

Utils.distance_between(*p1,*away_from)<=>

Utils.distance_between(*p2,*away_from)

end.first

end

#...

end

AiVision#can_go_forward?tellsiftankcanmoveahead,andAiVision#closest_free_pathfindsapointwheretankcanmovewithoutobstacles.YoucanalsocallAiVision#closest_free_path_away_fromandprovidecoordinatesyouaretryingtogetawayfrom.

Wewilluseclosest_free_pathmethodsinnewlyimplementedtankmotionstates,andcan_go_forward?inTankMotionFSM,tomakeadecisionwhentojumpintonavigatingorstuckstate.

Thosenewstatesarenothingfancy:13-advanced-ai/entities/components/ai/tank_navigating_state.rb

1classTankNavigatingState<TankMotionState

2definitialize(object,vision)

3@object=object

4@vision=vision

5end

6

7defupdate

8change_directionifshould_change_direction?

9drive

10end

11

Page 184: Developing Games With Ruby: For those who write code for living

12defchange_direction

[email protected]_free_path

14ifclosest_free_path

[email protected]_direction(

16Utils.angle_between(

[email protected],@object.y,*closest_free_path))

18end

19@changed_direction_at=Gosu.milliseconds

20@will_keep_direction_for=turn_time

21end

22

23defwait_time

24rand(10..100)

25end

26

27defdrive_time

28rand(1000..2000)

29end

30

31defturn_time

32rand(300..1000)

33end

34end

TankNavigatingStatesimplychoosesarandomfreepath,changesdirectiontoitandkeepsdriving.13-advanced-ai/entities/components/ai/tank_stuck_state.rb

1classTankNavigatingState<TankMotionState

2definitialize(object,vision)

3@object=object

4@vision=vision

5end

6

7defupdate

8change_directionifshould_change_direction?

9drive

10end

11

12defchange_direction

[email protected]_free_path

14ifclosest_free_path

[email protected]_direction(

16Utils.angle_between(

[email protected],@object.y,*closest_free_path))

18end

19@changed_direction_at=Gosu.milliseconds

20@will_keep_direction_for=turn_time

21end

22

23defwait_time

24rand(10..100)

25end

26

27defdrive_time

28rand(1000..2000)

29end

30

31defturn_time

32rand(300..1000)

33end

34end

TankStuckStateisnearlythesame,butitkeepsdrivingawayfrom@stuck_atpoint,whichissetbyTankMotionFSMupontransitiontothisstate.classTankMotionFSM

STATE_CHANGE_DELAY=500

LOCATION_CHECK_DELAY=5000

definitialize(object,vision,gun)

Page 185: Developing Games With Ruby: For those who write code for living

#...

@stuck_state=TankStuckState.new(object,vision,gun)

@navigating_state=TankNavigatingState.new(object,vision)

set_state(@roaming_state)

end

#...

defchoose_state

[email protected]_go_forward?

unless@current_state==@stuck_state

set_state(@navigating_state)

end

end

#Keepunstuckingitselfforawhile

change_delay=STATE_CHANGE_DELAY

if@current_state==@stuck_state

change_delay*=5

end

now=Gosu.milliseconds

returnunlessnow-@last_state_change>change_delay

if@last_location_update.nil?

@last_location_update=now

@[email protected]

end

ifnow-@last_location_update>LOCATION_CHECK_DELAY

puts"checkinlocation"

unless@last_location.nil?||@current_state.waiting?

ifUtils.distance_between(*@last_location,*@object.location)<20

set_state(@stuck_state)

@[email protected]

return

end

end

@last_location_update=now

@[email protected]

end

#...

end

#...

end

Whatthisdoesisautomaticallychangestatetonavigatingwhentankisabouttohitanobstacle.Italsotrackstanklocation,andiftankhasn’tmoved20pixelsawayfromit’soriginaldirectionfor5seconds,itentersTankStuckState,whichdeliberatelytriestonavigateawayfromthestock_atspot.

AInavigationhasjustgotsignificantlybetter,anditdidn’ttakethatmanychanges.

Page 186: Developing Games With Ruby: For those who write code for living

WrappingItUp

Ourjourneyintotheworldofgamedevelopmenthascometoanend.Wehavelearnedenoughtoproducedaplayablegame,yetonlyscratchedthesurface.Writingthisbookwasaveryenlighteningexperience,andhopefullyreadingitinspiredorhelpedsomeonetogetastart.

LessonsLearnedBuildingthissmalltanksgameandlearningaboutgamedevelopmentwithRubycertainlyhadsomenastybumpsalongtheway,someofthemmademyheadhittheceiling.

RubyIsSlow

Thisshouldn’tbeashocker,becauseRubyisadynamic,interpretedlanguage,buthowexactlyslowitisatsomepointswasastaggeringdiscovery.ProbablythebestevidenceisthatdrawingmaptilesoffscreenusingnativeextensionswasactuallyfasterthandoingCamera#can_view?checksthatinvolvesimpleintegerarithmeticandrangechecks.

Ifyourgameisgoingtodealwithlargenumberofentities,Rubywillstartlettingyoudown.Dreamingaboutgoingpro?GoforC++,youwon’tmakeamistakehere.

Knowingthis,keepinmindthatRubyisawonderfullanguage,thathasit’sownstrengths.It’sgreatforprototypinganddynamicthings.Some5-10linesofRubycouldtranslateinto50-100linesofC++.Also,knowingmultiplelanguagesmakesyouabetterdeveloper.

PackagingRubyGamesSucks

Unlessyouarereleasingyourgamefortechsavvyguyswhocangeminstallit,getreadytogothroughhell.ThereisnoniceandeasywaytocreateastandaloneexecutableapplicationfromRubycodethatinvolvesnativeextensions.Andyouwillgothroughhellonceforeveryoperatingsystemyouwanttopublishyourgamefor.

That’snoteverything.WanttousethelatestRubyversion?CheckifyoucanmakeapackageforitinyourtargetOSbeforeyoustartcoding.ThinkingofusingsomethingthatreliesonImageMagick?Toobad,youprobablywon’tbeabletopackagethegameintoanativestandaloneapp,atleastonOSX.Ifyouareplanningonreleasingthegame,packageearlyandpackageoften,foreveryOS,andcheckiftherewillbenoproblemswithnativeextensions.

PlanNetworkedMultiplayerEarly

Ifyouaregoingtobuildagame,don’tmakeamistakeofthinking“I’lljustmakeitmultiplayerlater”,startattheverybeginning.ThiswasalessonIlearnedthehardway.TherehadtobeachapterinthisbookaboutturningTanksintomultiplayer,butitdidn’thappen,becauseitwouldrequireamajorrewriteofthecode.

Page 187: Developing Games With Ruby: For those who write code for living

CreatingAWellPolishedGameRequiresExtraordinaryEffort

Hackinguparoughprototypeisextremelyfun.Yougettobuildanengine,wireeverythingtogether.Itdefinitelygivesasenseofachievement.Turningitintoagreatgame,however,isadifferentstory.Youcanspendhoursorevendaystweakinghowgamecontrolsworkandstillremainunsatisfied.Everytinydetailcanbepushedfurther.Preferqualityoverquantity,andrememberthatyouprobablycannotaffordbothandactuallyfinishitwithinnextcoupleofyears.

StartSmall,TakeBabySteps

Yourfirstfewgamesshouldbesmallexperiments,prototypesordemos.Don’tattempttobuildagameyouwantedtobuildforeverwithyourfirstshot.TryreimplementingTetris,PacmanorBejeweledinstead.Youwillfindittobechallengingenough,andwhenyouwillfeelyouhavetheskillstodosomethingbigger,practicejustalittlemore.

Don’tReinventTheWheel

Beforedoinganything,research.YouwillprobablynotgetpointinpolycollisiondetectionbetterthanW.RandolphFranklindiditinhisresearch.Evenifyouthinkyoucandoitonyourown,learnwhatothersdiscoveredbeforeyou.Learnfromother’smistakes,notyourown.

Page 188: Developing Games With Ruby: For those who write code for living

SpecialThanks

IwouldliketothankJulianRaschkeforcreatingandmaintainingGosuandforallthehelponIRC,GosuforumsandGitHub.ThisbookwouldnotexistwithoutyourenormouscontributiontoRubygamedevelopmentscene.

ShoutoutgoestoShawnAnderson,creatorofGamebox.Thankyouformoralsupportandencouragement.StudyingGameboxsourcecodetaughtmemanythingsaboutGosuandgamedevelopment.

YoucanfindJulian,Shawnandmoregamedevelopmententhusiastsin#gosuonFreeNode.

Andmostimportantly,thankyouforreadingthisbook!


Recommended