Burn: 8-Bit Game Development With Ruby

Fbe207d1bece86feb799fbc1841ce735?s=47 remore
September 20, 2014

Burn: 8-Bit Game Development With Ruby

There are countless ways out there that emulate .nes game, but very few that create .nes rom file without hassle. This is the reason why Kei have developed Burn, is_a handy toolkit to make a .nes application from Ruby DSLs. Burn v0.1.3 release is announced on reddit this April, and it spread to every corner of the world.

In this presentation we will explore what Burn is, how it is structured, and what homebrewed .nes application looks like in an NES Emulator. We'll also learn a few practical techniques for making the most from Ruby bindings, Ripper and Ruby DSL.

An introduction at RubyKaigi2014 official website:
http://rubykaigi.org/2014/presentation/S-KeiSawada

Github repository:
https://github.com/remore/burn

Fbe207d1bece86feb799fbc1841ce735?s=128

remore

September 20, 2014
Tweet

Transcript

  1. #VSO#JU(BNF %FWFMPQNFOU 8JUI3VCZ !SFNPSF 4FQUI 3VCZ,BJHJBU'VOBCPSJ +BQBO

  2. self.introduction do greetings "Hi" id "remore" name "Kei Sawada" icon

    from "Tokyo" job "Software Engineer" favorite do method "instance_eval" instrument "Double Bass" end end
  3. 8IBUPOFBSUIJT#VSO ʢ஌Βͳ͍ํ͕΄ͱΜͲͩͱࢥ͍·͕͢ʣ#VSOͱ͸ҰମͳΜͰ͠ΐ͏ʁ

  4. #VSOJTBSVCZHFNXIJDIFOBCMF ZPVUPDSFBUFZPVSPXO OFT30.*NBHF'JMF 5FMOFU4FSWFS"QQMJDBUJPO GSPN3VCZ%4- BTPGW #VSO͸SVCZHFNͰ͢ɻ#VSOΛ࢖͏ͱʢWͰ͸ʣɺOFT30.Πϝʔδ΍ 5FMOFUαʔόΞϓϦέʔγϣϯΛ3VCZͷ%4-Ͱ؆୯ʹهड़͢Δ͜ͱ͕Ͱ͖·͢

  5. #VSOXPSLTpOFXJUI 8JOEPXT .BDBOE-JOVY $3VCZ /PUZFUUFTUFE XJUI+3VCZBOE3VCJOJVT 7FSTJPO)JTUPSZ4VNNBSZ WSFMFBTFEOFTNPEF WBEEFEUFMOFUNPEF WMBUFTUWFSTJPO

  6. %FNP IUUQTHJUIVCDPNSFNPSFCVSO

  7. telnet 128.199.158.17 60000 0S-JWF%FNP *O0UIFS8PSET 4USFTT5FTU

  8. l"NB[JOHXPSLz l7FSZWFSZDPPM TUV⒎z

  9. l5IFDPPMFTUTIJUJT BMXBZTUIFNPTU VTFMFTTz ௒༁ɿ࠷ߴʹΫʔϧͳ΋ͷ͸͍ͭ΋ ࠷ߴʹ໾ʹཱͨͳ͍΋ͷͰ͢Ͷ

  10. *LOPX NBO ஌ͬͯͨ

  11. OFT$PNQJMBUJPO1SPDFTT 5FMOFU4FSWFS"SDIJUFDUVSF 8IZ 8IZBOE8IZ "QQFOEJY-JNJUBUJPOTBTPGW "QQFOEJY'VFM%4-&YBNQMFT

  12.  #VSOͷOFTੜ੒ೳྗ͸DDʹ׬શʹґଘ͍ͯ͠·͢ɻ ΑͬͯɺDDͰͰ͖Δ͜ͱ͸#VSOͰ΋Ͱ͖ΔΑ͏ʹকདྷతʹ͸ͳΔ΂͖Ͱ͢ ʢࠓ͸΄Μͷগ͠ͷදݱ͔࣮͠૷Ͱ͖͍ͯͳ͍ͱࢥ͍·͢ʣ

  13. #VSOJOHOFT'VFM 'VFM CVSOHFN QSFQSPDFTTPS NBJOD NVTJDT TPVOET NBJOOFT NVTJDEP 

    FOE EFDMBSFEP GPP FOE TDFOFEP MBCFMbIFMMP XPSME` FOE SVOUJNFMJC DPNQJMFS MJOLFS ϫʔΫϑϩʔ͸͜Μͳײ͡Ͱ͢ɻ̍ʣ࣮ߦɺ'VFMΛೖྗ ̎ʣ$ͷιʔε౳Λੜ੒ɹ̏ˍ̐ʣDDͰίϯύΠϧɾϦϯΫ
  14. UNQCVSONBJOD # installation sudo gem install burn sudo burn init

    # :nes mode echo "scene {label 'hello world'}" > main.rb burn vi tmp/burn/main.c 5IFCFTUXBZUPTFFNBJODJTUPUSZ CVSOJOH
  15. UNQCVSOBTTFUNVTJDT sarabande: .word @chn0,@chn1,@chn2,@chn3,@chn4,music_instruments .byte $03 @chn0: @chn0_0: .byte $c2,$43,$1a,$89,$3f,$89...$13,$89,$3f,$89,$3f,

    $13,$89,$3f,$95,$3f @chn0_loop: @chn0_1: .byte $9f .byte $fe .word @chn0_loop @chn1: @chn1_0: .byte $c2,$42,$29,$89,$3f,$89...,$89,$3f,$2e,$89,$3f, $89,$3f,$2e,$89,$3f,$95,$3f @chn1_loop: @chn1_1: .byte $9f .byte $fe .word @chn1_loop
  16. UNQCVSOBTTFUTPVOET sounds: .word @attack @attack: .byte $00,$af .byte $01,$c8 .byte

    $19 .byte $00,$af .byte $01,$c8 .byte $19 .byte $01,$64 .byte $1a .byte $00,$af .byte $01,$c8 .byte $19 .byte $01,$64 .byte $1a .byte $01,$32 .byte $1a .byte $ff
  17. 8SJUJOH3VCZ%4-BOE$PEF declare do frame 0 end scene do label "hello,

    world!", 4, 5 color :text, :white main_loop <<-EOH frame+=1 goto “fin” if frame==100 EOH end scene "fin”“do label "Good-bye!" end #JOEJOH  JOTUBODF@FWBM ݸਓతʹɺ3JQQFSͱ#JOEJOH ͱ಺෦%4- ͸ 3VCZͷ(PPE1BSUTͩͱࢥͬͯ·͢ɻ͜Ε͸Ͳͷ෦෼ʹ׆༻ͯ͠Δ͔ͷਤɻ 8IBUUIF)FMMJTUIJT
  18. %FUBJMFE1SPDFTTPGNBJO@MPPQ frame+=1 goto "fin" if frame==100 [:program, [[:opassign, [:var_field, [:@ident,

    "frame", [1, 0]]], [:@op, "+=", [1, 5]], [:@int, "1", [1, 7]]], [:if_mod, [:binary, [:var_ref, [:@ident, "frame", [2, 14]]], :==, [:@int "100", [2, 21]]], [:command, [:@ident, "goto", [2, 0]], [:args_add_block, [[:string_literal, [:string_content, [:@tstring_content, "fin", [ 6]]]]], false]]]]] 3JQQFSTFYQ SFNPSFTJNQMF@USBOTQJMFS 3JQQFS͕ѻ͍΍͍͢4ࣜΛఏڙͯ͘͠Ε͍ͯΔ͓͔͛Ͱɺ ߦͪΐͬͱͷίʔυͰ$ݴޠ෩ͷจࣈྻ΁ͷม׵͕؆୯ʹ࣮૷Ͱ͖·͢ ɾɾɾ3VCZ`T4ZOUBY frame+=1; if(frame==100){ goto("fin"); } ɾɾɾ$4ZOUBY
  19. &YBNQMF #!/usr/bin/env ruby require 'ripper' require './simple_transpiler' fuel = <<-EOH

    frame+=1 goto "fin" if frame==100 EOH SimpleTranspiler.ruby2c(Ripper.sexp(fuel)) # frame+=1; # if(fraome==100){ goto(“fin”); } ʢ΋ͪΖΜɺѻ͑Δߏจ͸ຊ౰ʹجຊతͳ΋ͷͷΈͰɺ੍ݶ͋Γ·͘ΓͰ͕͢ʣ
  20. OFT$PNQJMBUJPO1SPDFTT 5FMOFU4FSWFS"SDIJUFDUVSF 8IZ 8IZBOE8IZ "QQFOEJY-JNJUBUJPOTBTPGW "QQFOEJY'VFM%4-&YBNQMFT

  21. (FOFSBUJOH3VCZ$PEF  @___star = Sprite.new(" 11 11 1111 1111 1111111111111111

    11111111111111 111111111111 1111111111 11111111 11111111 11111111 1111 1111 11 11 1 1 ",0,0)  #main  #main-main_loop:2  @user_input.init_for_next_loop  @___star.x=20  @___star.y-=3  @screen.activated_sprite_objects << @___star if  @screen.activated_sprite_objects.index(@___star).nil?  @pc = @opcodes.index("#main-main_loop:2")  #END *OEFYBOE$POUFOUT 4USJOH PG@opcodes"SSBZ
  22. #VSOJOHUFMOFU'VFM 5FMOFU4FSWFS TFSWFSUFMOFUSC DPOpHBQQ UBSHFUUFMOFU FOE EFDMBSFEP GPP FOE TDFOFEP

    MBCFMbIFMMP` FOE class TelnetVm attr_reader :screen def initialize(fuel) @pc=0 @opcodes=JitCompiler .new(fuel).compile end def next_frame self.instance_eval @opcodes[@pc] @pc+=1 end def interrupt # trigger event # by setting interrupt flag end end 'VFM FWFOUNBDIJOF JOUFSSVQU OFYU@GSBNF !TDSFFO GVFM LFZFWFOU 5FSNJOBMT TDSFFO PVUQVU UFMOFUαʔόͷ؆୯ͳઃܭਤͰ͢ɻFWFOUNBDIJOFͷதͰͭͷ QFSJPEJD@UJNFSΛಈ͔͍ͯ͠·͢ɻͭ͸ը໘ग़ྗɺ΋͏̍ͭ͸ೖྗड෇༻
  23. )PX%PFT5IJT -JOF0G$PEF8PSL self.instance_eval @opcodes[@pc] !PQDPEFTͷத਎͸Կʁɹ࣮ͬͯ͜͜ࡍԿΛJOTUBODF@FWBMͯ͠Δͷʁ

  24.  @___star = Sprite.new(" 11 11 1111 1111 1111111111111111 11111111111111

    111111111111 1111111111 11111111 11111111 11111111 1111 1111 11 11 1 1 ",0,0)  #main  #main-main_loop:2  @user_input.init_for_next_loop  @___star.x=20  @___star.y-=3  @screen.activated_sprite_objects << @___star if  @screen.activated_sprite_objects.index(@___star).nil?  @pc = @opcodes.index("#main-main_loop:2")  #END 0OFWFSZTJOHMFQFSJPE UJNFGSBNF PG&WFOU.BDIJOF JOTUBODF@FWBMXJMMCFDBMMFE BT!QDJTJODSFNFOUFE TBNQMFEVNQEBUBPG!PQDPEFT
  25. 1FSGPSNBODF w 5FMOFUTFSWFSJTCVJMUXJUI&WFOU.BDIJOF#VUIPX NVDIDBQBCMFJTJU w %JERVJDLQFSGPSNBODFUFTUVOEFSGPMMPXJOH UFTUJOHFOWJSPONFOU w 4FUUJOHVQBDIFBQFTU7.PO%JHJUBM0DFBO .#

    DPSF XJUI$FOU04SFMFBTF 'JOBM w .3*WFSTJPOXBTQ ύϑΥʔϚϯεςετ΋΍ͬͯΈ·ͨ͠
  26. 1FSGPSNBOU&OPVHI *TO`U*U w -(5. w 6QUJNFEBZT DPOO-"NJOdNBY BWH d 

    d  d  d  ্ʑʁ
  27. OFT$PNQJMBUJPO1SPDFTT 5FMOFU4FSWFS"SDIJUFDUVSF 8IZ 8IZBOE8IZ "QQFOEJY-JNJUBUJPOTBTPGW "QQFOEJY'VFM%4-&YBNQMFT

  28. None
  29. None
  30. l"QQMJDBUJPO %FWFMPQNFOUz

  31. 5FMOFU͸೥ɺOFT͸೥Ҏ্ར༻͞Εଓ͚͖ͯͨɻ͔͠΋Ͳͷ04Ͱ΋ ಈ࡞͢ΔɻOFT30.΍UFMOFU͸ɺͳͥ͜Μͳʹ׆༻͞Ε͍ͯΔͷ͔ʁ w -POH-JWF w 5FMOFUJOJUJBMMZEFWFMPQFEJOBDDPSEJOHUP XJLJQFEJB*UIBTCFFOVTFECSPBEMZFWFOJO 8IZ w OFT30.JOWFOUFEJOTBOETUJMMCFJOHPOFPG

    UIFGBWPSJUFUPZBNPOHHFFLTVOUJMOPX8IZ w 8PSLT6OJWFSTBMMZ w #PUIUFMOFUDMJFOUBOEOFT30.FNVMBUPSBSF JNQMFNFOUFEJOBMMNBKPS048IZ 3FFWBMVBUJPOPG-PX5FDI
  32. “Why do they call it rush hour when nothing moves?”

    Robin Williams lԿ΋ಈ͍ͯͳ͍ͷʹɺͳΜͰϥογϡΞϫʔͳΜͯݺͿΜ͍ͩʁz ϩϏϯɾ΢ΟϦΞϜζʢΞϝϦΧͷίϝσΟΞϯɾആ༏ʣ
  33. None
  34. ΤϛϡϨʔλͷ਺ͱಉ͘͡Β͍ͨ͘͞Μͷ ஶ࡞ݖ͕ΫϦΞͳOFT30.ϑΝΠϧ͕ྲྀ௨͍ͯ͠Ε͹خ͍͠ͷͰ͕͢ *EFBM )BQQZ w OFT 㱣  &NVMBUPS 㱣

    w 4VQQMZBOEEFNBOEBSFJOHPPECBMBODF
  35. ݱ࣮͸ͦΜͳ͜ͱ͸ͳ͍ͷͰͨ͠ɻ άάΔͱ͙͢ʹݟ͔ͭΔɺஶ࡞ݖͱग़ॴ͕ෆ໌ͳ30.ͷ਺ʑɻ #VU3FBMJUZ 4BE w OFT '&8  &NVMBUPS 㱣

    w *OTUFBE 5IFSFJT."/:QJSBUFEOFT30.T
  36. ·ͣແཧ͚ͩͲΦʔϓϯιʔεͳ30.ւ଑൛30.ͳ ੈքʹ͢Δ͜ͱ͕5IF3JHIU8BZͩͱࢥͬͯ·͢ɻ $BO8F8PSLBSPVOE 5IF4JUVBUJPO w 4PPO "MNPTUIPQFMFTT w )PXFWFS JOMPOHUFSN

    #VSOJTBJNJOHUP NBLFUIJTXPSMEGVMMPGPQFOTPVSDF w FH'VFM)VC w )PQFGVMMZ FWFOUVBMMZ w 0QFOTPVSDFE30.1JSBUFE30.
  37. $PODMVTJPO OFT$PNQJMBUJPO1SPDFTT *4'6/ TJODF6TJOH3VCZJT'VO 5FMOFU4FSWFS"SDIJUFDUVSF $IJMEJTI#VU803,4 8IZ 8IZBOE8IZ #FDBVTF-PX5FDI*T4UJMM (PPE"OE0QFO4PVSDJOH*U

    .BLFT6T)BQQJFS
  38. "DLOPXMFEHFNFOU w -PHPT GSPN5IF/PVO1SPKFDU w IUUQUIFOPVOQSPKFDUDPNUFSNpSF w IUUQUIFOPVOQSPKFDUDPNUFSNSFGSFTI w 1IPUPT

    w IUUQTXXXqJDLSDPNQIPUPTIJTHMBTTXPSLT  w IUUQTXXXqJDLSDPNQIPUPTSBJOEPH 
  39. OFT$PNQJMBUJPO1SPDFTT 5FMOFU4FSWFS"SDIJUFDUVSF 8IZ 8IZBOE8IZ "QQFOEJY-JNJUBUJPOTBTPGW "QQFOEJY'VFM%4-&YBNQMFT

  40. -JNJUBUJPOT w 5IFSFBSF50/40'MJNJUBUJPOT w SPNNPEF GPMMPXJOHUIJOHTBSF/05TVQQPSUFE w FHQMBZFS TDSPMMJOHTDSFFO %1$.

    NBLJOHGVMM VTFPGBUUSJCVUFUBCMFFUD w UFMOFUNPEF  w TJOHMFJOQVUNPEFJTOPUTVQQPSUFE FUD w )POFTUMZ XJUI#VSOWEFWFMPQFSPOMZDBO CVJMEQPPSBQQMJDBUJPO ੍ݶࣄ߲͹͔ΓͳͷͰɺͰ͖Δ͜ͱ͚ͩΛॻ͍ͨํ͕ૣ͍ঢ়ଶͰ͕͢ɾɾɾ ը໘εΫϩʔϧ͕Ͱ͖ͳ͍ɺεϓϥΠτ͕Ξχϝʔγϣϯ͠ͳ͍ɺͳͲ
  41. JOTUBOU#65QPPSTPGBS

  42. OFT$PNQJMBUJPO1SPDFTT 5FMOFU4FSWFS"SDIJUFDUVSF 8IZ 8IZBOE8IZ "QQFOEJY-JNJUBUJPOTBTPGW "QQFOEJY'VFM%4-&YBNQMFT

  43. 'VFM%4-ˌEFDMBSF TDFOF declare do i 0 #equivalent to "int i=0;"

    in C end scene "game" do color :text, :red # use :red color for text label "hello", 3, 5 # display string "hello" at x=3, y=5 play "battle" # play "battle" music fade_in # trigger fade-in effect wait 30 # wait for certain period goto "intro" # move to another scene end EFDMBSFσΟϨΫςΟϒͰ͸ɺϓϩάϥϜͰར༻͢Δม਺Λએݴ͠·͢ɻ TDFOFσΟϨΫςΟϒ͕ɺ࣮ࡍͷϓϩάϥϜͷಈ࡞Λهड़͢ΔՕॴͰ͢ɻ
  44. config :app do target :telnet # :rom by default width

    80 # 67 by default height 24 # 13 by default fps :high # :moderate, :low user_input :enable # :disable by default end config :server do ip_addr '0.0.0.0' # 127.0.0.1 by default port 60000 # 60000 by default max_clients 20 # 10 by default end 'VFM%4-ˌDPOpH ͜ͷதͰ͸UBSHFU͕Ұ൪ॏཁͰɺ͜Ε͕#VSOͷಈ࡞ϞʔυΛنఆ͠·͢ɻ TFSWFSʹ͍ͭͯ͸ɺOFTͷ৔߹͸8&#SJDL UFMOFUͷ৔߹͸ &WFOU.BDIJOFͷಈ࡞Λنఆ͠·͢ɻ
  45. 'VFM%4-ˌNVTJD music "battle" do tempo :allegro # set tempo channel

    "string" do # select instrument d2 :eighth # :eighth is the length rest :eighth d2 :eighth rest end channel "piano" do a3 a3 end end ݸਓతʹ͸#VSOͰҰ൪໘ന͍ͱࢥͬͯΔ%4-Ͱ͢ɻ OFTϞʔυͷΈ͔͠ରԠ͍ͯ͠·ͤΜ͕ɺ%4-Λॻ͘ͱ OFTΤϛϡϨʔλʹ౥ࡌ͞ΕͯΔ'.ԻݯΛૢ࡞ͯ͠ԻָΛ࠶ੜͰ͖·͢ɻ
  46. 'VFM%4-ˌTPVOE sound "attack" do effect do duty_cycle :higher velocity 15

    # 7 by default envelope_decay :disable # disabled by default length 10 # range: 1-254 pitch 200 end effect do length 11 pitch 100 end end '.ԻݯΛར༻ͨ͠ޮՌԻͷੜ੒΋ՄೳͰ͢ɻ '.Իݯʹͦ͜·Ͱৄ͘͠ͳ͍ͷͰɺ֤ϝιουΛͲ͏͍͡Δͱ ͲΜͳԻʹͳΔͷ͔ɺ·ͩ๻ʹ΋૝૾͖ͭ·ͤΜɻ
  47. )BQQZ)PNFCSFXJOH