package main import ( "image/color" "log" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) const ( screenWidth = 640 screenHeight = 480 tileSize = 32 gravity = 0.5 jumpForce = -10.0 moveSpeed = 4.0 ) type Game struct { x, y float64 vx, vy float64 onGround bool cameraX float64 level *Level } func (g *Game) Update() error { // Input g.vx = 0 if ebiten.IsKeyPressed(ebiten.KeyLeft) { g.vx = -moveSpeed } if ebiten.IsKeyPressed(ebiten.KeyRight) { g.vx = moveSpeed } if (ebiten.IsKeyPressed(ebiten.KeySpace) || ebiten.IsKeyPressed(ebiten.KeyUp)) && g.onGround { g.vy = jumpForce g.onGround = false } // Gravity g.vy += gravity // Horizontal move newX := g.x + g.vx newY := g.y // vertical tentative remains for later g.onGround = false // Resolve horizontal collision if g.vx != 0 { // Determine future position bounds left := newX right := newX + float64(tileSize) top := g.y bottom := g.y + float64(tileSize) // Loop over tiles overlapping horizontally startX := int(left / tileSize) endX := int(right / tileSize) for ty := int(top / tileSize); ty <= int(bottom/tileSize); ty++ { for tx := startX; tx <= endX; tx++ { if g.level.GetTile(tx, ty) != TileEmpty { // Collision: place on left or right side if g.vx > 0 { newX = float64(tx*tileSize) - float64(tileSize) } else { newX = float64((tx + 1) * tileSize) } } } } } // Update X after horizontal resolution g.x = newX // Vertical move newY = g.y + g.vy if g.vy != 0 { left := g.x right := g.x + float64(tileSize) // Determine tile indices for collision if g.vy > 0 { // falling // tile below Mario bottomY := newY + float64(tileSize) ty := int(bottomY / tileSize) for tx := int(left / tileSize); tx <= int(right/tileSize); tx++ { if g.level.GetTile(tx, ty) != TileEmpty { newY = float64(ty*tileSize) - float64(tileSize) g.onGround = true g.vy = 0 break } } } else { // jumping // tile above Mario ty := int(newY / tileSize) for tx := int(left / tileSize); tx <= int(right/tileSize); tx++ { if g.level.GetTile(tx, ty) != TileEmpty { newY = float64((ty + 1) * tileSize) g.vy = 0 break } } } } g.y = newY // Keep within horizontal bounds of level if g.x < 0 { g.x = 0 } // Camera follows Mario g.cameraX = g.x - float64(screenWidth)/2 if g.cameraX < 0 { g.cameraX = 0 } return nil } func (g *Game) Draw(screen *ebiten.Image) { screen.Fill(color.RGBA{107, 155, 255, 255}) // sky // Draw tiles for ty := 0; ty < g.level.Height; ty++ { for tx := 0; tx < g.level.Width; tx++ { tile := g.level.GetTile(tx, ty) if tile == TileEmpty { continue } var c color.RGBA switch tile { case TileGround: c = color.RGBA{139, 69, 19, 255} case TileBrick: c = color.RGBA{165, 42, 42, 255} case TileQuestion: c = color.RGBA{255, 215, 0, 255} } drawX := float64(tx*tileSize) - g.cameraX drawY := float64(ty * tileSize) if drawX+float64(tileSize) >= 0 && drawX <= float64(screenWidth) { ebitenutil.DrawRect(screen, drawX, drawY, float64(tileSize), float64(tileSize), c) } } } // Mario ebitenutil.DrawRect(screen, g.x-g.cameraX, g.y, float64(tileSize), float64(tileSize), color.RGBA{255, 0, 0, 255}) ebitenutil.DebugPrint(screen, "Mario Squares - Use Arrows/Space to Move") } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return screenWidth, screenHeight } func main() { level := NewLevel11() g := &Game{x: 100, y: 100, level: level} ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Mario Squares") if err := ebiten.RunGame(g); err != nil { log.Fatal(err) } }