-
Notifications
You must be signed in to change notification settings - Fork 246
Pressing keys and clicking mouse
In this part, we'll learn how to handle keyboard and mouse input from the player and utilize it to plant trees and move camera to look around the forest.
So far, we've learned how to create windows, load pictures and draw sprites, even move them. But we didn't have any control over them, except through the code. That's about to change right now. Let's start with this code, you should be able to understand every single line of it, by now.
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)
}
win.SetSmooth(true)
for !win.Closed() {
win.Clear(colornames.Whitesmoke)
win.Update()
}
}
func main() {
pixelgl.Run(run)
}Today, we'll be planting trees, but we'll get clever about it. Instead of having a separate PNG file for each type of tree, we're going to use this spritesheet.
![]()
As you can see, a spritesheet is a single image file that contains multiple other images in it. In our case, the spritesheet looks really tiny, because it's pixel art. Don't worry, we'll scale them up so they look a lot better.
Download the spritesheet to the directory of your program and let's go! First, we need to load the spritesheet.
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
win.SetSmooth(true)
spritesheet, err := loadPicture("trees.png")
if err != nil {
panic(err)
}Now, let's just go ahead and draw one of the trees from the spritesheet. How do we do that? Remember, sprite constructor takes two arguments: the actual picture and a rectangle, which specifies, which portion (frame) from the picture we want to draw. So, it's fairly easy, since we know that each tree in the spritesheet is 32x32 pixels large.
Here, we create a sprite that draws the lower-left tree from the spritesheet and draw it to the center of the screen.
spritesheet, err := loadPicture("trees.png")
if err != nil {
panic(err)
}
tree := pixel.NewSprite(spritesheet, pixel.R(0, 0, 32, 32))
tree.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
for !win.Closed() {
win.Clear(colornames.Whitesmoke)
tree.Draw(win)
win.Update()
}Let's run the code!
![]()
Oh sure, the tree it very tiny because we didn't scale it up. Let's fix that right away!
tree := pixel.NewSprite(spritesheet, pixel.R(0, 0, 32, 32))
tree.SetMatrix(pixel.IM.Scaled(0, 16).Moved(win.Bounds().Center()))![]()
That's better, but it's not good either. The tree is really blurry. That's because we've told the window to draw pictures smoothly. When they get scaled up, they end up looking like this. When doing pixel art, this is far from appropriate. We need to disable it.
Just delete this line.
win.SetSmooth(true) // delete thisLet's run the code now!
![]()
Much better! We'd like to be able to draw all types of trees, not just this one. For that we'll create a slice of rectangles. Each rectangle in this slice will be the portion of one of the trees. Since each tree is 32x32 pixels and they're packed together as tightly as possible, it's very easy.
spritesheet, err := loadPicture("trees.png")
if err != nil {
panic(err)
}
var treesFrames []pixel.Rect
for x := spritesheet.Bounds().Min.X(); x < spritesheet.Bounds().Max.X(); x += 32 {
for y := spritesheet.Bounds().Min.Y(); y < spritesheet.Bounds().Max.Y(); y += 32 {
treesFrames = append(treesFrames, pixel.R(x, y, x+32, y+32))
}
}Here we cycle through all of the rectangles of the trees in the spritesheet. Note, that we cycle from Min to Max of the spritesheet bounds. Never rely on an assumption that bounds start at (0, 0). It might not be true.
Now replace this line
tree := pixel.NewSprite(spritesheet, pixel.R(0, 0, 32, 32))with this one
tree := pixel.NewSprite(spritesheet, treesFrames[6])See for yourself, that it works good.
Now that we've got the spritesheet of trees set up, we can go ahead and plant them using mouse!
Window has got a few methods for dealing with the user input. For example, win.Pressed checks whether a key on the keyboard or a button on the mouse is currently pressed down.
However, that's not what we want right now. We only want to plant a new tree when the user clicks the mouse. For that, we can use win.JustPressed which only checks whether a key or a mouse button has just been pressed down.
Just pressed means pressed somewhere between the previous and last call to win.Update.
There's also one more important method, win.MousePosition, which returns the position of the mouse inside the window.
With this knowledge, we can progress and type some code. First of all, let's delete all the lines dealing with the sprite we used to test whether our spritesheet works.
tree := pixel.NewSprite(spritesheet, treesFrames[6]) // delete
tree.SetMatrix(pixel.IM.Scaled(0, 16).Moved(win.Bounds().Center())) // delete
for !win.Closed() {
win.Clear(colornames.Whitesmoke)
tree.Draw(win) // delete
win.Update()
}Now, since we'll be planting many trees, we need to store them somewhere. Let's add an initially empty slice of tree sprites.
var trees []*pixel.Sprite
for !win.Closed() {
win.Clear(colornames.Whitesmoke)
win.Update()
}Finally, we add the code to actually plant them!
for !win.Closed() {
if win.JustPressed(pixelgl.MouseButtonLeft) {
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
tree.SetMatrix(pixel.IM.Scaled(0, 4).Moved(win.MousePosition()))
trees = append(trees, tree)
}Here we use the win.JustPressed method, which takes one argument: the button we want to check. It returns true if the button was just pressed down. Here's the list of all available buttons.
If the mouse actually got pressed, we create a new sprite with a random tree image from the spritesheet, scale it up four times, set it's position to the current position of the mouse, and add it to the slice of all trees.
One more thing, we need to draw the trees to the window.
win.Clear(colornames.Whitesmoke)
for _, tree := range trees {
tree.Draw(win)
}
win.Update()Perfect! Run the code and try clicking around to see that everything works!
![]()
Finally, let's change that dull industrial background. We're in the forest man!
win.Clear(colornames.Forestgreen)![]()
Before we add the camera, we need to understand another game development concept: game space and screen space. In case you're already familiar with this concept, feel free to skip this section.
Each object in a game is located somewhere in the game world. Some objects may move. The game world is usually too large to fit on the screen. That's why the player only sees a part of the world at any moment. The portion of the world that the player sees is determined by the camera.
Now, let's consider our trees. They are planted once and never move since. However, when the camera moves, the trees move all over the screen in the opposite direction to the movement of the camera. They move on the screen although they're static within the game.
Game space determines positions of objects inside the game. Our trees never move within the game space. Their position in the game space is constant, static. However, camera determines where on the screen they're located.
Screen space determines positions of objects on the screen. This position is affected by the camera.
In order to simulate a camera, we need to be able to translate between the game space and the screen space. Given a position in the game space, we need to calculate where on the screen it is located. Given a position on the screen, we need to calculate the position in the game the position on the screen corresponds to.
That's where matrices come handy again.
Now, we're ready to add the camera to look around our forest. For that, we'll create a new variable that stores the position of the camera in game space.
var (
camPos = pixel.V(0, 0)
trees []*pixel.Sprite
)
for !win.Closed() {The position of the camera determines the position in the game space that should be located at the center of the screen.
Now we somehow move all the trees so that they're located at the right position on the screen. However, changing their matrices would be wrong. Those should only determine their position in the game space. It would also be very ineffective. There's another way, win.SetMatrix method. Using this method, we can set a matrix for the whole window. Each drawn sprite will be put through this matrix. All we need to do in order to implement the camera is to set the right matrix for the window.
Let's add the camera matrix!
for !win.Closed() {
cam := pixel.IM.Moved(win.Bounds().Center() - camPos)
win.SetMatrix(cam)
if win.JustPressed(pixelgl.MouseButtonLeft) {All that the camera matrix does is that it moves the camera position to the center of the screen, which is what we want. Try running the code now!
![]()
Ugh, the planting is now totally off, clicking does not plant under the mouse any more! That makes sense. The center of the screen is the positon (512, 384) in the screen space. The win.MousePosition method returns the position of the mouse in the screen space. However, we're planting the trees in the game space. Where in the game space is the position (512, 384) located. Well, exactly there, but since the position of the camera is at (0, 0), this far from the center of the screen. We need to be able to take a screen position and determine it's equivalent in the game space.
With Pixel, this is very easy.
Matrix comes with a pair of handy methods: Project and Unproject. Project takes a vector and transforms it by the matrix. Unproject does the opposite.
Our camera matrix transforms positions from the game space into the screen space. We want to transform the mouse position in the screen space into the game space. That's why we need to use Unproject.
if win.JustPressed(pixelgl.MouseButtonLeft) {
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
mouse := cam.Unproject(win.MousePosition())
tree.SetMatrix(pixel.IM.Scaled(0, 4).Moved(mouse))
trees = append(trees, tree)
}Now everything works!
Static camera is kind of pointless, let's get it moving.