Cave-In: GUI Game In Red

Because I've idly started to think about what a Ren-C GUI dialect would look like (leveraging the web browser layout engine, of course), I've decided to reluctantly learn about Rebol's historical GUIs. VID code has traditionally been an eyesore for me, but in the coming months I'll try to dedicate a little time to look at examples--both old and new--from all the various incarnations of the VID dialect (VID, Red VID, VID/S (Spaces), RebGUI, GLASS, and I've just found out that DRAW is a different dialect from VID so there's probably at least 12 more variants of all this stuff)

To try and have a little fun with my first one, I'll look at the game "Cave-In" by @PlanetSizeCPU from Red Gitter:

Level.gif

The Code

The code is available on GitHub, and it has been committed to as recently as July.

Curiously, the level definitions themselves are not in Red syntax. They're like CSV files, except they use vertical bar as a separator instead of comma, and # for to-end-of-line comments

# ItemType|ObjectName|FaceName|FaceSize|FaceOffset|Rate|...
...
CAVERN|cavern1|cave|1599x600|0x0|0:0|0x0|0:0|0|0|cavern.png
GOLDBG|gold1|gld1|8x12|340x564|0:0|0x0|0:0|0|1|gold1.png
GOLDBG|gold2|gld2|8x12|310x461|0:0|0x0|0:0|0|1|gold1.png
GOLDBG|gold3|gld3|8x12|650x51|0:0|0x0|0:0|0|1|gold1.png
GOLDBG|gold4|gld4|8x12|200x185|0:0|0x0|0:0|0|1|gold1.png
...

The implementation is just four .red source files, three of which are small and the main implementation lives in something called makegame.red

https://github.com/planetsizecpu/Cave-In/blob/master/src/makegame.red

There's an Observable Lack Of... "Rebolism"

I don't want to be too critical of someone's early efforts in programming. This actually reminds me a lot of when I was a kid and got Turbo Pascal for a 286 PC, and tried to write a variation of a Commodore 64 game called "React". The example snippets I learned from were very simple and none even showed you how to break your code into different files, so I just had one long .PAS file... that grew and grew. Instead of seeing the growing code with tons of conditionals as a problem, I was kind of excited about how big it was getting!

But that aside, there clearly are a lot of repetitive patterns and lack of abstraction. To pick on one short example at random:

if (first f/extra/name) = #"a" [
    foreach img GameData/AgentDead [f/image: get img wait GameData/AgentDeadDelay]		
]
if (first f/extra/name) = #"f" [
    foreach img GameData/FAgentDead [f/image: get img wait GameData/AgentDeadDelay]		
]
if (first f/extra/name) = #"p" [
   foreach img GameData/PAgentDead [f/image: get img wait GameData/AgentDeadDelay]		
]
if (first f/extra/name) = #"y" [
    foreach img GameData/YAgentDead [f/image: get img wait GameData/AgentDeadDelay]		
]

But you can see that large-scale repetition of this kind is pervasive.

What I find perplexing is that if people aren't using Rebol's mechanisms, what do they actually like about the language enough that keeps them going with it?

Ren-C takes the refactoring and streamlining potential to new heights. It makes it easier to put expressions all on one line with good error locality with VOID-in-NULL-out. It gives you foolproof truthiness of any item you can put in a block (only antiforms can be are falsey). NIHIL lets you use put code midstream in any evaluative context without disturbing it, for things like ASSERT and ELIDE to be anywhere you please. The loop result protocol is super neat, and I could go on and on...

...but the basic idea of enabling smooth composition has been there since Rebol2, e.g. you could rewrite the above as:

if (dead: select [
    #"a" AgentDead
    #"f" FAgentDead
    #"p" PAgentDead
    #"y" YAgentDead
] first f/extra/name) [
    foreach img GameData/(dead) [
       f/image: get img wait GameData/AgentDeadDelay
    ]
]

As Carl said in Rebol: Not for everyone?:

"I've glanced over a lot of REBOL code written by a wide variety of programmers, and quite often I'm floored. Many programmers use REBOL like they're writing in C or BASIC. I can spot it in an instant; they did not bother to learn the fundamental concepts of the REBOL language. When I see that kind of code, I wonder why they bothered to use REBOL at all. C is better written in C. You will never hear me contest that fact."

...but the appeal may be "I can Use It Like BASIC!" I dunno.

Timers And Frame Rates

The definition of the main "layout" GameScr(een) includes a size and position, and uses what appears to be a RATE dialect keyword:

