diff --git a/collision/types.go b/collision/types.go new file mode 100644 index 0000000..f682d69 --- /dev/null +++ b/collision/types.go @@ -0,0 +1,87 @@ +package collision + +type Point struct { + X float64 + Y float64 +} + +type Polygon struct { + Points []Point +} + +type ObjectType int + +const ( + Hero ObjectType = 1 << iota + Zombie +) + +type Collidable interface { + CollisionShape() Polygon + HandleCollisionEvent(other Collidable) + CollisionObjectType() ObjectType +} + +// Helper function to calculate the dot product of two points +func dot(p1, p2 Point) float64 { + return p1.X*p2.X + p1.Y*p2.Y +} + +// Helper function to get the normal of the edge between two points +func edgeNormal(p1, p2 Point) Point { + return Point{X: p2.Y - p1.Y, Y: p1.X - p2.X} +} + +// Project a polygon onto an axis and return the min and max projections +func projectPolygon(axis Point, polygon Polygon) (float64, float64) { + min := dot(axis, polygon.Points[0]) + max := min + for _, p := range polygon.Points[1:] { + projection := dot(axis, p) + if projection < min { + min = projection + } + if projection > max { + max = projection + } + } + return min, max +} + +// Check if two projections on an axis overlap +func overlap(minA, maxA, minB, maxB float64) bool { + if minA > maxB || minB > maxA { + return false + } + return true +} + +// Overlaps checks if two polygons overlap using the Separating Axis Theorem +func (p Polygon) Overlaps(p2 Polygon) bool { + if len(p.Points) == 0 || len(p2.Points) == 0 { + return false + } + for i := 0; i < len(p.Points); i++ { + next := (i + 1) % len(p.Points) + axis := edgeNormal(p.Points[i], p.Points[next]) + minA, maxA := projectPolygon(axis, p) + minB, maxB := projectPolygon(axis, p2) + + if !overlap(minA, maxA, minB, maxB) { + return false + } + } + + for i := 0; i < len(p2.Points); i++ { + next := (i + 1) % len(p2.Points) + axis := edgeNormal(p2.Points[i], p2.Points[next]) + minA, maxA := projectPolygon(axis, p) + minB, maxB := projectPolygon(axis, p2) + + if !overlap(minA, maxA, minB, maxB) { + return false + } + } + + return true +} diff --git a/collision/world.go b/collision/world.go new file mode 100644 index 0000000..4a56698 --- /dev/null +++ b/collision/world.go @@ -0,0 +1,32 @@ +package collision + +type World struct { + Entities []Collidable +} + +func (w *World) AddEntity(e Collidable) { + w.Entities = append(w.Entities, e) +} + +func (w *World) RemoveEntity(e Collidable) { + for i, entity := range w.Entities { + if entity == e { + w.Entities = append(w.Entities[:i], w.Entities[i+1:]...) + } + } +} + +func (w *World) NotifyAboutCollisions() { + for i, entity := range w.Entities { + for _, other := range w.Entities[i+1:] { + if entity.CollisionShape().Overlaps(other.CollisionShape()) { + entity.HandleCollisionEvent(other) + other.HandleCollisionEvent(entity) + } + } + } +} + +func NewWorld() *World { + return &World{} +} diff --git a/hero/hero.go b/hero/hero.go index d411f19..f37e5b8 100644 --- a/hero/hero.go +++ b/hero/hero.go @@ -3,6 +3,7 @@ package hero import ( "bytes" "github.com/hajimehoshi/ebiten/v2" + "gitlab.com/kbr4/9heroja/collision" "gitlab.com/kbr4/9heroja/configuration" "gitlab.com/kbr4/9heroja/resources" "image" @@ -23,9 +24,11 @@ type Hero struct { direction configuration.Direction IsWalking bool Health int // Health as a percentage (0-100) + X float64 + Y float64 } -type spritePosition struct { +type spriteFramePosition struct { x int y int } @@ -34,8 +37,8 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { // set movement positions to be hashmap of sprite positions for each direction - movementPositions := map[configuration.Direction][]spritePosition{} - movementPositions[configuration.North] = []spritePosition{ + movementPositions := map[configuration.Direction][]spriteFramePosition{} + movementPositions[configuration.North] = []spriteFramePosition{ {0, 166}, {33, 166}, {66, 166}, @@ -45,7 +48,7 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { {66, 166}, {33, 166}, } - movementPositions[configuration.NorthEast] = []spritePosition{ + movementPositions[configuration.NorthEast] = []spriteFramePosition{ {168, 0}, {201, 0}, {0, 33}, @@ -56,7 +59,7 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { {201, 0}, } - movementPositions[configuration.East] = []spritePosition{ + movementPositions[configuration.East] = []spriteFramePosition{ {132, 99}, {165, 99}, {198, 99}, @@ -67,7 +70,7 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { {165, 99}, } - movementPositions[configuration.SouthEast] = []spritePosition{ + movementPositions[configuration.SouthEast] = []spriteFramePosition{ {33, 66}, {66, 66}, {99, 66}, @@ -78,7 +81,7 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { {66, 66}, } - movementPositions[configuration.South] = []spritePosition{ + movementPositions[configuration.South] = []spriteFramePosition{ {66, 132}, {0, 99}, {33, 99}, @@ -89,7 +92,7 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { {0, 99}, } - movementPositions[configuration.SouthWest] = []spritePosition{ + movementPositions[configuration.SouthWest] = []spriteFramePosition{ {99, 33}, {33, 0}, {66, 0}, @@ -100,7 +103,7 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { {33, 0}, } - movementPositions[configuration.West] = []spritePosition{ + movementPositions[configuration.West] = []spriteFramePosition{ {198, 66}, {99, 132}, {132, 132}, @@ -111,7 +114,7 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { {99, 132}, } - movementPositions[configuration.NorthWest] = []spritePosition{ + movementPositions[configuration.NorthWest] = []spriteFramePosition{ {0, 0}, {132, 33}, {165, 33}, @@ -129,7 +132,9 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { // ground op.GeoM.Reset() - op.GeoM.Translate(float64(screenWidth/2-16), float64(screenHeight/2-16)) + h.X = float64(screenWidth/2 - 16) + h.Y = float64(screenHeight/2 - 16) + op.GeoM.Translate(h.X, h.Y) //op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)), // float64((ny-1)*tileSize-floorMod(g.cameraY, tileSize))) @@ -138,8 +143,8 @@ func (h *Hero) DrawHero(screen *ebiten.Image) { // Drawing the dynamic health bar below the hero maxHealthBarWidth := resources.HeroTileSize // Maximum width of the health bar (same as the character width) healthBarHeight := 5 // Height of the health bar - healthBarX := float64(screenWidth/2 - 16) // Align with the hero - healthBarY := float64(screenHeight/2 + 20) // Position below the hero + healthBarX := float64(h.X) // Align with the hero + healthBarY := float64(h.Y + 37) // Position below the hero // Calculate the current width of the health bar based on the hero's health currentHealthBarWidth := int(float64(maxHealthBarWidth) * (float64(h.Health) / 100.0)) @@ -180,7 +185,7 @@ func NewHero() *Hero { hero := &Hero{ step: 0, IsWalking: false, - Health: 33, + Health: 100, } go func() { @@ -217,3 +222,29 @@ func (h *Hero) Stop() { walkTicker.Stop() h.IsWalking = false } + +func (h *Hero) CollisionShape() collision.Polygon { + return collision.Polygon{ + Points: []collision.Point{ + {X: h.X, Y: h.Y}, + {X: h.X + 33, Y: h.Y}, + {X: h.X + 33, Y: h.Y + 33}, + {X: h.X, Y: h.Y + 33}, + }, + } +} + +func (h *Hero) CollisionObjectType() collision.ObjectType { + return collision.Hero +} + +func (h *Hero) HandleCollisionEvent(other collision.Collidable) { + coll := other.CollisionObjectType() + switch coll { + case collision.Zombie: + h.Health -= 25 + if h.Health <= 0 { + h.Health = 10 + } + } +} diff --git a/main.go b/main.go index 2d74b73..c537853 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" + "gitlab.com/kbr4/9heroja/collision" "gitlab.com/kbr4/9heroja/configuration" "gitlab.com/kbr4/9heroja/hero" "gitlab.com/kbr4/9heroja/input" @@ -51,6 +52,7 @@ type Game struct { hero *hero.Hero terrain *terrain.Terrain zombies []*zombie.Zombie + world *collision.World } func (g *Game) Update() error { @@ -63,17 +65,18 @@ func (g *Game) Update() error { g.terrain.Move() g.hero.Walk() } + g.world.NotifyAboutCollisions() return nil } func (g *Game) Draw(screen *ebiten.Image) { g.terrain.DrawTerrain(screen) - g.hero.DrawHero(screen) for _, z := range g.zombies { z.OffsetX = g.terrain.PositionX z.OffsetY = g.terrain.PositionY z.DrawZombie(screen) } + g.hero.DrawHero(screen) } func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { @@ -86,10 +89,12 @@ var GameInstance *Game func init() { GameInstance = &Game{} + GameInstance.world = collision.NewWorld() GameInstance.control = &input.Keyboard{} GameInstance.hero = hero.NewHero() GameInstance.zombies = []*zombie.Zombie{zombie.NewZombie(), zombie.NewZombie(), zombie.NewZombie(), zombie.NewZombie(), zombie.NewZombie()} + GameInstance.world.AddEntity(GameInstance.hero) // put zombies in random places on the screen but not too close to the hero or each other for _, z := range GameInstance.zombies { z.X = float64(configuration.Random(50, screenWidth-50)) @@ -99,6 +104,7 @@ func init() { z.Y = float64(configuration.Random(0, screenHeight)) } z.Walk() + GameInstance.world.AddEntity(z) } GameInstance.terrain = terrain.NewTerrain() diff --git a/zombie/zombie.go b/zombie/zombie.go index 4c17d5a..3d71515 100644 --- a/zombie/zombie.go +++ b/zombie/zombie.go @@ -3,6 +3,7 @@ package zombie import ( "bytes" "github.com/hajimehoshi/ebiten/v2" + "gitlab.com/kbr4/9heroja/collision" "gitlab.com/kbr4/9heroja/configuration" "gitlab.com/kbr4/9heroja/resources" "image" @@ -25,6 +26,7 @@ type Zombie struct { Y float64 OffsetX float64 OffsetY float64 + IsDead bool } type spritePosition struct { @@ -57,7 +59,10 @@ func (z *Zombie) DrawZombie(screen *ebiten.Image) { op.GeoM.Reset() op.GeoM.Translate(z.X+z.OffsetX, z.Y+z.OffsetY) op.GeoM.Scale(1, 1) - + // Apply red tint if the zombie is dead + if z.IsDead { + op.ColorScale.Scale(1, 0, 0, 1) // Scale the red channel up, green and blue down. + } screen.DrawImage(zombieImage.SubImage(image.Rect(p.x+1, p.y, p.x+resources.ZombieTileSize, p.y+resources.HeroTileSize)).(*ebiten.Image), op) } @@ -73,23 +78,26 @@ func init() { } func NewZombie() *Zombie { - hero := &Zombie{ + zombie := &Zombie{ step: 0, IsWalking: false, + IsDead: false, } go func() { for { select { case <-walkTicker.C: - hero.step++ - if hero.step > 3 { - hero.step = 0 + if zombie.IsWalking { + zombie.step++ + if zombie.step > 3 { + zombie.step = 0 + } } } } }() - return hero + return zombie } func (z *Zombie) Walk() { @@ -109,6 +117,36 @@ func (z *Zombie) ChangeDirection(d configuration.Direction) { func (z *Zombie) Stop() { z.step = 2 - walkTicker.Stop() z.IsWalking = false } + +func (z *Zombie) CollisionShape() collision.Polygon { + if !z.IsDead { + return collision.Polygon{ + Points: []collision.Point{ + {X: z.X + z.OffsetX, Y: z.Y + z.OffsetY}, + {X: z.X + z.OffsetX + 32, Y: z.Y + z.OffsetY}, + {X: z.X + z.OffsetX + 32, Y: z.Y + z.OffsetY + 32}, + {X: z.X + z.OffsetX, Y: z.Y + z.OffsetY + 32}, + }, + } + } else { + return collision.Polygon{ + Points: []collision.Point{}, + } + } +} + +func (z *Zombie) CollisionObjectType() collision.ObjectType { + return collision.Zombie +} + +func (z *Zombie) HandleCollisionEvent(other collision.Collidable) { + coll := other.CollisionObjectType() + switch coll { + case collision.Hero: + z.Stop() + z.IsDead = true + + } +}