Roku Developer Program

Join our online forum to talk to Roku developers and fellow channel creators. Ask questions, share tips with the community, and find helpful resources.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
marcelo_cabral
Roku Guru

BrightScript Draw 2D Emulator (Web & Desktop Apps)

----- Update 2019-10-24

New release:

  • Audio playback now supported! Working with both  roAudioResource  and roAudioPlayer
  • Desktop Apps (Windows, Linux, Mac) now support Display Modes (SD, HD, FHD)
  • Desktop Apps (Windows, Linux, Mac) now can emulate TV overscan effect

Download latest release at:

https://github.com/lvcabral/brs-emu/releases/tag/v0.6.0-emu

 

The emulator code is available on the the repository: https://github.com/lvcabral/brs-emu

The demonstration site has the details about the project and several examples of games and channels running: https://lvcabral.com/brs

brightscript-emulator-v040.png

 

----- Original Post

I had this idea for a long time, but to start from scratch was always a big effort that I was not willing to put. Then recently I learned about this cool project: https://github.com/lvcabral/brs under the branch called "brsLib" and you can test it online at this link: https://lvcabral.com/brs/

I'm far from a TypeScript/Javascript expert, learning as I go, if you would be interested to help, let me know!

I will keep updating this thread with the progress.

The code below was extracted from Roku Draw 2D API example (Simple2D), you can save it to a brs file and load in the tool to see the animation above in your browser. You can change it or create your own animations with BrightScript.

sub main()
    screen=CreateObject("roScreen", true, 854, 480)
    screen.SetAlphaEnable(true)
    screen.Clear(&HFF)
    port = CreateObject("roMessagePort")
    screen.SetMessagePort(port)
    compositor=CreateObject("roCompositor")
    compositor.SetDrawTo(screen, 0)
    scaleblit(screen, port, 0, 0, 854, 480, 1)
end sub

sub scaleblit(screenFull as object, msgport as object, topx, topy, w, h, par)

        print "Scale Boing"
        screen = screenFull
        
        red = 255*256*256*256+255
        green = 255*256*256+255
        blue = 255*256+255
        
        clr = int(255*.55)
        background = &h8c8c8cff
        sidebarcolor = green

        screen.Clear(background)
        screenFull.SwapBuffers()

        ' create a red sprite '

        ballsize = h/4
        ballsizey = int(ballsize)
        ballsizex = int(ballsize*par)

        tmpballbitmap = createobject("robitmap","pkg:/img/AmigaBoingBall.png")

        scaley = ballsizey/tmpballbitmap.getheight()
        scalex = scaley*par

        ballbitmap = createobject("robitmap",{width:ballsizex,height:ballsizey,alphaenable:true})
        ballbitmap.drawscaledobject(0,0,scalex*1.0,scaley*1.0,tmpballbitmap)

        ballregion = createobject("roregion",ballbitmap,0,0,ballsizex,ballsizey)
        ballcenterX = int(ballsizex/2)
        ballcenterY = int(ballsizey/2)
        ballregion.setpretranslation(-ballcenterX, -ballcenterY)
        ballregion.setscalemode(0)
        
        ' construct ball shadow '
        tmpballbitmap = createobject("robitmap","pkg:/img/BallShadow.png")
        ballshadow = createobject("robitmap",{width:ballsizex,height:ballsizey,alphaenable:true})
        ballshadow.drawscaledobject(0,0,ballsizex/tmpballbitmap.getwidth(),ballsizey/tmpballbitmap.getheight(),tmpballbitmap)
        
        shadowregion = createobject("roregion",ballshadow,0,0,ballsizex,ballsizey)
        shadowregion.setpretranslation(-ballcenterX, -ballcenterY)
        shadowregion.setscalemode(0)

        ' calculate starting position and motion dynamics '
        x = w/10 + ballcenterX
        y = h/10 + ballcenterY
        
        dx = 2
        dy = 1
        ay = 1
        framecount = 0
        timestamp = createobject("rotimespan")
        swapbuff_timestamp = createobject("rotimespan")
        start = timestamp.totalmilliseconds()
        swapbuff_time = 0
        shadow_dx = int(ballsizex/4)
        shadow_dy = int(ballsizey/10)
        w_over_10 = w/10
        rightedge = int(ballcenterx + (w*9)/10)
        bottomedge = int(ballcentery + (h*9)/10)
        running = true
        ' codes = bslUniversalControlEventCodes() '
        grid = createobject("robitmap", {width:screen.getWidth(),height:screen.getheight(),alphaenable:false})
        regiondrawgrid(grid, background)
        grid.finish()
        while true
                screen.drawobject(0, 0, grid)
                screen.SetAlphaEnable(true)
                scalex = x/rightedge
                scaley = y/bottomedge
                screen.drawscaledobject(toInt(x+shadow_dx),toInt(y+shadow_dy),scalex*1.0,scaley*1.0,shadowregion)

                screen.drawscaledobject(toInt(x),toInt(y),scalex*1.0,scaley*1.0,ballregion)
                screen.SetAlphaEnable(false)
                screen.drawrect(toInt(x-2),toInt(y-2),5,5,green)
                
                swapbuff_timestamp.mark()
                screenFull.SwapBuffers()
                swapbuff_time = swapbuff_time + swapbuff_timestamp.totalmilliseconds()
                
                pullingmsgs = true
                while pullingmsgs
                    deltatime = timestamp.totalmilliseconds() - start
                    msg = msgport.getmessage()
                    if msg = invalid and deltatime > 16 'aprox 60fps '
                        timestamp.mark()
                        start = timestamp.totalmilliseconds()
                        pullingmsgs = false
                    else
                        if type(msg) = "roUniversalControlEvent"
                            button = msg.getint()
                            print "button=";button
                            'if button=codes.BUTTON_BACK_PRESSED   '
                                return
                            'endif '
                        endif
                    endif
                end while
                x = x + dx
                y = y + dy
                dy = dy + ay
                if x<w_over_10
                        x = w_over_10+(w_over_10-x)
                        dx = -dx
                endif
                if y<0
                        y = -y
                        dy = -dy
                endif
                if x+ballsizex > rightedge
                        x = 2*rightedge - x - 2*ballsizex
                        dx = -dx
                endif
                if y+ballsizey > bottomedge
                        y = 2*y - y
                        dy = -dy + ay
                endif
        end while
        print "Exiting APP"