GameScr: layout [
    title "Cave-In"
    size 800x600
    origin 0x0
    space 0x0

    ; Info field is also used for event management!
    at 10x610 info: base 780x30 orange blue font [
        name: "Arial" size: 14 style: 'bold
    ] focus 
    rate GameData/GameRate on-time [
        info/rate: none 
        if CheckStatus [alert "END OF GAME" quit] 
        info/rate: GameData/GameRate
    ]
]

RATE is defined in the VID dialect as:

rate - An INTEGER or TIME that specifies the rate of time events for a face. This is used for animation or repetitive events (such as holding the mouse down on certain types of user interface styles). An INTEGER indicates the number of events per second. A TIME provides the period between events.

Cave-In apparently uses the time-based parameter, which it initializes and then overwites.

GameRate: 0:00:00.003
either system/platform = 'Windows [
    ; Win cant handle 3ms rate so it goes as fast as possible
    GameRate: 0:00:00.006
][
    ; For other OSs as GTK we must test this value as they handle right
    GameRate: 0:00:00.065
]

So I take it that rate <time or integer> on-time [...] means that you want callbacks to run the code in the block at the given rate. I'm not clear on why you would say ON-TIME there vs. just put the block there.

Searching rebol.org for other VID examples using rate, I found tweener.r that does:

 rate 1 feel [
     engage: func [face action event] [...]
 ]

I don't know if ON-TIME is Red-specific or what, or what other things you can put there, or what this means.

There is actually code here for adjusting the timings for different CPUs. That seems to undercut Red's value proposition if that's necessary...isn't this supposed to be speaking about a concrete amount of time that works on all platforms?

Main Game

Anyway, the spawning of a modal graphic window instance is done with VIEW. There's a splashscreen instantiation via VIEW, then a settings instantiation via VIEW, then an instance of GameScr with:

view/options GameScr [
    actors: context [on-key: func [face event][CheckKeyboard face event/key]]
]

So I'm not clear on what the /OPTIONS is for, that can't be specified in the layout itself. The Rebol2 documentation for VIEW says:

Additionally, calls to view can specify options, such as whether the window has borders and is resizable. Single options are provided as a word and multiple options are specified in a block.

out: layout [vh1 "This is a window."]
view/options out [resize no-title]

As per usual, I'm baffled...what goes in the layout and what doesn't? Why isn't Cave-In able to just view GameScr with the keyboard handler mentioned alongside the rate timer in the definitoin of GameScr?

In any case, the code that runs at the RATE calls a function CheckStatus that does some work, and returns a truthy value if the game is over or a falsey value to keep on going.

CheckStatus adds the effect of gravity to objects, but that particular global timed function doesn't do things like make the bad guys move around. It seems individual objects have timers associated with them, e.g. when an "Agent" is being instantiated from a Level it makes a FACE! which has its own RATE set, leading to callbacks of its own ON-TIME function:

"A" [  ; "(A)gent"
    set (w) make face! [
        type: 'base
        size: ItemObj/size
        offset: ItemObj/offset
        image: copy ItemObj/image
        extra: ItemObj
        rate: ItemObj/rate
        actors: context [
            on-time: func [f e][AgentMotion f]
        ]
        ItemObj/lives: 64
    ]
    append cave/pane (get w)
]

So then AgentMotion does the updating.

Okay, I Understand This One

I can see a little bit of the appeal for why someone would have fun hacking on it this way. Red is giving you some game-engine type autonomy to all the objects, with each face its own timer. You get some very simple things like an overlap? utility function to see if two faces overlap (though it just checks bounding boxes, not collision detection on non-transparent portions).

Now that I'm learning a bit of the terminology, this isn't really using "VID" the dialect hardly at all. The only VID code is in %cave.red and it does nearly nothing.

But it's using "View", which is kind of like a browser layout engine. VID is a shorthand for building View code--it's maybe a little like HTML for declaratively specifying the starting point of the DOM...that then gets dynamically juggled all around as objects after that.

Cave-In basically is like one of those pages where if you "View Source" the .HTML is nearly empty, and everything is created dynamically with code. But that code does not leverage small VID fragments, the way some dynamic JavaScript might use HTML snippets to build DOM code. (Could it?)

I'll add that I notice nowhere in the source for Cave-In is the on-change* method used, so this isn't using the "reactive programming" features added by Red, FWIW.

Tune in next week (month?) when I set aside some time to look at another Rebol GUI thing.