veandco / go-sdl2

SDL2 binding for Go
https://godoc.org/github.com/veandco/go-sdl2
BSD 3-Clause "New" or "Revised" License
2.18k stars 219 forks source link

A non-"one and done" sample, sample request #413

Open madmagi opened 5 years ago

madmagi commented 5 years ago

Thanks for a great library.

Almost all of your samples are what would call "one and done" examples; that is build up something, draw it and that is it. Which is great, can learn specifically how a particular thing works.

However, in real life you end up building up objects, drawing and redrawing over and over. And that is where where I am struggling.

For example, draw A window, react to some input, now redraw, perhaps just adding a new item to the window.

When I look at how the sample code is done, i see ..

..renderer.Clear() Do build up of graphic routines, i.e. DrawLine, DrawImages, etc ..renderer.Present() ..renderer.UpdateSurface()

I look at the android code it is more comples with events, but of course your are trying to do other things in that code as well, .

when i try to do a basic event model where i put up something and listen to events i end up either blocking the redraws, or having some other issues either not redrawing, redrawing so much events don't occur, thread issues, or some other problems.

Would I would suggest is a simple example that shows a simple eventing model with redraw/update logic.

Thoughts?

veeableful commented 5 years ago

Hi @madmagi, do you have any specific example in mind? I created this example as a starter.

package main

import (
    "log"
    "os"

    "github.com/veandco/go-sdl2/sdl"
)

var (
    mouseX int32
    mouseY int32

    rects []sdl.Rect
)

func run() int {
    sdl.Init(sdl.INIT_EVERYTHING)

    window, err := sdl.CreateWindow("Render Event Loop", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, 640, 480, 0)
    if err != nil {
        log.Println(err)
        return 1
    }
    defer window.Destroy()

    renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED|sdl.RENDERER_PRESENTVSYNC)
    if err != nil {
        log.Println(err)
        return 2
    }
    defer renderer.Destroy()

    running := true
    for running {
        renderer.SetDrawColor(0, 0, 0, 255)
        renderer.Clear()

        for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
            switch t := event.(type) {
            case *sdl.QuitEvent:
                running = false
            case *sdl.KeyboardEvent:
                if t.Keysym.Sym == sdl.K_BACKSPACE {
                    rects = rects[0:0]
                }
            case *sdl.MouseButtonEvent:
                if t.Button == sdl.BUTTON_LEFT {
                    rects = append(rects, sdl.Rect{X: t.X, Y: t.Y, W: 10, H: 10})
                }
            case *sdl.MouseMotionEvent:
                mouseX = t.X
                mouseY = t.Y
            }
        }

        draw(renderer)
        renderer.Present()

        sdl.Delay(1000 / 60)
    }

    return 0
}

func draw(renderer *sdl.Renderer) {
    renderer.SetDrawColor(255, 255, 255, 255)

    for _, rect := range rects {
        renderer.FillRect(&rect)
    }

    renderer.FillRect(&sdl.Rect{X: mouseX, Y: mouseY, W: 10, H: 10})
}

func main() {
    os.Exit(run())
}
madmagi commented 5 years ago

I will take a look, thanks. Should have feedback or possible questions about my mental blocks later tonight. :)

madmagi commented 5 years ago

Ok, able to look at it. Here is my thoughts and would love to understand better.

In the example you are creating some rect objects, placing in slice array. prety straight forward. then after your event loop, you range over the rects and draw them out. correct?

So I can relate to that in what I have been attempting. In my case, I have a UX (Menus, Dialogs, Buttons, etc), each is a self contained object which ultimatly is in a rect. Each object can have a sdl.Scancode &/or a mouse rect where it is active within. I have a Draw Function for each object where the object draws itself by using the simple primitives such as renderer Copy(texture,rect,rect), DrawLine, DrawRect, FillRect, Color settings, to draw lines, text, blt graphics, etc. Of course order of doing these can be important.

So using your example as a baseline, I would need to create an array of operations to queue up, wait till I have done all the logic needed to exit the event loop, then send that array to my drawing (renderer) function to handle them all?

Everything I have tried so far, I either end up with things blocking in the event loop, getting only some of the events to fire (a back ground draws but not the button in the foreground), or I get redraws clobbering because the draw primitives are being called too often. Cannot seem to find the balance, hence the ask.

veeableful commented 5 years ago

Hi @madmagi, In order to unblock the thread, I'm thinking that perhaps the main thread should do only rendering. When the events come, they can be sent off to another go-routine via a channel and be processed there. How the UI elements will appear is then modified by that go-routine if necessary and the main go-routine will just call the UI elements' render function. I suppose there should be a way to wait for all background operations to finish too.

There's probably some details that I'm missing but overall, that's the idea! Perhaps I'll create a simple button example that fetches and saves an HTML page (for mocking the blocking part) to start with .

madmagi commented 5 years ago

@veeableful That does make sense. I did go down that road a couple of times with frustrating results. I think not knowing how the underlying portions of the SDL subsystem work have caused me some grief. One such adventure I ended up with panics due to thread issues; I think mutexes would have fixed that, but I find that when I start dropping mutexes in my code there usually is a sign of a design problem on my part ;). I did try a channel approach but that merely just moved where the block occurred, but probably should have dug deeper on that. Like I mentioned before, any help is well received. Just chatting with you has helped, as you mentioned that separation seems to be the key.

madmagi commented 5 years ago

Thanks for the suggestions; I finally made headway by doing the following:

I setup the main part of the application as a wrapper to create a) the graphics subsystem (SDL), the UX subsystem (GX) and the application (APP). I set the app to run within a seperate GO routine while leaving the GX and SDL on the main thread.

The main thread has the message pump for SDL, and the action classes for the GX work. I then setup a FIFO channel of Queue events to allow the app to send requests to the message pump and the interface to send status updates to the application. work gets funneled through the Queue and seems to make things work smoothly.

I ran into two weird problems that derailed me for more time than I would like, one was initializing the SDL package inside of a package instead of the main package. I now initialize the objects themselves in the main package and pass them into my sub package. The other was a timing problem caused between the message pump of the SDL and select/event queues with go. Once I pushed everything to operate out of the channels that problem went away. (I handle this by within the SDL event message poll, if an event fires, I send a message to the channel queue to get processed, allowing it to buffer and be processed in the right order)

Thanks again for your help, it pushed me into the right direction.

veeableful commented 5 years ago

Hi @madmagi, I'm glad you got it to work! And thanks for explaining how you got it to work as well. I'll be sure to keep those in mind when I create an example for this.