End Sub

sub drawline(screen, x0,y0,x1,y1,width,color)

    if (width = 1) and (y0 <> y1) and (x0 <> x1)
        screen.drawline(x0, y0, toInt(x1), y1, color)
        return
    end if

    if (x0=x1)
        ' vertical line '
        h = y1-y0
        if h<0 ' upside down? '
            y0=y1
            h = -h
        endif            
        screen.drawrect(toInt(x0),y0,toInt(width),h+1,color)
    else if (y0=y1)
        w = x1-x0
        if w<0
            x0=x1
            w = -w
        endif
        screen.drawrect(toInt(x0),y0,toInt(w+1),width,color)
    end if
end sub

function toInt(value) as integer
    if type(value) = "Integer" then return value
    return int(value)
end function

sub regiondrawgrid(screen, background)
    ' only draw into primary surface area now - do not touch sidebars '
    screen.clear(background)
    w = screen.getWidth()
    h = screen.getHeight()
    left = int(w/10)
    right = w-left
    top= int(h/10)
    bottom = h-top

    color = &hff00ffff
    ' draw vertical lines '
    i = 0
    x = left
    deltax = int(left/2)
    deltay = deltax
    lineheight = bottom - top
    bottom = top + deltay*int(lineheight/deltay)
    bottomXdelta = (deltax/3)
    bottomXdeltainit = bottomXdelta
    deltax_over_20 = int(deltax/20)
    deltay_over_2 = int(deltay/2)
    while x<=right
        drawline(screen,x,top,x,bottom,1,color)
        drawline(screen,x,bottom,x-bottomXdelta,bottom+deltay_over_2,1,color)
        x = x + deltax
        bottomXdelta =bottomXdelta - deltax_over_20
    end while
    ' correct for actual right edge '
    right = x - deltax
    y = top
    'draw horizontal lines '
    while y<=bottom
        drawline(screen,left,y,right,y,1,color)
        y = y + deltay
    end while
    ' draw floor '
    drawline(screen, left-bottomXdeltainit, bottom+deltay_over_2,right-bottomXdelta-deltax_over_20 , bottom+deltay_over_2, 1, color)
end sub

 

0 Kudos
34 REPLIES 34
Komag
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

This is incredible, something I've wished for since 2014! I haven't tried it yet, but will soon. I really hope you continue to work on it, as it will be a quite valuable tool for anyone making Roku games now and in the future.
0 Kudos
marcelo_cabral
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

"Komag" wrote:
This is incredible, something I've wished for since 2014! I haven't tried it yet, but will soon. I really hope you continue to work on it, as it will be a quite valuable tool for anyone making Roku games now and in the future.

It only work on recent versions of modern browsers, I tested in different flavors of Chromium (Chrome, MS Edge Chromium, Brave), it should work on Safari and Firefox, but I haven't tried yet.
It does not work on Microsoft IE or Edge.
0 Kudos
Komag
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

I tried selecting my main brs file, but I have many brs files in the project, and all the graphics files and xml files. I didn't see anything. In the example, the code shows the ball and shadow as png files in the pkg, but the emulator is only taking the brs file, so how can that work anyway? Do I need to run the emulator locally somehow, so it can have access to my folders and files?
0 Kudos
marcelo_cabral
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

