Refaktoriserat kartan till en solid 3D-Array [][][]Entity och adderat dubbla views (TopDown & SideView) var spelaren laddar sina Animation Frames

This commit is contained in:
2026-04-26 00:24:28 +02:00
parent b250629e34
commit feabe7366c
135 changed files with 316 additions and 153 deletions

View File

@@ -1,21 +1,24 @@
package scenes
import (
"image"
"image/color"
_ "image/jpeg"
_ "image/png"
"log"
"mountain/internal/entities"
"github.com/go-gl/mathgl/mgl64"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
const (
Gravity = 0.5
JumpForce = -10.0
MoveSpeed = 4.0
FloorHeight = 500.0
Gravity = 0.5
JumpForce = -8.0
MoveSpeed = 4.0
ViewSize = 380 // 800 width, we have 2 viewports (380 each with 20 padding)
)
type Player struct {
@@ -23,164 +26,237 @@ type Player struct {
Vel mgl64.Vec3 // Hastighet
Width, Height float64 // För (AABB)
IsGrounded bool
Sprite *ebiten.Image
// Sprites
SpriteRun *ebiten.Image
SpriteIdle *ebiten.Image
SpriteTop *ebiten.Image
AnimationFrame int
AnimationCounter int
FacingRight bool
}
type PlayScene struct {
player Player
groundImg *ebiten.Image
platforms []Platform
isGrounded bool
world *entities.World
player *Player
leftView *ebiten.Image
rightView *ebiten.Image
}
func NewPlayScene() *PlayScene {
pImg, _, err := ebitenutil.NewImageFromFile("assets/images/player.jpg")
w := entities.NewWorld(10, 10, 10)
tile12, _, _ := ebitenutil.NewImageFromFile("assets/images/1 Tiles/Tile_12.png")
tile02, _, _ := ebitenutil.NewImageFromFile("assets/images/1 Tiles/Tile_02.png")
tile31, _, _ := ebitenutil.NewImageFromFile("assets/images/1 Tiles/Tile_31.png")
for x := 0; x < 10; x++ {
for z := 0; z < 10; z++ {
// Surface grass
w.SetEntityAt(x, 9, z, &entities.Tile{
SideImg: tile02,
TopImg: tile31,
})
// Sub dirt
w.SetEntityAt(x, 8, z, &entities.Tile{
SideImg: tile12,
TopImg: tile12,
})
}
}
pRun, _, err := ebitenutil.NewImageFromFile("assets/images/Warrior_1/Run.png")
if err != nil {
log.Println("Kunde inte ladda spelar-sprite:", err)
pImg = ebiten.NewImage(32, 32)
log.Println("Run sprite error:", err)
pRun = ebiten.NewImage(96, 96)
}
pIdle, _, _ := ebitenutil.NewImageFromFile("assets/images/Warrior_1/Idle.png")
if pIdle == nil {
pIdle = pRun
}
gImg, _, err := ebitenutil.NewImageFromFile("assets/images/ground.jpg")
if err != nil {
log.Println("Kunde inte ladda mark-sprite:", err)
gImg = ebiten.NewImage(800, 100)
}
pTop := ebiten.NewImage(32, 32)
pTop.Fill(color.RGBA{255, 255, 0, 255})
wallImg, _, _ := ebitenutil.NewImageFromFile("assets/images/wall.png")
stairImg, _, _ := ebitenutil.NewImageFromFile("assets/images/stairs.png")
w, h := pImg.Bounds().Dx(), pImg.Bounds().Dy()
// Skapa en enkel karta med plattformar för att hoppa på
var mapPlatforms []Platform
if wallImg != nil {
pW := float64(wallImg.Bounds().Dx())
pH := float64(wallImg.Bounds().Dy())
mapPlatforms = append(mapPlatforms, Platform{
Pos: mgl64.Vec3{300.0, 400.0, 0.0},
Width: pW,
Height: pH,
Sprite: wallImg,
Scale: 0.2, // Skala ner väggen eftersom dina bildfiler (125k/325k) förmodligen är stora
})
mapPlatforms = append(mapPlatforms, Platform{
Pos: mgl64.Vec3{450.0, 300.0, 0.0},
Width: pW,
Height: pH,
Sprite: wallImg,
Scale: 0.2,
})
}
if stairImg != nil {
sW := float64(stairImg.Bounds().Dx())
sH := float64(stairImg.Bounds().Dy())
mapPlatforms = append(mapPlatforms, Platform{
Pos: mgl64.Vec3{150.0, 480.0, 0.0},
Width: sW,
Height: sH,
Sprite: stairImg,
Scale: 0.5,
})
player := &Player{
Pos: mgl64.Vec3{4.0 * entities.TileSize, 7.0 * entities.TileSize, 4.0 * entities.TileSize},
Width: 20,
Height: 40,
SpriteRun: pRun,
SpriteIdle: pIdle,
SpriteTop: pTop,
FacingRight: true,
}
return &PlayScene{
player: Player{
Pos: mgl64.Vec3{400.0, 100.0, 0.0},
Vel: mgl64.Vec3{0, 0, 0},
Width: float64(w),
Height: float64(h),
Sprite: pImg,
},
groundImg: gImg,
platforms: mapPlatforms,
world: w,
player: player,
leftView: ebiten.NewImage(ViewSize, ViewSize),
rightView: ebiten.NewImage(ViewSize, ViewSize),
}
}
func (s *PlayScene) Update() error {
// === Input (X axis) ===
s.player.Vel[0] = 0 // Återställ rörelse varje frame
p := s.player
p.Vel[0] = 0
p.Vel[2] = 0
isMoving := false
if ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA) {
s.player.Vel[0] = -MoveSpeed
p.Vel[0] = -MoveSpeed
p.FacingRight = false
isMoving = true
}
if ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD) {
s.player.Vel[0] = MoveSpeed
p.Vel[0] = MoveSpeed
p.FacingRight = true
isMoving = true
}
if ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyW) {
p.Vel[2] = -MoveSpeed
isMoving = true
}
if ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS) {
p.Vel[2] = MoveSpeed
isMoving = true
}
// === Hoppa (Y axis) ===
if (ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeySpace)) && s.isGrounded {
s.player.Vel[1] = JumpForce
s.isGrounded = false
if ebiten.IsKeyPressed(ebiten.KeySpace) && p.IsGrounded {
p.Vel[1] = JumpForce
p.IsGrounded = false
}
// === Fysik / Euler integrering ===
// 1. Applicera gravitation
if !s.isGrounded {
s.player.Vel[1] += Gravity
if !p.IsGrounded {
p.Vel[1] += Gravity
}
// 2. Planera Y förflyttning
nextY := s.player.Pos[1] + s.player.Vel[1]
s.isGrounded = false
nextX := p.Pos[0] + p.Vel[0]
nextY := p.Pos[1] + p.Vel[1]
nextZ := p.Pos[2] + p.Vel[2]
// Kolla om vi landar på en plattform (endast när vi faller)
if s.player.Vel[1] >= 0 {
for _, p := range s.platforms {
if p.CheckCollisionTop(&s.player, nextY) {
nextY = p.Pos[1] - s.player.Height
s.player.Vel[1] = 0
s.isGrounded = true
break
}
gridY := int((nextY + p.Height) / entities.TileSize)
gridX := int((nextX + p.Width/2) / entities.TileSize)
gridZ := int((nextZ + p.Width/2) / entities.TileSize)
entY := s.world.GetEntityAt(gridX, gridY, gridZ)
if entY != nil && entY.IsBlocking() && p.Vel[1] >= 0 {
nextY = float64(gridY)*entities.TileSize - p.Height - 0.1
p.Vel[1] = 0
p.IsGrounded = true
} else {
p.IsGrounded = false
}
p.Pos[0] = nextX
p.Pos[1] = nextY
p.Pos[2] = nextZ
if isMoving {
p.AnimationCounter++
if p.AnimationCounter > 5 {
p.AnimationFrame = (p.AnimationFrame + 1) % 6
p.AnimationCounter = 0
}
} else {
p.AnimationFrame = 0
}
// === "Kollision" med golvet (väldigt simpel 2D-kollision för att simulera ytan på the floor på 2D-skärmen) ===
if nextY+s.player.Height >= FloorHeight {
nextY = FloorHeight - s.player.Height
s.player.Vel[1] = 0
s.isGrounded = true
}
s.player.Pos[0] += s.player.Vel[0]
s.player.Pos[1] = nextY
return nil
}
func (s *PlayScene) Draw(screen *ebiten.Image) {
// Gör bakgrunden ljusblå (Sky blue)
screen.Fill(color.RGBA{135, 206, 235, 255})
screen.Fill(color.RGBA{20, 20, 20, 255})
// === 3D -> 2D Rendering Pipeline (Side View mode) ===
s.leftView.Fill(color.RGBA{135, 206, 235, 255})
s.rightView.Fill(color.RGBA{50, 150, 50, 255})
// Rita ut marken över hela skärmens bredd (Tiling)
if s.groundImg != nil {
gWidth := float64(s.groundImg.Bounds().Dx())
if gWidth == 0 {
gWidth = 32 // Undvik division med noll ifall bilden är tom
}
for x := 0.0; x < 800.0; x += gWidth {
gOp := &ebiten.DrawImageOptions{}
gOp.GeoM.Translate(x, FloorHeight)
screen.DrawImage(s.groundImg, gOp)
playerGridZ := int((s.player.Pos[2] + s.player.Width/2) / entities.TileSize)
playerGridY := int((s.player.Pos[1] + s.player.Height/2) / entities.TileSize)
camX := s.player.Pos[0] - float64(ViewSize)/2.0
camY := s.player.Pos[1] - float64(ViewSize)/2.0
camZ := s.player.Pos[2] - float64(ViewSize)/2.0
// Vänster vy: Sido-vy (renderar alla X och Y objekt vid spelarens Z)
for x := 0; x < s.world.Width; x++ {
for y := 0; y < s.world.Height; y++ {
ent := s.world.GetEntityAt(x, y, playerGridZ)
if ent != nil {
px := float64(x)*entities.TileSize - camX
py := float64(y)*entities.TileSize - camY
ent.DrawSide(s.leftView, px, py)
}
}
}
// Lägg ut karta/plattformar
for _, p := range s.platforms {
platOp := &ebiten.DrawImageOptions{}
platOp.GeoM.Scale(p.Scale, p.Scale)
platOp.GeoM.Translate(p.Pos[0], p.Pos[1])
screen.DrawImage(p.Sprite, platOp)
// Höger vy: Ovan-vy (renderar alla X och Z objekt vid underliggande rad)
viewY := playerGridY
if s.player.IsGrounded {
viewY += 1
}
// Rita Spelaren baserat på dess 3D-kordinater översatta till (X, Y) på 2D-skärmen (vi skippar Z i denna view)
pOp := &ebiten.DrawImageOptions{}
pOp.GeoM.Translate(s.player.Pos[0], s.player.Pos[1])
screen.DrawImage(s.player.Sprite, pOp)
for x := 0; x < s.world.Width; x++ {
for z := 0; z < s.world.Depth; z++ {
ent := s.world.GetEntityAt(x, viewY, z)
if ent != nil {
px := float64(x)*entities.TileSize - camX
pz := float64(z)*entities.TileSize - camZ
ent.DrawTop(s.rightView, px, pz)
}
}
}
ebitenutil.DebugPrint(screen, "Spelar Scene: SIDOVY. Styr med PILAR och Space.")
s.drawPlayerSide(s.leftView, camX, camY)
s.drawPlayerTop(s.rightView, camX, camZ)
opL := &ebiten.DrawImageOptions{}
opL.GeoM.Translate(10, 100)
screen.DrawImage(s.leftView, opL)
opR := &ebiten.DrawImageOptions{}
opR.GeoM.Translate(410, 100)
screen.DrawImage(s.rightView, opR)
ebitenutil.DebugPrint(screen, "VÄNSTER (Sido vy) HÖGER (Top vy: Slice av din Hojd)")
}
func (s *PlayScene) drawPlayerSide(screen *ebiten.Image, camX, camY float64) {
p := s.player
sx := p.AnimationFrame * 96
srcRect := image.Rect(sx, 0, sx+96, 96)
var activeSprite *ebiten.Image
if p.Vel[0] != 0 || p.Vel[2] != 0 { // Is moving X or Z
activeSprite = p.SpriteRun
} else {
activeSprite = p.SpriteIdle
}
subImg := activeSprite.SubImage(srcRect).(*ebiten.Image)
op := &ebiten.DrawImageOptions{}
if !p.FacingRight {
op.GeoM.Scale(-1, 1)
op.GeoM.Translate(96, 0)
}
drawX := p.Pos[0] - camX - 38
drawY := p.Pos[1] - camY - 56
op.GeoM.Translate(drawX, drawY)
screen.DrawImage(subImg, op)
}
func (s *PlayScene) drawPlayerTop(screen *ebiten.Image, camX, camZ float64) {
p := s.player
op := &ebiten.DrawImageOptions{}
drawX := p.Pos[0] - camX
drawZ := p.Pos[2] - camZ
op.GeoM.Translate(drawX, drawZ)
screen.DrawImage(p.SpriteTop, op)
}