diff --git a/examples/platformer/main.go b/examples/platformer/main.go new file mode 100644 index 0000000..f136b30 --- /dev/null +++ b/examples/platformer/main.go @@ -0,0 +1,168 @@ +package main + +import ( + "image/color" + "log" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/vector" + "github.com/setanarut/kamera/v2" + "github.com/setanarut/tilecollider" +) + +func main() { + // ebiten.SetTPS(10) + ebiten.SetWindowSize(512, 512) + if err := ebiten.RunGame(&Game{}); err != nil { + log.Fatal(err) + } +} + +var TileMap = [][]uint8{ + {1, 0, 1, 0, 1, 1, 0, 1}, + {1, 0, 0, 0, 0, 0, 0, 1}, + {1, 1, 0, 0, 0, 1, 0, 1}, + {0, 0, 0, 1, 0, 1, 0, 1}, + {0, 0, 0, 0, 0, 1, 0, 1}, + {1, 0, 1, 1, 1, 1, 0, 1}, + {1, 0, 0, 0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}} + +func init() { + Controller.SetPhyicsScale(2.2) + cam.LerpEnabled = true +} + +var infoText string + +func Translate(box *[4]float64, x, y float64) { + box[0] += x + box[1] += y +} + +var ( + Offset = [2]int{0, 0} + GridSize = [2]int{8, 8} + TileSize = [2]int{64, 64} + Box = [4]float64{70, 70, 24, 32} + Vel = [2]float64{0, 4} + cam = kamera.NewCamera(Box[0], Box[1], 512, 512) +) + +var Controller = NewPlayerController() +var collider = tilecollider.NewCollider(TileMap, TileSize[0], TileSize[1]) + +func (g *Game) Update() error { + if Vel[1] < 0 { + Controller.IsOnFloor = false + } + Vel = Controller.ProcessVelocity(Vel) + dx, dy := collider.Collide( + Box[0], + Box[1], + Box[2], + Box[3], + Vel[0], + Vel[1], + func(ci []tilecollider.CollisionInfo[uint8], f1, f2 float64) { + + for _, v := range ci { + if v.Normal[1] == -1 { + Controller.IsOnFloor = true + } + if v.Normal[1] == 1 { + Controller.IsJumping = false + Vel[1] = 0 + } + if v.Normal[0] == 1 || v.Normal[0] == -1 { + Vel[0] = 0 + } + } + }, + ) + Translate(&Box, dx, dy) + cam.LookAt(Box[0], Box[1]) + return nil +} + +func (g *Game) Layout(w, h int) (int, int) { + return 512, 512 +} + +type Game struct{} + +func (g *Game) Draw(s *ebiten.Image) { + + for y, row := range TileMap { + for x, value := range row { + if value != 0 { + px, py := float64(x*TileSize[0]), float64(y*TileSize[1]) + geom := &ebiten.GeoM{} + cam.ApplyCameraTransform(geom) + px, py = geom.Apply(px, py) + vector.DrawFilledRect( + s, + float32(px), + float32(py), + float32(TileSize[0]), + float32(TileSize[1]), + color.Gray{127}, + false, + ) + } + } + } + + // draw collided tiles + for _, col := range collider.Collisions { + + px, py := float64(col.TileCoords[0]*collider.TileSize[0]), float64(col.TileCoords[1]*collider.TileSize[1]) + geom := &ebiten.GeoM{} + cam.ApplyCameraTransform(geom) + px, py = geom.Apply(px, py) + + vector.DrawFilledRect( + s, + float32(px), + float32(py), + float32(collider.TileSize[0]), + float32(collider.TileSize[1]), + color.RGBA{R: 255, G: 255, B: 0, A: 30}, + false, + ) + } + + // draw player + x, y := Box[0], Box[1] + geom := &ebiten.GeoM{} + cam.ApplyCameraTransform(geom) + x, y = geom.Apply(x, y) + vector.DrawFilledRect( + s, + float32(x), + float32(y), + float32(Box[2]), + float32(Box[3]), + color.Gray{180}, + false, + ) + + ebitenutil.DebugPrint(s, infoText) +} + +func Axis() (axisX, axisY float64) { + if ebiten.IsKeyPressed(ebiten.KeyUp) { + axisY -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyDown) { + axisY += 1 + } + if ebiten.IsKeyPressed(ebiten.KeyLeft) { + axisX -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyRight) { + axisX += 1 + } + return axisX, axisY +} diff --git a/examples/platformer/player.go b/examples/platformer/player.go new file mode 100644 index 0000000..95a7f13 --- /dev/null +++ b/examples/platformer/player.go @@ -0,0 +1,261 @@ +package main + +import ( + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +const ( + minSpeed = 0.07421875 + maxSpeed = 2.5625 + maxWalkSpeed = 1.5625 + maxFallSpeed = 4.5 + maxFallSpeedCap = 4 + minSlowDownSpeed = 0.5625 + walkAcceleration = 0.037109375 + runAcceleration = 0.0556640625 + walkFriction = 0.05078125 + skidFriction = 0.1015625 + stompSpeed = 4 + stompSpeedCap = 4 + jumpSpeedNormal = -4 + jumpSpeedRun = -4 + jumpSpeedLong = -5 + longJumpGravityNormal = 0.12 + longJumpGravityRun = 0.11 + longJumpGravityLong = 0.15 + gravity = 0.43 + speedThreshold1 = 1 + speedThreshold2 = 2.3125 +) + +type PlayerController struct { + // Constants (replaced magic numbers with named constants) + MinSpeed float64 + MaxSpeed float64 + MaxWalkSpeed float64 + MaxFallSpeed float64 + MaxFallSpeedCap float64 + MinSlowDownSpeed float64 + WalkAcceleration float64 + RunAcceleration float64 + WalkFriction float64 + SkidFriction float64 + StompSpeed float64 + StompSpeedCap float64 + JumpSpeed [3]float64 + LongJumpGravity [3]float64 + Gravity float64 + SpeedThresholds [2]float64 + // states + IsFacingLeft bool + IsRunning bool + IsJumping bool + IsFalling bool + IsSkidding bool + IsCrouching bool + IsOnFloor bool + // private + minSpeedValue float64 + maxSpeedValue float64 + accel float64 + speedThresholdIndex int +} + +func NewPlayerController() *PlayerController { + + pc := &PlayerController{ + MinSpeed: minSpeed, + MaxSpeed: maxSpeed, + MaxWalkSpeed: maxWalkSpeed, + MaxFallSpeed: maxFallSpeed, + MaxFallSpeedCap: maxFallSpeedCap, + MinSlowDownSpeed: minSlowDownSpeed, + WalkAcceleration: walkAcceleration, + RunAcceleration: runAcceleration, + WalkFriction: walkFriction, + SkidFriction: skidFriction, + StompSpeed: stompSpeed, + StompSpeedCap: stompSpeedCap, + + JumpSpeed: [3]float64{jumpSpeedNormal, jumpSpeedRun, jumpSpeedLong}, + LongJumpGravity: [3]float64{longJumpGravityNormal, longJumpGravityRun, longJumpGravityLong}, + Gravity: gravity, + SpeedThresholds: [2]float64{speedThreshold1, speedThreshold2}, + IsFacingLeft: false, + IsRunning: false, + + IsJumping: false, + IsFalling: false, + IsSkidding: false, + IsCrouching: false, + IsOnFloor: false, + + speedThresholdIndex: 0, + } + + pc.minSpeedValue = pc.MinSpeed + pc.maxSpeedValue = pc.MaxSpeed + pc.accel = pc.WalkAcceleration + + return pc +} + +func (pc *PlayerController) SetPhyicsScale(s float64) { + pc.MinSpeed *= s + pc.MaxSpeed *= s + pc.MaxWalkSpeed *= s + pc.MaxFallSpeed *= s + pc.MaxFallSpeedCap *= s + pc.MinSlowDownSpeed *= s + pc.WalkAcceleration *= s + pc.RunAcceleration *= s + pc.WalkFriction *= s + pc.SkidFriction *= s + pc.StompSpeed *= s + pc.StompSpeedCap *= s + pc.JumpSpeed[0] *= s + pc.JumpSpeed[1] *= s + pc.JumpSpeed[2] *= s + pc.LongJumpGravity[0] *= s + pc.LongJumpGravity[1] *= s + pc.LongJumpGravity[2] *= s + pc.Gravity *= s + pc.SpeedThresholds[0] *= s + pc.SpeedThresholds[1] *= s +} + +func (pc *PlayerController) ProcessVelocity(vel [2]float64) [2]float64 { + inputAxisX, inputAxisY := getAxis() + + if pc.IsOnFloor { + pc.IsRunning = ebiten.IsKeyPressed(ebiten.KeyShift) + pc.IsCrouching = ebiten.IsKeyPressed(ebiten.KeyDown) + if pc.IsCrouching && inputAxisX != 0 { + pc.IsCrouching = false + inputAxisX = 0.0 + } + } + + if pc.IsOnFloor { + if inpututil.IsKeyJustPressed(ebiten.KeySpace) { + pc.IsJumping = true + speed := math.Abs(vel[0]) + pc.speedThresholdIndex = 0 + if speed >= pc.SpeedThresholds[1] { + pc.speedThresholdIndex = 2 + } else if speed >= pc.SpeedThresholds[0] { + pc.speedThresholdIndex = 1 + } + + vel[1] = pc.JumpSpeed[pc.speedThresholdIndex] + + } + } else { + gravityValue := pc.Gravity + if ebiten.IsKeyPressed(ebiten.KeySpace) && pc.IsJumping && vel[1] < 0 { + gravityValue = pc.LongJumpGravity[pc.speedThresholdIndex] + } + vel[1] += gravityValue + if vel[1] > pc.MaxFallSpeedCap { + vel[1] = pc.MaxFallSpeedCap + } + } + + // Update states + if vel[1] > 0 { + pc.IsJumping = false + pc.IsFalling = true + } else if pc.IsOnFloor { + pc.IsFalling = false + } + + if inputAxisX != 0 { + if pc.IsOnFloor { + if vel[0] != 0 { + pc.IsFacingLeft = inputAxisX < 0.0 + pc.IsSkidding = vel[0] < 0.0 != pc.IsFacingLeft + } + if pc.IsSkidding { + pc.minSpeedValue = pc.MinSlowDownSpeed + pc.maxSpeedValue = pc.MaxWalkSpeed + pc.accel = pc.SkidFriction + } else if pc.IsRunning { + pc.minSpeedValue = pc.MinSpeed + pc.maxSpeedValue = pc.MaxSpeed + pc.accel = pc.RunAcceleration + } else { + pc.minSpeedValue = pc.MinSpeed + pc.maxSpeedValue = pc.MaxWalkSpeed + pc.accel = pc.WalkAcceleration + } + } else if pc.IsRunning && math.Abs(vel[0]) > pc.MaxWalkSpeed { + pc.maxSpeedValue = pc.MaxSpeed + } else { + pc.maxSpeedValue = pc.MaxWalkSpeed + } + targetSpeed := inputAxisX * pc.maxSpeedValue + + // Manually implementing moveToward() + if vel[0] < targetSpeed { + vel[0] += pc.accel + if vel[0] > targetSpeed { + vel[0] = targetSpeed + } + } else if vel[0] > targetSpeed { + vel[0] -= pc.accel + if vel[0] < targetSpeed { + vel[0] = targetSpeed + } + } + + } else if pc.IsOnFloor && vel[0] != 0 { + if !pc.IsSkidding { + pc.accel = pc.WalkFriction + } + if inputAxisY != 0 { + pc.minSpeedValue = pc.MinSlowDownSpeed + } else { + pc.minSpeedValue = pc.MinSpeed + } + if math.Abs(vel[0]) < pc.minSpeedValue { + vel[0] = 0.0 + } else { + // Manually implementing moveToward() for deceleration + if vel[0] > 0 { + vel[0] -= pc.accel + if vel[0] < 0 { + vel[0] = 0 + } + } else { + vel[0] += pc.accel + if vel[0] > 0 { + vel[0] = 0 + } + } + } + } + if math.Abs(vel[0]) < pc.MinSlowDownSpeed { + pc.IsSkidding = false + } + + return vel +} + +func getAxis() (axisX, axisY float64) { + if ebiten.IsKeyPressed(ebiten.KeyW) { + axisY -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyS) { + axisY += 1 + } + if ebiten.IsKeyPressed(ebiten.KeyA) { + axisX -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyD) { + axisX += 1 + } + return axisX, axisY +} diff --git a/go.mod b/go.mod index b340005..adc8c0c 100644 --- a/go.mod +++ b/go.mod @@ -2,18 +2,21 @@ module github.com/setanarut/tilecollider go 1.23.2 -retract [v1.0.0, v1.4.0] // several bugs +retract [v1.0.0, v1.4.1] // several bugs require ( github.com/hajimehoshi/ebiten/v2 v2.8.5 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f ) +require github.com/setanarut/fastnoise v1.1.1 // indirect + require ( github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/purego v0.8.1 // indirect github.com/jezek/xgb v1.1.1 // indirect + github.com/setanarut/kamera/v2 v2.8.1 golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect ) diff --git a/go.sum b/go.sum index f957bcd..e8645fd 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,10 @@ github.com/hajimehoshi/ebiten/v2 v2.8.5 h1:w1/3XxjEwIo+amtQCOnCrwGzu4e6dr0ewu83J github.com/hajimehoshi/ebiten/v2 v2.8.5/go.mod h1:SXx/whkvpfsavGo6lvZykprerakl+8Uo1X8d2U5aAnA= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/setanarut/fastnoise v1.1.1 h1:cD9gUjY9GMxVab+B7AY09j2GAHMdiKzchDE5z6dQ7eE= +github.com/setanarut/fastnoise v1.1.1/go.mod h1:74vG3/RcPPcNi2M0riHJXQp9/+eTSh0rnQ/0WTEY6SU= +github.com/setanarut/kamera/v2 v2.8.1 h1:Qf2lmehCrdKBn3zwKZ8J+sa1wKwXFAq2Mgt+L8z65Z4= +github.com/setanarut/kamera/v2 v2.8.1/go.mod h1:AtkVyDeQi/uQ1VGll51HqmxLSbicPCqsq4Mxa3PKm84= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= diff --git a/tilecollider.go b/tilecollider.go index 0d5b5db..6e4067b 100644 --- a/tilecollider.go +++ b/tilecollider.go @@ -71,20 +71,13 @@ func (c *Collider[T]) Collide(rectX, rectY, rectW, rectH, moveX, moveY float64, // collideX checks for collisions along the X axis and returns the allowed X movement func (c *Collider[T]) collideX(rectX, rectY, rectW, rectH, moveX float64) float64 { - // Sadece konum ve boyut hesaplamalarında yuvarlama yap - posX := math.Round(rectX) - posY := math.Round(rectY) - width := math.Ceil(rectW) - height := math.Ceil(rectH) - // moveX'i yuvarlama! - checkLimit := max(1, int(math.Ceil(math.Abs(moveX)/float64(c.TileSize[0])))+1) - playerTop := int(math.Floor(posY / float64(c.TileSize[1]))) - playerBottom := int(math.Ceil((posY+height)/float64(c.TileSize[1]))) - 1 + playerTop := int(math.Floor(rectY / float64(c.TileSize[1]))) + playerBottom := int(math.Ceil((rectY+rectH)/float64(c.TileSize[1]))) - 1 if moveX > 0 { - startX := int(math.Floor((posX + width) / float64(c.TileSize[0]))) + startX := int(math.Floor((rectX + rectW) / float64(c.TileSize[0]))) endX := startX + checkLimit endX = min(endX, len(c.TileMap[0])) @@ -98,7 +91,7 @@ func (c *Collider[T]) collideX(rectX, rectY, rectW, rectH, moveX float64) float6 } if c.TileMap[y][x] != c.NonSolidTileID { tileLeft := float64(x * c.TileSize[0]) - collision := tileLeft - (posX + width) + collision := tileLeft - (rectX + rectW) if collision <= moveX { moveX = collision c.Collisions = append(c.Collisions, CollisionInfo[T]{ @@ -113,7 +106,7 @@ func (c *Collider[T]) collideX(rectX, rectY, rectW, rectH, moveX float64) float6 } if moveX < 0 { - endX := int(math.Floor(posX / float64(c.TileSize[0]))) + endX := int(math.Floor(rectX / float64(c.TileSize[0]))) startX := endX - checkLimit startX = max(startX, 0) @@ -127,7 +120,7 @@ func (c *Collider[T]) collideX(rectX, rectY, rectW, rectH, moveX float64) float6 } if c.TileMap[y][x] != c.NonSolidTileID { tileRight := float64((x + 1) * c.TileSize[0]) - collision := tileRight - posX + collision := tileRight - rectX if collision >= moveX { moveX = collision c.Collisions = append(c.Collisions, CollisionInfo[T]{ @@ -147,20 +140,13 @@ func (c *Collider[T]) collideX(rectX, rectY, rectW, rectH, moveX float64) float6 // collideY checks for collisions along the Y axis and returns the allowed Y movement func (c *Collider[T]) collideY(rectX, rectY, rectW, rectH, moveY float64) float64 { - // Sadece konum ve boyut hesaplamalarında yuvarlama yap - posX := math.Round(rectX) - posY := math.Round(rectY) - width := math.Ceil(rectW) - height := math.Ceil(rectH) - // moveY'yi yuvarlama! - checkLimit := max(1, int(math.Ceil(math.Abs(moveY)/float64(c.TileSize[1])))+1) - playerLeft := int(math.Floor(posX / float64(c.TileSize[0]))) - playerRight := int(math.Ceil((posX+width)/float64(c.TileSize[0]))) - 1 + playerLeft := int(math.Floor(rectX / float64(c.TileSize[0]))) + playerRight := int(math.Ceil((rectX+rectW)/float64(c.TileSize[0]))) - 1 if moveY > 0 { - startY := int(math.Floor((posY + height) / float64(c.TileSize[1]))) + startY := int(math.Floor((rectY + rectH) / float64(c.TileSize[1]))) endY := startY + checkLimit endY = min(endY, len(c.TileMap)) @@ -174,7 +160,7 @@ func (c *Collider[T]) collideY(rectX, rectY, rectW, rectH, moveY float64) float6 } if c.TileMap[y][x] != c.NonSolidTileID { tileTop := float64(y * c.TileSize[1]) - collision := tileTop - (posY + height) + collision := tileTop - (rectY + rectH) if collision <= moveY { moveY = collision c.Collisions = append(c.Collisions, CollisionInfo[T]{ @@ -189,7 +175,7 @@ func (c *Collider[T]) collideY(rectX, rectY, rectW, rectH, moveY float64) float6 } if moveY < 0 { - endY := int(math.Floor(posY / float64(c.TileSize[1]))) + endY := int(math.Floor(rectY / float64(c.TileSize[1]))) startY := endY - checkLimit startY = max(startY, 0) @@ -203,7 +189,7 @@ func (c *Collider[T]) collideY(rectX, rectY, rectW, rectH, moveY float64) float6 } if c.TileMap[y][x] != c.NonSolidTileID { tileBottom := float64((y + 1) * c.TileSize[1]) - collision := tileBottom - posY + collision := tileBottom - rectY if collision >= moveY { moveY = collision c.Collisions = append(c.Collisions, CollisionInfo[T]{