"Komag" wrote:
I tried selecting my main brs file, but I have many brs files in the project, and all the graphics files and xml files. I didn't see anything. In the example, the code shows the ball and shadow as png files in the pkg, but the emulator is only taking the brs file, so how can that work anyway? Do I need to run the emulator locally somehow, so it can have access to my folders and files?

This is a client only typescript/javascript app, it does not upload anything. If you want to use your own assets, your would need to download the app and run in your own local webserver.
I will support zip files eventually to unpack the assets and expose internally in the browser, as I said in the original post, at this stage is a very limited emulator, so you can play with Draw 2D to draw and create animations with the images preloaded (see the source of the page to a list of bitmaps).
0 Kudos
marcelo_cabral
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

I implemented support for animated sprites and collision, here the emulator running the example Roku published back in 2012: https://blog.roku.com/developer/2012/09/15/colliding-sprites

[youtube:2z4ueck8]DSO7p1Dpy7o[/youtube:2z4ueck8]

The code to test this animation (I changed to create balls automatically, as the emulator still can't be controlled with the keyboard)
Library "v30/bslDefender.brs"

Function Main() as void
   screen = CreateObject("roScreen", true)
   port = CreateObject("roMessagePort")
   bitmapset = dfNewBitmapSet(ReadAsciiFile("pkg:/assets/bitmapset.xml"))
   ballsize = bitmapset.extrainfo.ballsize.ToInt()
   compositor = CreateObject("roCompositor")
   compositor.SetDrawTo(screen, &h000000FF)
   screenWidth = screen.GetWidth()
   screenHeight= screen.GetHeight()
   clock = CreateObject("roTimespan")
   clock.Mark()
   screen.SetMessagePort(port)
   screen.SetAlphaEnable(true)
   codes = bslUniversalControlEventCodes()
   sprites = []
   balls = []
   balls[0] = bitmapset.animations.animated_3ball
   balls[1] = bitmapset.animations.animated_4ball
   balls[2] = bitmapset.animations.animated_5ball
   balls[3] = bitmapset.animations.animated_6ball
   balls[4] = bitmapset.animations.animated_8ball
   spriteCount = 0
   sprites[0] = compositor.NewAnimatedSprite(Rnd(screenWidth-ballSize), Rnd(screenHeight-ballSize), balls[0])    
   sprites[0].SetData( {dx: Rnd(20)+10, dy: Rnd(20)+10, index: spriteCount} )

   timestamp = createobject("rotimespan")
   start = timestamp.totalmilliseconds()

   speed = 100
   while true
       for each sprite in sprites
           sprite.SetMemberFlags(1)
       end for
       event = port.GetMessage()
       deltatime = timestamp.totalmilliseconds() - start
       if (type(event) = "roUniversalControlEvent" or deltatime > 5000)
           id = 0 'event.GetInt() '
           if spriteCount < 3 'Add a sprite '
               spriteCount = spriteCount + 1
               sprites[spriteCount] = compositor.NewAnimatedSprite(Rnd(screenWidth-ballSize), Rnd(screenHeight-ballSize), balls[spriteCount])
               sprites[spriteCount].SetData( {dx: Rnd(20)+10, dy: Rnd(20)+10, index: spriteCount} )
           else if (id = codes.BUTTON_LEFT_PRESSED)
               wait(0, port)
           endif
           timestamp.mark()
           start = timestamp.totalmilliseconds()
           
       else if (event = invalid)
               ticks = clock.TotalMilliseconds()
           if (ticks > speed)
               for each sprite in sprites
                   dx = sprite.GetData().dx
                   dy = sprite.GetData().dy
                   sprite.MoveTo( (sprite.GetX() + dx), (sprite.GetY() + dy) )
                   collidingSprite = sprite.CheckCollision()
                   if (collidingSprite <> invalid)
                       doCollision(sprite, collidingSprite, sprites)
                   endif                    
               end for
               
               sprites = checkEdgeCollisions(sprites, screenWidth, screenHeight, ballsize)
               compositor.AnimationTick(ticks)
               compositor.DrawAll()
               screen.SwapBuffers()
               clock.Mark()
           endif
       
       endif
   end while
End Function

Function checkEdgeCollisions(sprites as object, screenWidth as integer, screenHeight as integer, ballsize as integer) as object
   for each sprite in sprites
       x = sprite.GetX()
       y = sprite.GetY()
       i = sprite.GetData().index
       deltaX = sprite.GetData().dx
       deltaY = sprite.GetData().dy
       if ((x + ballsize) > screenWidth)
           x = screenWidth - ballsize
           if (deltaX > 0)
               deltaX = -deltaX
           endif
           sprite.SetData( {dx: deltaX, dy: deltaY, index: i} )
       endif

       if (x < 0)
           x = 0
           if (deltaX < 0)
               deltaX = -deltaX
           endif
           sprite.SetData( {dx: deltaX, dy: deltaY, index: i} )
       endif
       
       if ((y + ballsize) > screenHeight)
           y = screenHeight - ballSize
           if (deltaY > 0)
               deltaY = -deltaY
           endif
           sprite.SetData( {dx: deltaX, dy: deltaY, index: i} )
       endif

       if (y < 0)
           y = 0
           if (deltaY < 0)
               deltaY = -deltaY
           endif
           sprite.SetData( {dx: deltaX, dy: deltaY, index: i} )
       endif
       sprite.MoveOffset(deltaX, deltaY)
   end for
   return sprites
End Function

Function doCollision(sprite0 as object, sprite1 as object, sprites as object) as object
   index0 = sprite0.GetData().index
   index1 = sprite1.GetData().index
   dx = sprite1.GetX() - sprite0.GetX()
   dy = sprite1.GetY() - sprite0.GetY()
   if (dx > 0)
       angle = atn(dy / dx)
   else
       angle = 1.5708
   endif
   fSin = sin(angle)
   fCos = cos(angle)
   
   pos0 = {}
   pos0.x = 0
   pos0.y = 0
   pos1 = rotate(dx, dy, fSin, fCos, true)
   
   vel0 = rotate(sprite0.GetData().dx, sprite0.GetData().dy, fSin, fCos, true)
   vel1 = rotate(sprite1.GetData().dx, sprite1.GetData().dy, fSin, fCos, true)
   
   'collision reaction'
   vxTotal = vel0.x - vel1.x
   vel0.x = vel1.x
   vel1.x = vxTotal + vel0.x

   'update position'
   pos0.x = pos0.x + vel0.x
   pos1.x = pos1.x + vel1.x

   'rotate positions back'
   pos0F = rotate(pos0.x, pos0.y, fSin, fCos, false)
   pos1F = rotate(pos1.x, pos1.y, fSin, fCos, false)

   'adjust positions to actual screen positions'
   sprites[index0].MoveOffset(pos0F.x, pos0F.y)    
   sprites[index1].MoveOffset(pos1F.x, pos1F.y)
   'rotate velocities back'
   vel0F = rotate(vel0.x, vel0.y, fSin, fCos, false)
   vel1F = rotate(vel1.x, vel1.y, fSin, fCos, false)
   sprites[index0].SetData( {dx: vel0F.x, dy: vel0F.y, index: index0} )
   sprites[index1].SetData( {dx: vel1F.x, dy: vel1F.y, index: index1} )
   sprites[index0].SetMemberFlags(0)
   sprites[index1].SetMemberFlags(0)    
   return sprites
   
End Function

Function rotate(x as float, y as float, fSin as float, fCos as float, reverse as boolean) as object
   res = {}
   if (reverse)
       res.x = (x * fCos + y * fSin)
       res.y = (y * fCos - x * fSin)
   else
       res.x = (x * fCos - y * fSin)
       res.y = (y * fCos + x * fSin)    
   endif
   return res
End Function
0 Kudos
Komag
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

good progress 🙂
0 Kudos
marcelo_cabral
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

The remote control emulation was the most complicated feature to find a solution so far, as this tool is an interpreter, it's basically a big-infinite-synchronous-nested-loop, and even moving it to a "web worker" to free the browser UI thread, Javascript does not implement any way to to stop the web worker and process the event log. After nights of investigation I discovered SharedArrayBuffer() and it did the trick.

Below is a video of the emulator running one of the early prototypes of Prince of Persia with no code changes! The performance is bad as the code is repainting every single wall tile every frame, and as an interpreter things get slow. I will work to improve both the emulator and the sample code to make it faster.

0 Kudos
Komag
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

😄 8-)
0 Kudos
marcelo_cabral
Roku Guru

Re: BrightScript Draw 2D Emulator (in development)

Great progress during this labor day weekend! The video below shows a quick demonstration of the current state running a full version of my open source remake of Lode Runner. The same zip file that you can load on a Roku, now you can run on your Chrome browser.

To download and play the game follow this link: https://github.com/lvcabral/Lode-Runner-Roku/releases/tag/v0.17.700

[youtube:20u0gema]zY3DvM6LAXU[/youtube:20u0gema]
0 Kudos
Need Assistance?
Welcome to the Roku Community! Feel free to search our Community for answers or post your question to get help.

Become a Roku Streaming Expert!

Share your expertise, help fellow streamers, and unlock exclusive rewards as part of the Roku Community. Learn more.