5

I'm writing a little GUI system for my game in Go. So far my structure is kind of like this:

type Component interface {
    Update()
    Render(ctx)

    Translate()

    GetComponent() []Component
    AddComponent(c Component)

    AddInput()
    GetInput()
}

And then I would have say a Panel which implements these, and a button, etc. One issue I'm facing is it's generally quite messy and confusing to get your head around the structure of this thing. I don't know how to write idiomatic Go code, so I'm kind of writing it like it's Java or something.

Can anyone recommend a better approach that will keep my sanity intact? Thanks

To elaborate, this is particularly tedious:

func (f *Button) Update() {
    for _, c := range f.components {
        c.Update()
    }
}

I have to do that for every component I make, as well as I have to write

func (f Button) AddComponent(c Component) {
    f.components = append(f.components, c)
}

I feel like I'm repeating myself a lot!

  • 1
    “One issue I'm facing is it's generally quite messy and confusing to get your head around the structure of this thing.” – Could you explain what specifically your problems with this design are? How does this design not meet your needs? From the declaration you've shown, there's nothing inherently bad about the design. I can't guess what specifically troubles you. – amon Nov 24 '16 at 11:46
  • 1
    @amon Because each component has an array of components I have a lot of duplicate code where I have to actually add this field to the structures. Plus I have to do a for loop over the components in the Update/Render method to invoke their Update/Render methods. i.e. fn (f Foo) Update() { for _, c := range f.components { c.Update() } } – flooblebit Nov 24 '16 at 12:26
  • I figured there's either a better way to do this, or something inherently wrong with my design – flooblebit Nov 24 '16 at 12:26
2

You might want to extract common functionality of those interface methods to separate classes/functions and only implement handlers for logic specific to your components.

A slightly simplified example:

type Component interface {
    OnRender(ctx RenderContext)

    GetComponents() []Component
    AddComponent(Component)
}

Approach #0: Inherit methods that are implemented the same everywhere using anonymous fields

type BaseComponent struct {
    components []Component
}

func(c BaseComponent) GetComponents() []Component {
    return c.components
}

func(c *BaseComponent) AddComponent(a Component) {
    c.components = append(c.components, a)
}

Approach #1: Extract the common logic to a free function

func Render(c Component, ctx RenderContext) {
    // implement common render logic here

    // call component specific handler
    c.OnRender(ctx)

    // update children
    for _, child := range c.GetComponents() {
        Render(child, ctx)
    }
}

Approach #2: Extract the common logic to a dedicated "class" (struct)

type ComponentRenderer struct {
    // store common information required for rendering
}

func(cr *ComponentRenderer) Render(c Component, ctx RenderContext) {
    // implement common render logic here

    // call component specific handler
    c.OnRender(ctx)

    // update children
    for _, child := range c.GetComponents() {
        cr.Render(child, ctx)
    }
}

Example for a final component:

type Button struct {
    BaseComponent
    // button specific fields
}

func(b *Button) OnRender(ctx RenderContext) {
    // implement button specific rendering code here
}

Addendum for Approach #2: If you want, you can declare ComponentRenderer as an interface and implement specific renderers for different platforms (e.g. one for Windows, one for Linux, one for a web page, one for an ingame GUI, ...).

You can mix and match these different approaches for different purposes.

Note: You could try to implement a Render method on BaseComponent - but be aware that there is no virtual dispatch and no method overriding on structs in Go (so the Render method needs to somehow get the information about which handler to call from somewhere else). So I would only recommend Approach #0 for cases where every implementation uses the exact same logic.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.