Vaya responsabilidad se me ha encomendado, no sólo tengo que escribir el primer artículo del año, sino que además es día de Reyes, el día de Reyes es una celebración cristiana que se realiza en diversos países entre ellos España, donde se reúne la familia y se produce un intercambio de regalos, los que más lo disfrutan son los niños que esperan con ansías a esos Reyes Magos de Oriente que les traen los regalos que han pedido en una carta que les envían. Aunque sea una fiesta cristiana, creo que el rito cristiano en muchos casos se omite, y lo que importa es pasarlo bien y tener muchos regalos.
Pero bueno, no venimos a hablar de historia, sino de Go, y teniendo está responsabilidad como ya habéis visto por el título del post, he dicho ¿por qué no? que mejor que recibir un videojuego en reyes, y que mejor que hacerlo tu mismo, como hacerlo todo en un sólo artículo es demasiado, he decidido que mejor partirlo en varias partes, ¿cuántas te estarás preguntando? pues no lo sé, unas cuantas, a ver a donde nos lleva esto.
¿Qué necesito para empezar?
Bueno, como ya te estarás imaginando necesitaremos bastantes herramientas externas para que todo esto funcione, tendremos que tener Unity instalado… es bromaaaa, sólo necesitaremos lo de siempre Go instalado y nuestro IDE favorito.
Para ponernos todos en la misma página, la estructura de directorios que utilizaremos es la siguiente:
Dentro del directorio internal
, como viene siendo costumbre en nuestras arquitecturas, encontraremos todo el código relacionado con el funcionamiento del juego en sí.
En resources
, tendremos las imágenes que necesitaremos que ya os adelantamos que hemos utilizado un pack gratuito bastante resultón, el cual podéis utilizar vosotros también, el autor es Kenney y tiene arte bastante divertido para hacer vuestros pinitos en esto de los juegos.
Además necesitaréis la librería que se encargará de realizar toda la verdadera magia, y no es otra que github.com/faiface/pixel.
Al final del artículo os daremos también el repositorio como siempre con todo el código.
Pues ya lo tenemos todo, pongámonos manos a la obra.
Run()
Vamos a empezar con algo muy simple, vamos a abrir una ventana con un título y le pondremos un color de fondo.
Lo primero es irnos a nuestro archivo cmd/main.go
y crear el siguiente código:
import (
"log"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
const (
windowWidth = 1024
windowHeight = 768
)
func main() {
pixelgl.Run(run)
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Friends of Go: Space Game",
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
log.Fatal(err)
}
win.Clear(colornames.Aqua)
for !win.Closed() {
win.Update()
}
}
Parece mucho código, pero tranquilos es bastante sencillo de seguir, vayamos por partes
func main() {
pixelgl.Run(run)
}
Lo primero que tenemos es una función main
con una línea de código, pixelgl.Run(run)
esta es una función que nos ofrece la librería de github.com/faiface/pixel, dentro del paquete pixelgl, la cual espera una func()
la cual se ejecutará después de cargar todo lo necesario para poder hacer uso del sistema de ventanas, pintar gráficos etc.
Dentro de nuestra función run()
, lo primero que nos encontramos es con una configuración:
cfg := pixelgl.WindowConfig{
Title: "Friends of Go: Space Game",
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
VSync: true,
}
pixelgl.WindowConfig, es un struct que tiene una serie de propiedades, para mantenernos simple hemos utilizado la más básicas.
- Title: será el nombre que reciba nuestra ventana.
- Bounds: Cuanto medirá nuestra ventana, en este caso la hemos hecho de 1024x768, pero podéis poner la resolución que queráis, además si añadis la opción,
Resizable
atrue
, podréis escalarla, y la famosa opciónVsync
, la cual sincroniza la tasa de frames de la ventana con el monitor.
Una vez tenemos nuestra configuración simplemente creamos la ventana, win, err := pixelgl.NewWindow(cfg)
, hacemos crashear la aplicación si algo ha ido mal, ya que este es el punto de inicialización, si algo ha ido mal aquí no podemos continuar.
Seguidamente, lo que haremos es utilizar el método Clear
de la ventana, para limpiar la información que haya en ella y además añadirle un color de fondo.
Por último lo más importante, sino estáis familiarizados con la creación de videojuegos esto os puede chocar un poco, pero nuestros juegos funcionan en un bucle infinito, que sería la representación de nuestros frames, en este caso, será hasta que cerremos la ventana, siempre dentro de este bucle, tendremos que ejecutar el método Update
de la ventana, el cual se encarga de actualizar el intercambio de buffers y los polls de eventos.
Si todo ha ido bien deberías tener algo similar a esto:
Vamos a poner un fondo
Todo bien hasta aquí, ya tenemos nuestra ventana funcionando, con un color así muy gopher, pero nuestro juego no va a triunfar con esos gráficos, así que ¿cómo añado un fondo?
Lo primero que vamos a hacer es crear un fichero, con una función auxiliar que nos irá muy bien durante todo nuestro desarrollo, dicha función sera la encargada de cargar nuestros gráficos, veamos como.
Lo primero crearemos un fichero internal/picture.go
que contendrá una función loadPicture(path string) (pixel.Picture, error)
como vemos ya aparece en escena un elemento nuevo, el pixel.Picture, pixel.Picture
es una interfaz que tiene un método Bounds()
el cual será muy útil para saber las medidas del rectángulo de nuestras imágenes.
Así que vamos a escribir el contenido de dicha función:
import (
"image"
_ "image/png"
"os"
"github.com/faiface/pixel"
)
func loadPicture(path string) (pixel.Picture, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
Como podemos ver el principio de la función nos lo sabemos de carrerilla, simplemente es abrir un fichero, con el os.Open
de toda la vida, lo siguiente no deja de ser igual de sencillo, primero llamaremos a image.Decode
, también nada nuevo en el horizonte, simplemente decodearemos el fichero para tener una imagen válida, recordad que cuando usamos el paquete image
, tenemos que cargar aquellos formatos que queremos decodear, en este caso, png
, por ello incluimos el import pertinente.
Por último teniendo ya nuestra imagen, lo que haremos es llamar al método de PictureDataFromImage
del paquete pixel
, pasando nuestra imagen, esto nos devolverá un pixel.Picture
y un error
que es todo lo que necesitamos.
Vamos a lo complicado, pintar nuestro fondo, si habéis revisado los archivos y sino os lo adelanto nuestro fondo, es un cuadrado de 256x256px
, pero nuestra ventana es de 1024x768px
así que si lo pintamos sin más pues nos quedará mucha área por cubrir, ¿sacamos el photoshop? que no cunda el pánico, las matemáticas están ahí para ayudarnos, pero no nos adelantemos vamos a ir por pasos.
Primero que nada crearemos un nuevo fichero go, internal/world.go
, obviamente esto son los nombres que hemos elegido nosotros, podéis poner los nombres que mejor se adapten a vuestro caso.
Lo primero para crear un World
es poder inicializarlo, para ello crearemos su struct
y su constructora.
type World struct {
width float64
height float64
bg pixel.Picture
}
func NewWorld(w, h float64) *World {
return &World{
width: w,
height: h,
}
}
Hasta aquí no creo que necesite una mayor explicación, tenemos un struct
con las medidas de nuestro World
y un pixel.Picture
que será nuestra imagen de fondo, pero solo el trozo que hablamos.
A continuación vamos a crear un método para poder cargar nuestra imagen y que este disponible cuando queramos pintarla.
func (w *World) AddBackground(path string) error {
bg, err := loadPicture(path)
if err != nil {
return err
}
w.bg = bg
return nil
}
Lo que hemos hecho aquí es utilizar el método que creamos anteriormente para cargar nuestra imagen y añadirla en nuestro World
.
Ahora viene la parte de pintar, para ello por consenso, nuestros objetos suelen disponer de un método Draw
, el cual será el encargado de gestionar como pintar el objeto en cada frame.
func (w World) Draw(t pixel.Target) {
spriteBg := pixel.NewSprite(w.bg, w.bg.Bounds())
bgBatch := pixel.NewBatch(&pixel.TrianglesData{}, w.bg)
for x := 0.0; x <= w.width; x += spriteBg.Frame().W() {
for y := 0.0; y <= w.height; y += spriteBg.Frame().H() {
pos := pixel.V(x, y)
spriteBg.Draw(bgBatch, pixel.IM.Moved(pos))
}
}
bgBatch.Draw(t)
}
En este caso, hemos creado un método Draw
que espera un pixel.Target, un Target
es algo en lo que podemos dibujar, una ventana, un canvas, etc.
Veamos por parte nuestra función y como funciona, primero lo que haremos es crear un sprite
a partir de nuestra imagen previamente cargada, un sprite es básicamente un marco dibujable de nuestra Picture
, NewSprite
como parámetros espera primeramente una Picture
y luego sus medidas, en este caso lo tenemos todo.
A continuación vamos a crear un Batch
, un Batch
es un Target
donde podemos pintar de manera más eficiente varios objetos de la misma Picture
.
Ya sólo nos queda la parte de repetir nuestra imagen en dicho Batch
, para ello simplemente tenemos que pensar un poquito, tenemos una imagen de 256x256px
que se tiene que repetir a lo largo de 1024x768
, tenemos que tener en cuenta que todo funciona en un eje de coordenadas, x
e y
, así que sabiendo eso, podemos decir que partiendo del eje (0,0) y mientras la x
sea menor o igual que 1024
y la y
menor o igual que 768
, debemos ir incrementando la posición en 256
, ¿fácil no?
El paquete pixel
además nos ofrece un método para crear vectores, pixel.V
donde sólo tenemos que indicar la x
y la y
.
Una vez tenemos la localización de cada uno de las piezas de nuestro background, lo que haremos será utilizar el método Draw
de nuestro sprite
, para indicarle que queremos pintar dentro del Batch
en la posición calculada.
Finalmente una vez acabado el bucle, le diremos al Batch
donde tiene que pintar, en este caso en nuestro Target
, que será la ventana como veremos a continuación.
Creando y pintando un mundo
Ahora debemos cambiar nuestro cmd/main.go
un poco para pintar dicho mundo y ver que todo ha funcionado como esperábamos.
Así pues dentro de la función run()
añadiremos lo siguiente:
func run() {
...
world := spacegame.NewWorld(windowWidth, windowHeight)
if err := world.AddBackground("resources/background.png"); err != nil {
log.Fatal(err)
}
world.Draw(win)
for !win.Closed() {
win.Update()
}
}
Si todo ha ido bien deberías tener algo como esto:
¿Chulo eh?, como podéis suponer por el fondo vamos a crear un juego de navecitas, ¿siguen de moda no?
Conclusión
Lo sé, lo sé te has quedado con ganas de más, pero sino nos hubiera quedado un artículo demasiado denso, y pienso que es mejor ir asimilando conceptos y conocimientos poco a poco, en el siguiente artículo veremos ¡cómo incluir una nave y cómo moverla!
Podéis ir siguiendo todos los avances (con spoilers incluidos) en nuestro repositorio del juego, https://github.com/friendsofgo/spacegame
Y recordad que cualquier duda podéis dejarla en los comentarios o en nuestro twitter @FriendsofGoTech