Skip to content

Drawing a Sprite

Michal Štrba edited this page Apr 2, 2017 · 30 revisions

In this part, we'll learn how draw pictures on the screen using sprites.

What's sprite?

Sprite is an ancient concept in video game development. In the old times, a sprite was usually a 16x16px or 32x32px "stamp" that could be stamped on the screen. One sprite could be the picture of the player, more sprites could be used to display enemies and platforms, and so on. This allowed efficient drawing of complicated scenes. Here's a screenshot of an old C64 game shamelessly taken from Wikipedia.

Today, sprites still follow the same concept, however, allow for much wider range of possibilities. A sprite is basically a picture, which can be drawn somewhere on the screen. In Pixel, as we'll learn later, sprites (alongside all other graphical objects) can be drawn not only to the screen, but onto an arbitrary Target.

Previous lesson

We'll start off with the code from the end of the previous lesson.

package main

import (
	"github.com/faiface/pixel"
	"github.com/faiface/pixel/pixelgl"
	"golang.org/x/image/colornames"
)

func run() {
	cfg := pixelgl.WindowConfig{
		Title:  "Pixel Rocks!",
		Bounds: pixel.R(0, 0, 1024, 768),
		VSync:  true,
	}
	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		panic(err)
	}

	win.Clear(colornames.Skyblue)

	for !win.Closed() {
		win.Update()
	}
}

func main() {
	pixelgl.Run(run)
}

Picture

Before we can draw any sprites, we first need to have some pictures to use. If scroll through the Pixel documentation, you'll find this function.

func PictureDataFromImage(img image.Image) *PictureData

What's PictureData? Well, don't get scared, it's simple. In Pixel, pictures can have a large variety of forms. There are pictures stored in the memory, there are pictures stored in the video memory, and so on. What does this hint? Yes, there's an interface called Picture. To create a sprite, we just need a picture of any form, and PictureData is the simplest one to obtain, it's just some pixels stored in the memory. Also, as you can see, we can create it from the standard Go's image.Image.

So, we first need to load an image.Image. If you're not familiar with the image package from the standard library, I highly recommend you read this article.

First, we need to import the "image" package and we also need to "underscore import" the "image/png" package to load PNG files. We also need to import the "os" package to load files from the filesystem.

import (
	"image"
	"os"

	_ "image/png"

	"github.com/faiface/pixel"
	"github.com/faiface/pixel/pixelgl"
	"golang.org/x/image/colornames"
)

Next, we'll write a short helper function to load pictures.

func loadPicture(path string) (pixel.Picture, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	img, _, err := image.Decode(file)
	if err != nil {
		return nil, err
	}
	return pixel.PictureDataFromImage(img), nil
}

Let's break down this function. First, it opens a file using the "os" package. We mustn't forget to close the file. Second, we decode the image file using the "image" package. In our case, we can only decode PNG files, since we've only imported "image/png". If we've wanted to be able to load and decode more formats, we'd need to import the respective package (such as "image/jpeg" for decoding JPEG files). Finally, we convert the image.Image to PictureData using the pixel.PictureDataFromImage function. Notice, that the return type is just pixel.Picture. There's no good reason for that, it simply doesn't matter. All the information that we need about the return type right now is that it's a Picture.

Pardon the rather complicated nature of loading a picture from a file in Pixel. It might get simpler someday, I just haven't figured out the right way yet.

Now, we need an actual PNG file to load a picture from. I'll use a beautiful hiking gopher from this repo, which contains many free beautiful images of gophers in various situations of their life. You can download an image from there, or directly use this one, which I'll use.

Just download the PNG image file into the directory of your Go program. Now, we're ready to load it.

func run() {
	cfg := pixelgl.WindowConfig{
		Title:  "Pixel Rocks!",
		Bounds: pixel.R(0, 0, 1024, 768),
		VSync:  true,
	}
	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		panic(err)
	}

	pic, err := loadPicture("hiking.png")
	if err != nil {
		panic(err)
	}

