Going to the Stars With SDL2 and Go
Tags: go, sdl2, atlg
Published: 2019-05-12 05:58:41

---
title: Going to the Stars With SDL2 and Go
published: true
description: "Take to the stars"
tags: go, sdl2, atlg
cover_image: https://dev.shindakun.net/img/attemptinggo_w.png
series: "ATLG SDL2"
---

# Attempting to Learn SDL2

## Its Full of Stars

![stars](https://thepracticaldev.s3.amazonaws.com/i/045yug7vvbse3n3zb1jl.gif)
##### It looks better in real life, honest

Welcome back to another post. I'm still taking some time on things other the normal APIs or web servers. Instead this time we're doing our own riff on an article from the latest issue of [Wireframe](https://www.raspberrypi.org/blog/create-an-arcade-style-zooming-starfield-effect-wireframe-issue-13/) by one [Daniel Pope](https://twitter.com/lordmauve). The article covers a basic Python implementation of a star field warp similar to what was seen in by Konami's Gyruss.

We're not going to do a one-for-one reimagining since we're leaving out the speed changing part for now. Maybe we'll add that next week if we don't transition back to working on the roguelike.

***

## Code Walkthrough

The first thing you'll note if you compare this to the [Python code](https://github.com/Wireframe-Magazine/Wireframe-13/blob/master/stars.py) is that we've got a lot more setup to do. A large chunk of the code is actually dedicated to getting SDL2 ready to go. After that, it is just a matter of updating our star field array and writing it to a texture. Ready? Well then let's dive in.

### main.go

As always we'll start off with our imports. We'll be using `math` and `time` packages from the standard library. And of course, we'll [import the SDL2](https://github.com/veandco/go-sdl2) bindings to actually make something appear on the screen. Using SDL2 gives us an easy path to release a cross-platform application.

```go
package main

import (
  "math"
  "math/rand"
  "time"

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

We're setting up a few constants, the window size, how fast we want the warp to start at, and the center of the screen. Oh, and the number of stars we want to use in the warp display.

```go
const (
  winWidth      = 1280
  winHeight     = 720
  minWarpFactor = 0.1
  numStars      = 300
  centerX       = winWidth / 2
  centerY       = winHeight / 2
)
```

The `position` struct is going to be used to hold a star's current coordinates. We'll also use it to hold our velocities. I suppose we didn't need to call it `position` since that's not quite what it is in that case, `coords` might have been better, or just `xy`, or something - you get the idea.

```go
type position struct {
  x float64
  y float64
}
```

Our basic `star` struct is pretty simple, it holds the position, the velocity, and the brightness (current color). We hold a single `byte` value for the brightness and apply it to the red, green, and blue color channels to make a color from black to white. We then set up a `stars` struct which will hold our `[]star`.

```go
type star struct {
  pos        position
  vel        position
  brightness byte
}

type stars struct {
  stars []star
}
```

`randFloat64()` is a small helper function that will help us create the initial angle our new star will leave the center of the screen from.

```go
func randFloat64(min float64, max float64) float64 {
  rand.Seed(time.Now().UTC().UnixNano())
  return min + rand.Float64()*(max-min)
}
```

`clear()` does what it says on the tin. We use this to quickly clear out of pixel "map" in between screen draws. This way we start from a blank canvas and don't have trails. It does look pretty nice when you skip clearing, creating a nice trail effect but I haven't implemented anything to clean up so eventually, the screen would fill with mostly white. `setPixel()` is used to write our stars in the correct location with the current color starting from black `(0,0,0)`.

```go
// clear a slice of pixels.
func clear(pixels []byte) {
  for i := range pixels {
    pixels[i] = 0
  }
}

func setPixel(x, y int, c byte, pixels []byte) {
  index := (y*winWidth + x) * 4

  if index < len(pixels)-4 && index >= 0 {
    pixels[index] = c
    pixels[index+1] = c
    pixels[index+2] = c
  }
}
```

Here we create a `newStar()`! The code is more or less the same as the original article. I did leave out the trailing tail though since I'm not using SDL2s line draw - instead, we draw pixels directly into a texture (you'll see that later on).

```go
func newStar() star {

  // # Pick a direction and speed
  // angle = random.uniform(-math.pi, math.pi)
  angle := randFloat64(float64(-3.14), float64(3.14))

  // speed = 255 * random.uniform(0.3, 1.0) ** 2
  speed := 255 * math.Pow(randFloat64(float64(0.3), float64(1.0)), 2)

  // # Turn the direction into position and velocity vectors
  // dx = math.cos(angle)
  dx := math.Cos(angle)

  // dy = math.sin(angle)
  dy := math.Sin(angle)

  // d = random.uniform(25 + TRAIL_LENGTH, 100)
  d := rand.Intn(100) + 25 //+ traillength

  // pos = centerx + dx * d, centery + dy * d
  pos := position{
    x: centerX + dx*float64(d),
    y: centerY + dy*float64(d),
  }

  // vel = speed * dx, speed * dy
  vel := position{
    x: speed * dx,
    y: speed * dy,
  }

  s := star{
    pos:        pos,
    vel:        vel,
    brightness: 0,
  }

  return s
}
```

On each loop through our main... loop, we'll need to update the positions of the stars. We're passing in the `elapsedTime` but in the case, we're not actually using it in this function. We could do some updating in the future to make sure everything runs a stable frame rate and moves the proper distances within those frames. But, for this exercise, we're just going to roll with it. I also decided to just use a simple incrementing brightness. It's probably a touch to fast at getting brighter but it works.

```go
func (s *stars) update(elapsedTime float32) {

  // calculate the stars new position
  for i := 0; i < len(s.stars); i++ {
    newPosX := s.stars[i].pos.x + (s.stars[i].vel.x * minWarpFactor) //* dt
    newPosY := s.stars[i].pos.y + (s.stars[i].vel.y * minWarpFactor) //* dt

    // if we're off the screen with the new position reset else update position
    if newPosX > winWidth || newPosY > winHeight || newPosX < 0 || newPosY < 0 {
      s.stars[i] = newStar()
    } else {
      s.stars[i].pos.x = newPosX
      s.stars[i].pos.y = newPosY

      // # Grow brighter
      // s.brightness = min(s.brightness + warp_factor * 200 * dt, s.speed)
      if s.stars[i].brightness < 255 {
        s.stars[i].brightness += 40
      }
    }
  }
}
```

Our `draw()` loops through the stars and calls `setPixel()` to write each star "pixel" into our slice.

```go
func (s *stars) draw(pixels []byte) {
  for i := 0; i < len(s.stars); i++ {
    if int(s.stars[i].pos.x) >= 0 {
      setPixel(int(s.stars[i].pos.x), int(s.stars[i].pos.y), s.stars[i].brightness, pixels)
    }
  }
}
```

Now we have our `main()`! I'm going to kind of jump over the vast majority of the SDL2 boilerplate code. It's all there to get SDL all set and ready to run. The code is pretty readable but if you have any questions feel free to let me know in the comments. A look at the [Go/SDL2 examples in the repo](https://github.com/veandco/go-sdl2#examples) is worth it as well.

```go
func main() {
  err := sdl.Init(sdl.INIT_EVERYTHING)
  if err != nil {
    panic(err)
  }
  defer sdl.Quit()

  sdl.SetHint(sdl.HINT_RENDER_SCALE_QUALITY, "1")

  window, err := sdl.CreateWindow("Stars", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, int32(winWidth), int32(winHeight), sdl.WINDOW_SHOWN)
  if err != nil {
    panic(err)
  }
  defer window.Destroy()

  renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED)
  if err != nil {
    panic(err)
  }
  defer renderer.Destroy()

  tex, err := renderer.CreateTexture(sdl.PIXELFORMAT_ABGR8888, sdl.TEXTUREACCESS_STREAMING, int32(winWidth), int32(winHeight))
  if err != nil {
    panic(err)
  }
  defer tex.Destroy()
```

Now that SDL is all set and ready to go we need to get our initial star set ready to go. First, we'll set up our basics, `elpasedTime`, our `pixels` map, and our `starField`. `all` is going to hold the full set of stars.

```go
  var elapsedTime float32
  pixels := make([]byte, winWidth*winHeight*4)
  starField := make([]star, numStars)
  all := &stars{}

  for i := 0; i < len(starField); i++ {
    all.stars = append(all.stars, newStar())
  }
```

We're using the following `for` loop to run through 2000 iterations of our `update()` and `clear()` functions. This makes it so we don't pop up a black screen and then just dump our a new set of stars. That doesn't look very nice. 2000 might be a bit much but I don't think this adds any significate time to the start up so it should be OK.

```go
  for i :=; i < 2000; i++ {
    all.update(nil)
    clear(pixels)
  }
```

And here we are our final `for` loop. This will set up our event to shut down SDL when we exit. Then `update()` and `draw()` our stars. That's a bit misleading since we're not actually drawing them to screen but instead to a texture. Using it as a texture buffer, we then draw the texture to screen and clear out our pixel set to make them ready for the next go around. Finally, we've got a little delay built in which hopefully will give us a steady frame rate. I've never timed it out so I'm not sure just how well it works in practice but, the screen seems smooth enough.

```go
  for {
    frameStart := time.Now()
    for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
      switch event.(type) {
      case *sdl.QuitEvent:
        return
      }
    }

    all.update(elapsedTime)
    all.draw(pixels)

    tex.Update(nil, pixels, winWidth*4)
    renderer.Copy(tex, nil, nil)
    renderer.Present()
    clear(pixels)
    elapsedTime = float32(time.Since(frameStart).Seconds() * 1000)
    if elapsedTime < 7 {
      sdl.Delay(7 - uint32(elapsedTime))
      elapsedTime = float32(time.Since(frameStart).Seconds() * 1000)
    }
  }
}
```

## Wrapping Up

And there we have it! Another use of Go that isn't focused on business logic! I've been toying with the idea of re-writing a portion of the roguelike code to use SDL2 instead of a plain console so I can incorporate effects. Though I suppose I'd have to put in some time learning how to do shaders in SDL. Who knows where we may end up next week, more graphics type stuff, or roguelikes, or back to APIs...

Now, I really need to prep an update to the ATLG repo is a few articles behind!

***

You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.

{% github shindakun/atlg %}

***

| Enjoy this post? |
|:----:|
| [How about buying me a coffee?](https://ko-fi.com/shindakun) |

***