├── .gitignore ├── Makefile ├── README.md ├── chartable.png ├── game.go ├── gopherrun.go ├── jump07.mp3 ├── spritetable.png └── tools ├── chartable └── chartable.go └── spritetable └── spritetable.go /.gitignore: -------------------------------------------------------------------------------- 1 | gopherrun 2 | gopherrun.exe 3 | tmp/ 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go run *.go 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gopher Run! 2 | 3 | Sample game using SDL2 4 | -------------------------------------------------------------------------------- /chartable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koron/gopherrun/b60737484c7ad2f03ddbeb1c919c1c299ba14722/chartable.png -------------------------------------------------------------------------------- /game.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/veandco/go-sdl2/sdl" 7 | mix "github.com/veandco/go-sdl2/sdl_mixer" 8 | "golang.org/x/image/math/fixed" 9 | ) 10 | 11 | var ( 12 | // scw is screen character width 13 | scw = (screenWidth+cellWidth-1)/cellWidth + 1 14 | 15 | // sch is screen character height 16 | sch = (screenHeight+cellHeight-1)/cellHeight + 1 17 | 18 | // maxBgOffx is max value for bgOffX 19 | maxBgOffx = fixed.I(16) 20 | 21 | // gravityPower 22 | gravityPower = fixed.I(3) / 2 23 | 24 | maxSpeedY = fixed.I(16) / 2 25 | 26 | // risingPower 27 | risingPower = fixed.I(11) / 2 28 | 29 | risingInitN = 10 30 | 31 | gopherX = fixed.I(320) / 5 32 | 33 | initSpeedX = fixed.I(1) / 3 34 | 35 | maxSpeedX = fixed.I(6) / 2 36 | 37 | accelX = fixed.I(1) / 40 38 | 39 | gopherInitY = fixed.I(8 * 16) 40 | 41 | walkPattern = []int{3, 4, 5} 42 | ) 43 | 44 | type Game struct { 45 | win *sdl.Window 46 | ren *sdl.Renderer 47 | ch1 *sdl.Texture 48 | ch2 *sdl.Texture 49 | se1 *mix.Music 50 | 51 | running bool 52 | frameNum uint64 53 | pressedA bool 54 | releasedA bool 55 | pressedB bool 56 | 57 | bgMap []uint8 58 | bgOffX fixed.Int26_6 59 | bgOffY fixed.Int26_6 60 | 61 | spPatterns []SpritePattern 62 | sprites []Sprite 63 | 64 | mode Mode 65 | gopherY fixed.Int26_6 66 | speedX fixed.Int26_6 67 | speedY fixed.Int26_6 68 | floating bool 69 | risingN int 70 | 71 | animeIndex int 72 | animeFrame int 73 | 74 | rand *rand.Rand 75 | groundHeight int 76 | groundHole bool 77 | groundCont int 78 | } 79 | 80 | type Sprite struct { 81 | id int 82 | x int32 83 | y int32 84 | } 85 | 86 | type SpritePattern struct { 87 | x int32 88 | y int32 89 | w int32 90 | h int32 91 | } 92 | 93 | type Mode int 94 | 95 | const ( 96 | title Mode = iota 97 | playing 98 | gameover 99 | ) 100 | 101 | // Init initialize all game status. 102 | func (g *Game) Init() error { 103 | g.bgMap = make([]uint8, scw*sch) 104 | g.spPatterns = []SpritePattern{ 105 | SpritePattern{x: 0, y: 0, w: 16, h: 32}, 106 | SpritePattern{x: 16, y: 0, w: 16, h: 32}, 107 | SpritePattern{x: 32, y: 0, w: 16, h: 32}, 108 | SpritePattern{x: 48, y: 0, w: 16, h: 32}, 109 | SpritePattern{x: 64, y: 0, w: 16, h: 32}, 110 | SpritePattern{x: 80, y: 0, w: 16, h: 32}, 111 | SpritePattern{x: 96, y: 0, w: 16, h: 32}, 112 | } 113 | g.sprites = []Sprite{ 114 | Sprite{id: 0, x: int32(gopherX.Floor()), y: 0}, 115 | } 116 | g.gotoTitle() 117 | return nil 118 | } 119 | 120 | func (g *Game) gotoTitle() { 121 | for x := 0; x < scw; x++ { 122 | for y := 0; y < 10; y++ { 123 | g.bgMap[x*sch+y] = 0x00 124 | } 125 | for y := 10; y < sch; y++ { 126 | g.bgMap[x*sch+y] = 0x10 127 | } 128 | } 129 | g.bgOffX = 0 130 | g.mode = title 131 | g.gopherY = gopherInitY 132 | g.running = true 133 | g.speedX = initSpeedX 134 | g.speedY = 0 135 | g.floating = false 136 | g.risingN = 0 137 | g.animeIndex = 0 138 | g.animeFrame = 0 139 | g.rand = rand.New(rand.NewSource(114514)) 140 | g.groundHeight = 10 141 | g.groundHole = false 142 | g.groundCont = 5 143 | } 144 | 145 | func (g *Game) Run() error { 146 | for g.running { 147 | g.initNewFrame() 148 | for ev := sdl.PollEvent(); ev != nil; ev = sdl.PollEvent() { 149 | g.procEvent(ev) 150 | } 151 | g.update() 152 | if err := g.render(); err != nil { 153 | return err 154 | } 155 | g.ren.Present() 156 | } 157 | return nil 158 | } 159 | 160 | func (g *Game) drawBG() error { 161 | i := 0 162 | src := sdl.Rect{0, 0, int32(cellWidth), int32(cellHeight)} 163 | dst := sdl.Rect{0, 0, int32(cellWidth), int32(cellHeight)} 164 | for x := 0; x < scw; x++ { 165 | dst.X = int32(x*cellWidth - g.bgOffX.Floor()) 166 | for y := 0; y < sch; y++ { 167 | n := int(g.bgMap[i]) 168 | dst.Y = int32(y*cellHeight - g.bgOffY.Floor()) 169 | src.X = int32((n % 16) * cellWidth) 170 | src.Y = int32((n / 16) * cellWidth) 171 | if err := g.ren.Copy(g.ch1, &src, &dst); err != nil { 172 | return err 173 | } 174 | i++ 175 | } 176 | } 177 | return nil 178 | } 179 | 180 | // initNewFrame initialize state for a new frame. 181 | func (g *Game) initNewFrame() { 182 | g.frameNum++ 183 | g.pressedA = false 184 | g.releasedA = false 185 | g.pressedB = false 186 | } 187 | 188 | func (g *Game) procEvent(raw sdl.Event) { 189 | switch ev := raw.(type) { 190 | case *sdl.WindowEvent: 191 | if ev.Event == sdl.WINDOWEVENT_CLOSE { 192 | g.running = false 193 | } 194 | case *sdl.KeyDownEvent: 195 | if ev.Repeat != 0 { 196 | break 197 | } 198 | switch ev.Keysym.Sym { 199 | case sdl.K_SPACE, sdl.K_RETURN: 200 | g.pressedA = true 201 | case sdl.K_LSHIFT, sdl.K_RSHIFT, sdl.K_LCTRL, sdl.K_RCTRL: 202 | g.pressedB = true 203 | case sdl.K_ESCAPE: 204 | g.running = false 205 | } 206 | case *sdl.KeyUpEvent: 207 | switch ev.Keysym.Sym { 208 | case sdl.K_SPACE, sdl.K_RETURN: 209 | g.releasedA = true 210 | } 211 | } 212 | } 213 | 214 | func (g *Game) shiftBG() { 215 | l := len(g.bgMap) 216 | copy(g.bgMap[0:l-sch], g.bgMap[sch:]) 217 | } 218 | 219 | func (g *Game) drawSprites() error { 220 | for i := len(g.sprites) - 1; i >= 0; i-- { 221 | s := g.sprites[i] 222 | p := g.spPatterns[s.id] 223 | src := sdl.Rect{p.x, p.y, p.w, p.h} 224 | dst := sdl.Rect{s.x, s.y, p.w, p.h} 225 | if err := g.ren.Copy(g.ch2, &src, &dst); err != nil { 226 | return err 227 | } 228 | } 229 | return nil 230 | } 231 | 232 | func (g *Game) render() error { 233 | if err := g.drawBG(); err != nil { 234 | return err 235 | } 236 | if err := g.drawSprites(); err != nil { 237 | return err 238 | } 239 | return nil 240 | } 241 | 242 | func (g *Game) update() { 243 | if g.floating { 244 | if g.risingN > 0 { 245 | if g.releasedA { 246 | g.risingN = 0 247 | } else { 248 | g.risingN-- 249 | } 250 | g.speedY = -risingPower 251 | } else { 252 | g.speedY += gravityPower 253 | if g.speedY > maxSpeedY { 254 | g.speedY = maxSpeedY 255 | } 256 | } 257 | } else { 258 | if g.pressedA { 259 | g.floating = true 260 | g.risingN = risingInitN 261 | g.speedY = -risingPower 262 | g.mode = playing 263 | g.se1.Play(1) 264 | } 265 | } 266 | 267 | switch g.mode { 268 | case title: 269 | g.speedX = initSpeedX 270 | case playing: 271 | g.speedX += accelX 272 | if g.speedX > maxSpeedX { 273 | g.speedX = maxSpeedX 274 | } 275 | case gameover: 276 | g.speedX = 0 277 | } 278 | 279 | g.bgOffX += g.speedX 280 | // check to hit wall 281 | if g.speedX > 0 { 282 | y := g.gopherY.Floor() 283 | cx := ((gopherX + g.bgOffX).Floor() + cellWidth) / cellWidth 284 | cy := y / cellWidth 285 | ch := 3 286 | if y%cellHeight == 0 { 287 | ch = 2 288 | } 289 | hit := false 290 | for i := 0; i < ch; i++ { 291 | cy2 := cy + i 292 | if cy2 < 0 { 293 | continue 294 | } else if cy2 >= sch { 295 | break 296 | } 297 | if g.bgMap[cx*sch+cy+i] >= 0x10 { 298 | hit = true 299 | break 300 | } 301 | } 302 | if hit { 303 | g.speedX = 0 304 | g.bgOffX = fixed.I((cx-1)*cellWidth) - gopherX 305 | } 306 | } 307 | 308 | // scroll and prepare new area 309 | for g.bgOffX >= maxBgOffx { 310 | g.bgOffX -= maxBgOffx 311 | g.shiftBG() 312 | // insert new bgMap at right 313 | switch g.mode { 314 | case title: 315 | n := (scw - 1) * sch 316 | for y := 0; y < sch; y++ { 317 | if y < 10 { 318 | g.bgMap[n+y] = 0x00 319 | } else { 320 | g.bgMap[n+y] = 0x10 321 | } 322 | } 323 | case playing: 324 | // TODO: generate stage data 325 | n := (scw - 1) * sch 326 | for y := 0; y < sch; y++ { 327 | if !g.groundHole && y >= g.groundHeight { 328 | g.bgMap[n+y] = 0x10 329 | } else { 330 | g.bgMap[n+y] = 0x00 331 | } 332 | } 333 | g.groundCont-- 334 | if g.groundCont <= 0 { 335 | if !g.groundHole && g.rand.Float32() < 0.17 { 336 | c := int(g.rand.ExpFloat64() * 1.5) 337 | if c < 1 { 338 | c = 1 339 | } else if c > 4 { 340 | c = 4 341 | } 342 | g.groundHole = true 343 | g.groundCont = c 344 | } else { 345 | if r := g.rand.Float32(); r < 0.18 { 346 | c := int(g.rand.ExpFloat64() * 1) 347 | if c < 1 { 348 | c = 1 349 | } else if c > 4 { 350 | c = 4 351 | } 352 | g.groundHeight -= c 353 | if g.groundHeight < 4 { 354 | g.groundHeight = 4 355 | } 356 | } else if r >= 0.82 { 357 | c := int(g.rand.ExpFloat64() * 1) 358 | if c < 1 { 359 | c = 1 360 | } else if c > 4 { 361 | c = 4 362 | } 363 | g.groundHeight += c 364 | if g.groundHeight > 10 { 365 | g.groundHeight = 10 366 | } 367 | } 368 | c := int(g.rand.NormFloat64()*2 + 3) 369 | if c < 1 { 370 | c = 1 371 | } else if c > 8 { 372 | c = 8 373 | } 374 | g.groundHole = false 375 | g.groundCont = c 376 | } 377 | } 378 | } 379 | } 380 | 381 | g.gopherY += g.speedY 382 | // check to touch grand 383 | if g.speedY >= 0 { 384 | x := (gopherX + g.bgOffX).Floor() 385 | cx := x / cellWidth 386 | cy := (g.gopherY.Floor() + cellHeight*2) / cellHeight 387 | cw := 2 388 | if x%cellWidth == 0 { 389 | cw = 1 390 | } 391 | if cy >= 0 && cy < sch { 392 | touch := false 393 | for i := 0; i < cw; i++ { 394 | if g.bgMap[(cx+i)*sch+cy] >= 0x10 { 395 | touch = true 396 | break 397 | } 398 | } 399 | if touch { 400 | g.gopherY = fixed.I((cy - 2) * cellHeight) 401 | g.speedY = 0 402 | g.floating = false 403 | g.risingN = 0 404 | } else { 405 | g.floating = true 406 | } 407 | } 408 | } 409 | if g.gopherY.Floor() > screenHeight { 410 | // game over 411 | g.mode = gameover 412 | // FIXME: show game over message 413 | } 414 | 415 | if g.mode == gameover && g.pressedA { 416 | // back to title 417 | g.gotoTitle() 418 | } 419 | 420 | if g.floating { 421 | g.animeIndex = 6 422 | g.animeFrame = 0 423 | } else if g.speedX > 0 { 424 | g.animeIndex = walkPattern[g.animeFrame/10] 425 | g.animeFrame++ 426 | if g.animeFrame >= len(walkPattern)*10 { 427 | g.animeFrame = 0 428 | } 429 | } else { 430 | g.animeFrame = 0 431 | g.animeIndex = 0 432 | } 433 | 434 | g.sprites[0].y = int32(g.gopherY.Floor()) 435 | g.sprites[0].id = g.animeIndex 436 | } 437 | -------------------------------------------------------------------------------- /gopherrun.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/veandco/go-sdl2/sdl" 7 | img "github.com/veandco/go-sdl2/sdl_image" 8 | mix "github.com/veandco/go-sdl2/sdl_mixer" 9 | ) 10 | 11 | var renderFlags uint32 = sdl.RENDERER_ACCELERATED | sdl.RENDERER_PRESENTVSYNC 12 | 13 | var ( 14 | screenWidth = 320 15 | screenHeight = 180 16 | cellWidth = 16 17 | cellHeight = 16 18 | ) 19 | 20 | func loadTexture(r *sdl.Renderer, name string) (*sdl.Texture, *sdl.Surface, error) { 21 | s, err := img.Load(name) 22 | if err != nil { 23 | return nil, nil, err 24 | } 25 | t, err := r.CreateTextureFromSurface(s) 26 | if err != nil { 27 | s.Free() 28 | return nil, nil, err 29 | } 30 | return t, s, nil 31 | } 32 | 33 | // runGame setup resources and run a game. 34 | func runGame() error { 35 | sdl.Init(sdl.INIT_EVERYTHING) 36 | defer sdl.Quit() 37 | 38 | w, err := sdl.CreateWindow("Gopher Run!", sdl.WINDOWPOS_UNDEFINED, 39 | sdl.WINDOWPOS_UNDEFINED, 1280, 720, sdl.WINDOW_SHOWN) 40 | if err != nil { 41 | return err 42 | } 43 | defer w.Destroy() 44 | 45 | r, err := sdl.CreateRenderer(w, -1, renderFlags) 46 | if err != nil { 47 | return err 48 | } 49 | defer r.Destroy() 50 | r.SetLogicalSize(320, 180) 51 | 52 | // background characters 53 | t1, s1, err := loadTexture(r, "chartable.png") 54 | if err != nil { 55 | return err 56 | } 57 | defer t1.Destroy() 58 | defer s1.Free() 59 | 60 | // sprite characters 61 | t2, s2, err := loadTexture(r, "spritetable.png") 62 | if err != nil { 63 | return err 64 | } 65 | defer t2.Destroy() 66 | defer s2.Free() 67 | 68 | err = mix.OpenAudio(mix.DEFAULT_FREQUENCY, mix.DEFAULT_FORMAT, 69 | mix.DEFAULT_CHANNELS, mix.DEFAULT_CHUNKSIZE) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | m1, err := mix.LoadMUS("jump07.mp3") 75 | if err != nil { 76 | return err 77 | } 78 | defer m1.Free() 79 | 80 | // FIXME: setup more resources 81 | 82 | g := &Game{ 83 | win: w, 84 | ren: r, 85 | ch1: t1, 86 | ch2: t2, 87 | se1: m1, 88 | } 89 | if err := g.Init(); err != nil { 90 | return err 91 | } 92 | return g.Run() 93 | } 94 | 95 | func main() { 96 | if err := runGame(); err != nil { 97 | log.Fatal(err) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /jump07.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koron/gopherrun/b60737484c7ad2f03ddbeb1c919c1c299ba14722/jump07.mp3 -------------------------------------------------------------------------------- /spritetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koron/gopherrun/b60737484c7ad2f03ddbeb1c919c1c299ba14722/spritetable.png -------------------------------------------------------------------------------- /tools/chartable/chartable.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/png" 8 | "os" 9 | 10 | "golang.org/x/image/font" 11 | "golang.org/x/image/font/inconsolata" 12 | "golang.org/x/image/math/fixed" 13 | ) 14 | 15 | // generate character table 16 | func main() { 17 | img := image.NewRGBA(image.Rect(0, 0, 256, 256)) 18 | 19 | face := inconsolata.Regular8x16 20 | m := face.Metrics() 21 | d := &font.Drawer{ 22 | Dst: img, 23 | Src: image.NewUniform(color.RGBA{255, 255, 255, 255}), 24 | Face: face, 25 | } 26 | for x := 0; x < 16; x++ { 27 | for y := 0; y < 16; y++ { 28 | s, t := x*16, y*16 29 | drawBG(img, s, t, 16, 16) 30 | d.Dot = fixed.Point26_6{ 31 | fixed.Int26_6(s << 6), 32 | fixed.Int26_6((t-1)<<6) + m.Ascent, 33 | } 34 | d.DrawString(fmt.Sprintf("%X%x", y, x)) 35 | } 36 | } 37 | 38 | f, err := os.Create("chartable.png") 39 | if err != nil { 40 | panic(err) 41 | } 42 | defer f.Close() 43 | if err := png.Encode(f, img); err != nil { 44 | panic(err) 45 | } 46 | } 47 | 48 | func drawBG(img *image.RGBA, x, y, w, h int) { 49 | fg := color.RGBA{128, 0, 0, 255} 50 | bg := color.RGBA{0, 0, 0, 255} 51 | for i := 0; i < w; i++ { 52 | img.Set(x+i, y, fg) 53 | img.Set(x+i, y+h-1, fg) 54 | } 55 | for i := 1; i < h-1; i++ { 56 | img.Set(x, y+i, fg) 57 | img.Set(x+w-1, y+i, fg) 58 | for j := 1; j < w-1; j++ { 59 | img.Set(x+j,y+i, bg) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tools/spritetable/spritetable.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/png" 8 | "os" 9 | 10 | "golang.org/x/image/font" 11 | "golang.org/x/image/font/inconsolata" 12 | "golang.org/x/image/math/fixed" 13 | ) 14 | 15 | // generate character table 16 | func main() { 17 | img := image.NewRGBA(image.Rect(0, 0, 256, 256)) 18 | 19 | face := inconsolata.Regular8x16 20 | m := face.Metrics() 21 | d := &font.Drawer{ 22 | Dst: img, 23 | Src: image.NewUniform(color.RGBA{255, 255, 255, 255}), 24 | Face: face, 25 | } 26 | for x := 0; x < 16; x++ { 27 | for y := 0; y < 16; y++ { 28 | s, t := x*16, y*16 29 | z := (y%3 + x) % 3 30 | drawBG(img, s, t, 16, 16, z) 31 | d.Dot = fixed.Point26_6{ 32 | fixed.Int26_6(s << 6), 33 | fixed.Int26_6((t-1)<<6) + m.Ascent, 34 | } 35 | d.DrawString(fmt.Sprintf("%X%x", y, x)) 36 | } 37 | } 38 | 39 | f, err := os.Create("spritetable.png") 40 | if err != nil { 41 | panic(err) 42 | } 43 | defer f.Close() 44 | if err := png.Encode(f, img); err != nil { 45 | panic(err) 46 | } 47 | } 48 | 49 | var bgcolors = []color.RGBA{ 50 | {128, 0, 0, 255}, 51 | {0, 128, 0, 255}, 52 | {0, 0, 128, 255}, 53 | } 54 | 55 | func drawBG(img *image.RGBA, x, y, w, h, z int) { 56 | c := bgcolors[z] 57 | for i := 2; i < w-2; i++ { 58 | for j := 2; j < h-2; j++ { 59 | img.Set(x+i, y+j, c) 60 | } 61 | } 62 | } 63 | --------------------------------------------------------------------------------