We load the picture and panic if an error occurred while loading it. Panicking is sufficient in this situation, since the file should just be there. In a real game, a more sophisticated error handling would be appropriate.

Sprite

Now that we've loaded a picture, we're ready to create a sprite.

	pic, err := loadPicture("hiking.png")
	if err != nil {
		panic(err)
	}

	sprite := pixel.NewSprite(pic, pic.Bounds())

Let's break down this line. We're using the function pixel.NewSprite, which we've never seen before. This function, as we can tell, creates a new sprite from a picture. The first argument to this function is the picture itself. No wonders here. What's the second argument? The second argument is a rectangular portion of the picture that we want our sprite to draw. Since we want to draw the whole picture, we just use pic.Bounds().

Now we're ready to draw the sprite to the window. Drawing a sprite to the window is as simple as calling sprite.Draw(win).

	sprite := pixel.NewSprite(pic, pic.Bounds())

	win.Clear(colornames.Skyblue)
	sprite.Draw(win)

	for !win.Closed() {
		win.Update()
	}

Wonderful! Let's run the code now. If you didn't make any mistakes and you have the PNG file in the right directory, you should see this.

Matrix

That's not very good. Our sprite's got drawn to the lower-left corner of the window and we can't event see all of it. Why is that?

We didn't tell the sprite where on the screen we want it to be drawn. By default, sprite draws to the position (0, 0), which in our case, is the lower-left corner of the window (the y-axis increases upwards). Also, sprites are anchor by their center, that's why we're able to see it's upper-right quadrant. The center of the sprite is located exactly at the position (0, 0).

To move a sprite to a different position, we need to change it's matrix. No scary stuff here, Matrix is the way Pixel handles transformations, such as movement, rotations and scaling. We'll learn a lot more about it in the next part.

To change the matrix of a sprite, we'll use the sprite.SetMatrix method.

	sprite := pixel.NewSprite(pic, pic.Bounds())

	win.Clear(colornames.Skyblue)

	sprite.SetMatrix(pixel.IM)
	sprite.Draw(win)

What's pixel.IM? The letters IM stand for identity matrix. This is a matrix that doesn't change anything. So, adding this line of code makes no difference. However, Matrix has a handful of useful methods for adding transformations to it. Let's change that line.

	sprite := pixel.NewSprite(pic, pic.Bounds())

	win.Clear(colornames.Skyblue)

	sprite.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
	sprite.Draw(win)

Calling pixel.IM.Moved adds a movement to the matrix. As you can tell, we moved the sprite to the center of the window's bounds, which is the center of the window. Let's run the code now!

And just for the sake of freshness, let's change the background color.

	win.Clear(colornames.Greenyellow)

In the next part, we'll learn how to move, rotate and scale sprites using the matrix and also how to add vectors using the + operator and many more things. Things will be moving, dynamically!

Here's the whole code of the program from this part.

package main

import (
	"image"
	"os"

	_ "image/png"

	"github.com/faiface/pixel"
	"github.com/faiface/pixel/pixelgl"
	"golang.org/x/image/colornames"
)

func loadPicture(path string) (pixel.Picture, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	img, _, err := image.Decode(file)
	if err != nil {
		return nil, err
	}
	return pixel.PictureDataFromImage(img), nil
}

func run() {
	cfg := pixelgl.WindowConfig{
		Title:  "Pixel Rocks!",
		Bounds: pixel.R(0, 0, 1024, 768),
		VSync:  true,
	}
	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		panic(err)
	}

	pic, err := loadPicture("hiking.png")
	if err != nil {
		panic(err)
	}

	sprite := pixel.NewSprite(pic, pic.Bounds())

	win.Clear(colornames.Greenyellow)

	sprite.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
	sprite.Draw(win)

	for !win.Closed() {
		win.Update()
	}
}

func main() {
	pixelgl.Run(run)
}