Your Digital Media Has Never Looked So Good

 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

BrightScript Draw 2D Emulator (in development)

Thu Aug 15, 2019 3:05 pm

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/sjbarag/brs
That is an interpreter for the BrigthScript language to be executed as a command line tool, it can execute BrigtScript files or line by line statements over the command line. Of course as text based tool it can only show results using the BrightScript "print" statement. It is still not complete, but has all the basic statements and objects, so I decided to collaborate with the project, to implement things that would allow me fork it and extend as an emulator for Draw 2D API, basically to make possible to run my games and apps off the Roku device. 

It is far from complete, basically it can draw on the screen and run animations, no iteraction with the program is possible now,  but it can already execute some of the examples created by Roku to demonstrate the Draw 2D API like this one:

Image

The code is in my fork 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
 
User avatar
Komag
Posts: 808
Joined: Fri Aug 22, 2014 3:42 am

Re: BrightScript Draw 2D Emulator (in development)

Fri Aug 16, 2019 12:47 am

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.
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Fri Aug 16, 2019 11:33 am

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.
 
User avatar
Komag
Posts: 808
Joined: Fri Aug 22, 2014 3:42 am

Re: BrightScript Draw 2D Emulator (in development)

Fri Aug 16, 2019 11:58 pm

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?
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Sat Aug 17, 2019 10:30 am

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).
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Tue Aug 20, 2019 11:17 pm

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



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
 
User avatar
Komag
Posts: 808
Joined: Fri Aug 22, 2014 3:42 am

Re: BrightScript Draw 2D Emulator (in development)

Tue Aug 20, 2019 11:49 pm

good progress :-)
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Fri Aug 23, 2019 9:38 am

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.

 
User avatar
Komag
Posts: 808
Joined: Fri Aug 22, 2014 3:42 am

Re: BrightScript Draw 2D Emulator (in development)

Sat Aug 24, 2019 9:44 pm

:D 8)
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Mon Sep 02, 2019 6:43 pm

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

 
User avatar
Komag
Posts: 808
Joined: Fri Aug 22, 2014 3:42 am

Re: BrightScript Draw 2D Emulator (in development)

Tue Sep 03, 2019 2:49 am

Nice, I tested it with Lode Runner (the new 0.17) and it runs great! I think I found a small bug with the game, when I press * for an extra life, the number doesn't change in the game, so I pressed it again, still no change, then I pressed / to restart the level, and my number of men went UP one. Thus I think * is increasing the number of men just not showing it, so I had increased it by two, then naturally decreased one when restarting the level with /

The emulator doesn't work with my game, probably various reasons of me not setting it up correctly. I do see the splash screen image correctly for a couple seconds, then it goes away to a blank black screen.
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Tue Sep 03, 2019 8:31 am

Komag wrote:
Nice, I tested it with Lode Runner (the new 0.17) and it runs great! I think I found a small bug with the game, when I press * for an extra life, the number doesn't change in the game, so I pressed it again, still no change, then I pressed / to restart the level, and my number of men went UP one. Thus I think * is increasing the number of men just not showing it, so I had increased it by two, then naturally decreased one when restarting the level with /

The emulator doesn't work with my game, probably various reasons of me not setting it up correctly. I do see the splash screen image correctly for a couple seconds, then it goes away to a blank black screen.

Before loading the zip file, right clock and choose "Inspect" to open Chrome Developer Tools, and click on the "Console" tab. Then load your zip and check the errors that will pop up. I'm now making Prince of Persia compatible, and started a list of changes needed, for the current state of the emulator. Send me a list of the errors I can advise you what to do.
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Tue Sep 03, 2019 8:55 am

The current main limitations are:
- roUrlTransfer - Not supported
- roXMLElement - Usage of @ to access attributes from roXMLElements
- roXMLElement - creation of sub elements and attributes
- RAF Components - Not supported
- roUniversalControlEvent can't be compared to integer
- Integer .toStr() method is still not supported, create your own function for that.
- Tokenize() is not implemented, replace it with Split()
- Undocumented parameter rgba in DrawObject() method not supported
- Reserved words like "mod" cannot be used as variables (Roku does allow that)
- Dim statement to create multi-dimensional arrays is not supported

Also there is a bug on the interpreter that messes up with if clauses when you have a if..then in a single line nested, my advise, for now do not use if then  in a single line at all, until this is fixed.

Most of those limitations will be addressed, but that's what we have for now, another issue (visual only)  is that the alpha (transparency) is not working as in Roku, sometimes things  "leak".
 
User avatar
squirreltown
Posts: 870
Joined: Sun Apr 21, 2013 2:20 pm

Re: BrightScript Draw 2D Emulator (in development)

Tue Sep 03, 2019 10:00 am

I've gotten one channel to show the splash screen,  and then nothing after. It's got about 20 errors like this:

index.ts:156 source/mediarsstoolkit.brs(175,35-36): Unexpected character '@'g @ index.ts:156

and one of these:

index.ts:156 source/main.brs(23,4-19): Attempting to retrieve property from non-iterable value  left: Invalid

This is just the landing page for a media player, it's pretty old.  My serious animation projects get 200-300 errors.

ETA: "- Undocumented parameter rgba in DrawObject() method not supported"
Pretty sure it's actually documented now. Shocking right? Anyway, none of my stuff will run without this.
Looks cool though.
Kinetics Screensavers
 
User avatar
marcelo.cabral
Topic Author
Posts: 377
Joined: Tue Mar 20, 2012 8:53 am

Re: BrightScript Draw 2D Emulator (in development)

Tue Sep 03, 2019 10:19 am

squirreltown wrote:
I've gotten one channel to show the splash screen,  and then nothing after. It's got about 20 errors like this:

index.ts:156 source/mediarsstoolkit.brs(175,35-36): Unexpected character '@'g @ index.ts:156

and one of these:

index.ts:156 source/main.brs(23,4-19): Attempting to retrieve property from non-iterable value  left: Invalid

This is just the landing page for a media player, it's pretty old.  My serious animation projects get 200-300 errors.

ETA: "- Undocumented parameter rgba in DrawObject() method not supported"
Pretty sure it's actually documented now. Shocking right? Anyway, none of my stuff will run without this.
Looks cool though.

Thanks for the feedback. Some comments about it:
The @ issue is the XMLElement limitation I mentioned above, you would need to access it using getattributes() method.
The "left invalid" means some object is invalid on the line described, it can be consequence of the issue above (or not)
BTW no media playback or SDK1 objects are supported only pure Draw2D SDK and vanilla BrightScript.
The "undocumented" is because the new SDK documentation was poorly migrated and they messed up completely the ifDraw2D documentation, I reported on the thread here already. And I will add the rgba parameter soon, I also use it on Prince of Persia for cross fading in several places

Who is online

Users browsing this forum: No registered users and 2 guests