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:
20
internal/entities/entity.go
Normal file
20
internal/entities/entity.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package entities
|
||||
|
||||
import "github.com/go-gl/mathgl/mgl64"
|
||||
|
||||
// Entity definierar alla bas-objekt i varlden
|
||||
type Entity interface {
|
||||
Pos() mgl64.Vec3
|
||||
SetPos(pos mgl64.Vec3)
|
||||
IsBlocking() bool
|
||||
IsMovable() bool
|
||||
Move(dx, dy, dz float64)
|
||||
Damage(amount int)
|
||||
Pickup() bool
|
||||
GetHealth() int
|
||||
|
||||
// Ritar ut framkallningen av spriten
|
||||
// sideView: ifall vi ritar från sidan (vänster) eller uppifrån (höger)
|
||||
DrawSide(screen interface{}, x, y float64)
|
||||
DrawTop(screen interface{}, x, y float64)
|
||||
}
|
||||
42
internal/entities/tiles.go
Normal file
42
internal/entities/tiles.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type Tile struct {
|
||||
pos mgl64.Vec3
|
||||
health int
|
||||
SideImg *ebiten.Image
|
||||
TopImg *ebiten.Image
|
||||
}
|
||||
|
||||
func (t *Tile) Pos() mgl64.Vec3 { return t.pos }
|
||||
func (t *Tile) SetPos(pos mgl64.Vec3) { t.pos = pos }
|
||||
func (t *Tile) IsBlocking() bool { return true }
|
||||
func (t *Tile) IsMovable() bool { return false }
|
||||
func (t *Tile) Move(dx, dy, dz float64) {
|
||||
// Not feasible for soil etc.
|
||||
}
|
||||
func (t *Tile) Damage(am int) { t.health -= am }
|
||||
func (t *Tile) Pickup() bool { return false }
|
||||
func (t *Tile) GetHealth() int { return t.health }
|
||||
|
||||
func (t *Tile) DrawSide(screen interface{}, x, y float64) {
|
||||
scr := screen.(*ebiten.Image)
|
||||
if t.SideImg != nil {
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(x, y)
|
||||
scr.DrawImage(t.SideImg, op)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tile) DrawTop(screen interface{}, x, y float64) {
|
||||
scr := screen.(*ebiten.Image)
|
||||
if t.TopImg != nil {
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(x, y)
|
||||
scr.DrawImage(t.TopImg, op)
|
||||
}
|
||||
}
|
||||
45
internal/entities/world.go
Normal file
45
internal/entities/world.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package entities
|
||||
|
||||
import "github.com/go-gl/mathgl/mgl64"
|
||||
|
||||
const (
|
||||
TileSize = 32.0 // Varje block ar 32x32 world units
|
||||
)
|
||||
|
||||
type World struct {
|
||||
Width, Height, Depth int
|
||||
Grid [][][]Entity
|
||||
}
|
||||
|
||||
func NewWorld(w, h, d int) *World {
|
||||
grid := make([][][]Entity, w)
|
||||
for x := 0; x < w; x++ {
|
||||
grid[x] = make([][]Entity, h)
|
||||
for y := 0; y < h; y++ {
|
||||
grid[x][y] = make([]Entity, d)
|
||||
}
|
||||
}
|
||||
return &World{Width: w, Height: h, Depth: d, Grid: grid}
|
||||
}
|
||||
|
||||
func (w *World) GetEntityAt(x, y, z int) Entity {
|
||||
if x < 0 || x >= w.Width || y < 0 || y >= w.Height || z < 0 || z >= w.Depth {
|
||||
return nil
|
||||
}
|
||||
return w.Grid[x][y][z]
|
||||
}
|
||||
|
||||
func (w *World) SetEntityAt(x, y, z int, e Entity) {
|
||||
if x < 0 || x >= w.Width || y < 0 || y >= w.Height || z < 0 || z >= w.Depth {
|
||||
return
|
||||
}
|
||||
w.Grid[x][y][z] = e
|
||||
if e != nil {
|
||||
e.SetPos(mgl64.Vec3{float64(x), float64(y), float64(z)})
|
||||
}
|
||||
}
|
||||
|
||||
// Convert absolute 3D position to Grid index
|
||||
func ToGridIndex(pos mgl64.Vec3) (int, int, int) {
|
||||
return int(pos[0]), int(pos[1]), int(pos[2])
|
||||
}
|
||||
@@ -62,4 +62,4 @@ func (g *Game) ChangeScene(s SceneType) {
|
||||
case ScenePlay:
|
||||
g.currentScene = scenes.NewPlayScene()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package scenes
|
||||
|
||||
import (
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type Platform struct {
|
||||
Pos mgl64.Vec3
|
||||
Width float64
|
||||
Height float64
|
||||
Sprite *ebiten.Image
|
||||
Scale float64
|
||||
}
|
||||
|
||||
// Kolla ifall spelaren nuddar ovansidan av plattformen (Fallande)
|
||||
func (p *Platform) CheckCollisionTop(player *Player, nextY float64) bool {
|
||||
// AABB (Axis-Aligned Bounding Box) X-kollision
|
||||
if player.Pos[0] < p.Pos[0]+(p.Width*p.Scale) && player.Pos[0]+player.Width > p.Pos[0] {
|
||||
// Kolla om spelaren är på väg ner och "korsar" toppen av plattformen
|
||||
if player.Pos[1]+player.Height <= p.Pos[1] && nextY+player.Height >= p.Pos[1] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user