├── .gitignore ├── LICENSE ├── cell.go ├── circle.go ├── contactSet.go ├── convexPolygon.go ├── examples ├── common.go ├── cpu.pprof ├── excel.ttf ├── go.mod ├── go.sum ├── main.go ├── viewPProfInHTML.sh ├── worldBouncer.go ├── worldCircleTest.go └── worldPlatformer.go ├── go.mod ├── go.sum ├── logo.png ├── readme.md ├── shape.go ├── shapefilter.go ├── space.go ├── tags.go ├── utils.go └── vector.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | Game 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | .vscode/launch.json 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2021 SolarLune 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cell.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | // Cell is used to contain and organize Object information. 4 | type Cell struct { 5 | X, Y int 6 | Shapes []IShape // The Objects that a Cell contains. 7 | } 8 | 9 | // newCell creates a new cell at the specified X and Y position. Should not be used directly. 10 | func newCell(x, y int) *Cell { 11 | return &Cell{ 12 | X: x, 13 | Y: y, 14 | Shapes: []IShape{}, 15 | } 16 | } 17 | 18 | // register registers an object with a Cell. Should not be used directly. 19 | func (cell *Cell) register(obj IShape) { 20 | if !cell.Contains(obj) { 21 | cell.Shapes = append(cell.Shapes, obj) 22 | } 23 | } 24 | 25 | // unregister unregisters an object from a Cell. Should not be used directly. 26 | func (cell *Cell) unregister(obj IShape) { 27 | 28 | for i, o := range cell.Shapes { 29 | 30 | if o == obj { 31 | cell.Shapes[i] = cell.Shapes[len(cell.Shapes)-1] 32 | cell.Shapes = cell.Shapes[:len(cell.Shapes)-1] 33 | break 34 | } 35 | 36 | } 37 | 38 | } 39 | 40 | // Contains returns whether a Cell contains the specified Object at its position. 41 | func (cell *Cell) Contains(obj IShape) bool { 42 | for _, o := range cell.Shapes { 43 | if o == obj { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | // ContainsTags returns whether a Cell contains an Object that has the specified tag at its position. 51 | func (cell *Cell) HasTags(tags Tags) bool { 52 | for _, o := range cell.Shapes { 53 | if o.Tags().Has(tags) { 54 | return true 55 | } 56 | } 57 | return false 58 | } 59 | 60 | // IsOccupied returns whether a Cell contains any Objects at all. 61 | func (cell *Cell) IsOccupied() bool { 62 | return len(cell.Shapes) > 0 63 | } 64 | 65 | // CellSelection is a selection of cells. It is primarily used to filter down Shapes. 66 | type CellSelection struct { 67 | StartX, StartY, EndX, EndY int // The start and end position of the Cell in cellular locations. 68 | space *Space 69 | excludeSelf IShape 70 | } 71 | 72 | // FilterShapes returns a ShapeFilter of the shapes within the cell selection. 73 | func (c CellSelection) FilterShapes() ShapeFilter { 74 | 75 | if c.space == nil { 76 | return ShapeFilter{} 77 | } 78 | 79 | return ShapeFilter{ 80 | operatingOn: c, 81 | } 82 | 83 | } 84 | 85 | // ForEach loops through each shape in the CellSelection. 86 | func (c CellSelection) ForEach(iterationFunction func(shape IShape) bool) { 87 | // Internally, this function allows us to pass a CellSelection as the operatingOn property in a ShapeFilter. 88 | 89 | cellSelectionForEachIDSet = cellSelectionForEachIDSet[:0] 90 | 91 | for y := c.StartY; y <= c.EndY; y++ { 92 | 93 | for x := c.StartX; x <= c.EndX; x++ { 94 | 95 | cell := c.space.Cell(x, y) 96 | 97 | if cell != nil { 98 | 99 | for _, s := range cell.Shapes { 100 | 101 | if s == c.excludeSelf { 102 | continue 103 | } 104 | 105 | if cellSelectionForEachIDSet.idInSet(s.ID()) { 106 | continue 107 | } 108 | if !iterationFunction(s) { 109 | break 110 | } 111 | cellSelectionForEachIDSet = append(cellSelectionForEachIDSet, s.ID()) 112 | 113 | } 114 | 115 | } 116 | 117 | } 118 | 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /circle.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | // Circle represents a circle (naturally), and is essentially a point with a radius. 4 | type Circle struct { 5 | ShapeBase 6 | radius float64 7 | } 8 | 9 | // NewCircle returns a new Circle, with its center at the X and Y position given, and with the defined radius. 10 | func NewCircle(x, y, radius float64) *Circle { 11 | circle := &Circle{ 12 | ShapeBase: newShapeBase(x, y), 13 | radius: radius, 14 | } 15 | circle.ShapeBase.owner = circle 16 | return circle 17 | } 18 | 19 | // Clone clones the Circle. 20 | func (c *Circle) Clone() IShape { 21 | newCircle := NewCircle(c.position.X, c.position.Y, c.radius) 22 | newCircle.tags.Set(*c.tags) 23 | newCircle.ShapeBase = c.ShapeBase 24 | newCircle.id = globalShapeID 25 | globalShapeID++ 26 | newCircle.ShapeBase.space = nil 27 | newCircle.ShapeBase.touchingCells = []*Cell{} 28 | newCircle.ShapeBase.owner = newCircle 29 | return newCircle 30 | } 31 | 32 | // Bounds returns the top-left and bottom-right corners of the Circle. 33 | func (c *Circle) Bounds() Bounds { 34 | return Bounds{ 35 | Min: Vector{c.position.X - c.radius, c.position.Y - c.radius}, 36 | Max: Vector{c.position.X + c.radius, c.position.Y + c.radius}, 37 | space: c.space, 38 | } 39 | } 40 | 41 | func (c *Circle) Project(axis Vector) Projection { 42 | axis = axis.Unit() 43 | projectedCenter := axis.Dot(c.position) 44 | projectedRadius := axis.Magnitude() * c.radius 45 | 46 | min := projectedCenter - projectedRadius 47 | max := projectedCenter + projectedRadius 48 | 49 | if min > max { 50 | min, max = max, min 51 | } 52 | 53 | return Projection{min, max} 54 | } 55 | 56 | // Radius returns the radius of the Circle. 57 | func (c *Circle) Radius() float64 { 58 | return c.radius 59 | } 60 | 61 | // SetRadius sets the radius of the Circle, updating the scale multiplier to reflect this change. 62 | func (c *Circle) SetRadius(radius float64) { 63 | c.radius = radius 64 | c.update() 65 | } 66 | 67 | // Intersection returns an IntersectionSet for the other Shape provided. 68 | // If no intersection is detected, the IntersectionSet returned is empty. 69 | func (c *Circle) Intersection(other IShape) IntersectionSet { 70 | 71 | switch otherShape := other.(type) { 72 | case *ConvexPolygon: 73 | return circleConvexTest(c, otherShape) 74 | 75 | case *Circle: 76 | return circleCircleTest(c, otherShape) 77 | } 78 | 79 | // This should never happen 80 | panic("Unimplemented intersection") 81 | 82 | } 83 | -------------------------------------------------------------------------------- /contactSet.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Intersection represents a single point of contact against a line or surface. 8 | type Intersection struct { 9 | Point Vector // The point of contact. 10 | Normal Vector // The normal of the surface contacted. 11 | } 12 | 13 | // IntersectionSet represents a set of intersections between the calling object and one other intersecting Shape. 14 | // A Shape's intersection test may iterate through multiple IntersectionSets - one for each pair of intersecting objects. 15 | type IntersectionSet struct { 16 | Intersections []Intersection // Slice of points indicating contact between the two Shapes. 17 | Center Vector // Center of the Contact set; this is the average of all Points contained within all contacts in the IntersectionSet. 18 | MTV Vector // Minimum Translation Vector; this is the vector to move a Shape on to move it to contact with the other, intersecting / contacting Shape. 19 | OtherShape IShape // The other shape involved in the contact. 20 | } 21 | 22 | func newIntersectionSet() IntersectionSet { 23 | return IntersectionSet{} 24 | } 25 | 26 | // LeftmostPoint returns the left-most point out of the IntersectionSet's Points slice. 27 | // If the IntersectionSet is empty, this returns a zero Vector. 28 | func (is IntersectionSet) LeftmostPoint() Vector { 29 | 30 | var left Vector 31 | set := false 32 | 33 | for _, contact := range is.Intersections { 34 | 35 | if !set || contact.Point.X < left.X { 36 | left = contact.Point 37 | set = true 38 | } 39 | 40 | } 41 | 42 | return left 43 | 44 | } 45 | 46 | // RightmostPoint returns the right-most point out of the IntersectionSet's Points slice. 47 | // If the IntersectionSet is empty, this returns a zero Vector. 48 | func (is IntersectionSet) RightmostPoint() Vector { 49 | 50 | var right Vector 51 | set := false 52 | 53 | for _, contact := range is.Intersections { 54 | 55 | if !set || contact.Point.X > right.X { 56 | right = contact.Point 57 | set = true 58 | } 59 | 60 | } 61 | 62 | return right 63 | 64 | } 65 | 66 | // TopmostPoint returns the top-most point out of the IntersectionSet's Points slice. I 67 | // f the IntersectionSet is empty, this returns a zero Vector. 68 | func (is IntersectionSet) TopmostPoint() Vector { 69 | 70 | var top Vector 71 | set := false 72 | 73 | for _, contact := range is.Intersections { 74 | 75 | if !set || contact.Point.Y < top.Y { 76 | top = contact.Point 77 | set = true 78 | } 79 | 80 | } 81 | 82 | return top 83 | 84 | } 85 | 86 | // BottommostPoint returns the bottom-most point out of the IntersectionSet's Points slice. 87 | // If the IntersectionSet is empty, this returns a zero Vector. 88 | func (is IntersectionSet) BottommostPoint() Vector { 89 | 90 | var bottom Vector 91 | set := false 92 | 93 | for _, contact := range is.Intersections { 94 | 95 | if !set || contact.Point.Y > bottom.Y { 96 | bottom = contact.Point 97 | set = true 98 | } 99 | 100 | } 101 | 102 | return bottom 103 | 104 | } 105 | 106 | // IsEmpty returns if the IntersectionSet is empty (and so contains no points of iontersection). This should never actually be true. 107 | func (is IntersectionSet) IsEmpty() bool { 108 | return len(is.Intersections) == 0 109 | } 110 | 111 | // Distance returns the distance between all of the intersection points when projected against an axis. 112 | func (is IntersectionSet) Distance(alongAxis Vector) float64 { 113 | alongAxis = alongAxis.Unit() 114 | top, bottom := math.MaxFloat64, -math.MaxFloat64 115 | for _, c := range is.Intersections { 116 | d := alongAxis.Dot(c.Point) 117 | top = min(top, d) 118 | bottom = max(bottom, d) 119 | } 120 | return bottom - top 121 | } 122 | -------------------------------------------------------------------------------- /convexPolygon.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "math" 7 | "sort" 8 | ) 9 | 10 | // ConvexPolygon represents a series of points, connected by lines, constructing a convex shape. 11 | // The polygon has a position, a scale, a rotation, and may or may not be closed. 12 | type ConvexPolygon struct { 13 | ShapeBase 14 | 15 | scale Vector 16 | rotation float64 // How many radians the ConvexPolygon is rotated around in the viewing vector (Z). 17 | Points []Vector // Points represents the points constructing the ConvexPolygon. 18 | Closed bool // Closed is whether the ConvexPolygon is closed or not; only takes effect if there are more than 2 points. 19 | bounds Bounds 20 | } 21 | 22 | // NewConvexPolygon creates a new convex polygon at the position given, from the provided set of X and Y positions of 2D points (or vertices). 23 | // You don't need to pass any points at this stage, but if you do, you should pass whole pairs. The points should generally be ordered clockwise, 24 | // from X and Y of the first, to X and Y of the last. 25 | // For example: NewConvexPolygon(30, 20, 0, 0, 10, 0, 10, 10, 0, 10) would create a 10x10 convex 26 | // polygon square, with the vertices at {0,0}, {10,0}, {10, 10}, and {0, 10}, with the polygon itself occupying a position of 30, 20. 27 | // You can also pass the points using vectors with ConvexPolygon.AddPointsVec(). 28 | func NewConvexPolygon(x, y float64, points []float64) *ConvexPolygon { 29 | 30 | cp := &ConvexPolygon{ 31 | ShapeBase: newShapeBase(x, y), 32 | scale: NewVector(1, 1), 33 | Points: []Vector{}, 34 | Closed: true, 35 | } 36 | 37 | cp.owner = cp 38 | 39 | if len(points) > 0 { 40 | err := cp.AddPoints(points...) 41 | if err != nil { 42 | log.Println(err) 43 | } 44 | } 45 | 46 | return cp 47 | } 48 | 49 | func NewConvexPolygonVec(position Vector, points []Vector) *ConvexPolygon { 50 | 51 | cp := &ConvexPolygon{ 52 | ShapeBase: newShapeBase(position.X, position.Y), 53 | scale: NewVector(1, 1), 54 | Points: []Vector{}, 55 | Closed: true, 56 | } 57 | 58 | cp.owner = cp 59 | 60 | if len(points) > 0 { 61 | cp.AddPointsVec(points...) 62 | } 63 | 64 | return cp 65 | 66 | } 67 | 68 | // Clone returns a clone of the ConvexPolygon as an IShape. 69 | func (cp *ConvexPolygon) Clone() IShape { 70 | 71 | points := append(make([]Vector, 0, len(cp.Points)), cp.Points...) 72 | 73 | newPoly := NewConvexPolygonVec(cp.position, points) 74 | newPoly.tags.Set(*cp.tags) 75 | 76 | newPoly.ShapeBase = cp.ShapeBase 77 | newPoly.id = globalShapeID 78 | globalShapeID++ 79 | newPoly.ShapeBase.space = nil 80 | newPoly.ShapeBase.touchingCells = []*Cell{} 81 | newPoly.ShapeBase.owner = newPoly 82 | 83 | newPoly.rotation = cp.rotation 84 | newPoly.scale = cp.scale 85 | newPoly.Closed = cp.Closed 86 | 87 | return newPoly 88 | } 89 | 90 | // AddPoints allows you to add points to the ConvexPolygon with a slice or selection of float64s, with each pair indicating an X or Y value for 91 | // a point / vertex (i.e. AddPoints(0, 1, 2, 3) would add two points - one at {0, 1}, and another at {2, 3}). 92 | func (cp *ConvexPolygon) AddPoints(vertexPositions ...float64) error { 93 | if len(vertexPositions) < 4 { 94 | return errors.New("addpoints called with not enough passed vertex positions") 95 | } 96 | if len(vertexPositions)%2 == 1 { 97 | return errors.New("addpoints called with a non-even amount of vertex positions") 98 | } 99 | for v := 0; v < len(vertexPositions); v += 2 { 100 | cp.Points = append(cp.Points, Vector{vertexPositions[v], vertexPositions[v+1]}) 101 | } 102 | 103 | // Call updateBounds first so that the bounds are updated to determine cellular location 104 | cp.updateBounds() 105 | cp.update() 106 | return nil 107 | } 108 | 109 | // AddPointsVec allows you to add points to the ConvexPolygon with a slice of Vectors, each indicating a point / vertex. 110 | func (cp *ConvexPolygon) AddPointsVec(points ...Vector) { 111 | cp.Points = append(cp.Points, points...) 112 | cp.updateBounds() 113 | cp.update() 114 | } 115 | 116 | // Lines returns a slice of transformed internalLines composing the ConvexPolygon. 117 | func (cp *ConvexPolygon) Lines() []collidingLine { 118 | 119 | lines := []collidingLine{} 120 | 121 | vertices := cp.Transformed() 122 | 123 | for i := 0; i < len(vertices); i++ { 124 | 125 | start, end := vertices[i], vertices[0] 126 | 127 | if i < len(vertices)-1 { 128 | end = vertices[i+1] 129 | } else if !cp.Closed || len(cp.Points) <= 2 { 130 | break 131 | } 132 | 133 | line := newCollidingLine(start.X, start.Y, end.X, end.Y) 134 | 135 | lines = append(lines, line) 136 | 137 | } 138 | 139 | return lines 140 | 141 | } 142 | 143 | // Transformed returns the ConvexPolygon's points / vertices, transformed according to the ConvexPolygon's position. 144 | func (cp *ConvexPolygon) Transformed() []Vector { 145 | transformed := []Vector{} 146 | for _, point := range cp.Points { 147 | p := Vector{point.X * cp.scale.X, point.Y * cp.scale.Y} 148 | if cp.rotation != 0 { 149 | p = p.Rotate(-cp.rotation) 150 | } 151 | transformed = append(transformed, Vector{p.X + cp.position.X, p.Y + cp.position.Y}) 152 | } 153 | return transformed 154 | } 155 | 156 | // Bounds returns two Vectors, comprising the top-left and bottom-right positions of the bounds of the 157 | // ConvexPolygon, post-transformation. 158 | func (cp *ConvexPolygon) Bounds() Bounds { 159 | cp.bounds.space = cp.space 160 | return cp.bounds.MoveVec(cp.position) 161 | } 162 | 163 | func (cp *ConvexPolygon) updateBounds() { 164 | 165 | transformed := cp.Transformed() 166 | 167 | topLeft := Vector{transformed[0].X, transformed[0].Y} 168 | bottomRight := topLeft 169 | 170 | for i := 0; i < len(transformed); i++ { 171 | 172 | point := transformed[i] 173 | 174 | if point.X < topLeft.X { 175 | topLeft.X = point.X 176 | } else if point.X > bottomRight.X { 177 | bottomRight.X = point.X 178 | } 179 | 180 | if point.Y < topLeft.Y { 181 | topLeft.Y = point.Y 182 | } else if point.Y > bottomRight.Y { 183 | bottomRight.Y = point.Y 184 | } 185 | 186 | } 187 | 188 | cp.bounds = Bounds{ 189 | Min: topLeft, 190 | Max: bottomRight, 191 | space: cp.space, 192 | } 193 | 194 | // Untransform those points so that we don't have to update it whenever it moves 195 | cp.bounds = cp.bounds.Move(-cp.position.X, -cp.position.Y) 196 | 197 | } 198 | 199 | // Center returns the transformed Center of the ConvexPolygon. 200 | func (cp *ConvexPolygon) Center() Vector { 201 | 202 | // pos := Vector{0, 0} 203 | 204 | // for _, v := range cp.Transformed() { 205 | // pos = pos.Add(v) 206 | // } 207 | 208 | // pos.X /= float64(len(cp.Transformed())) 209 | // pos.Y /= float64(len(cp.Transformed())) 210 | 211 | // return pos 212 | 213 | return cp.Bounds().Center() 214 | 215 | } 216 | 217 | // Project projects (i.e. flattens) the ConvexPolygon onto the provided axis. 218 | func (cp *ConvexPolygon) Project(axis Vector) Projection { 219 | axis = axis.Unit() 220 | vertices := cp.Transformed() 221 | min := axis.Dot(vertices[0]) 222 | max := min 223 | for i := 1; i < len(vertices); i++ { 224 | p := axis.Dot(vertices[i]) 225 | if p < min { 226 | min = p 227 | } else if p > max { 228 | max = p 229 | } 230 | } 231 | return Projection{min, max} 232 | } 233 | 234 | // SATAxes returns the axes of the ConvexPolygon for SAT intersection testing. 235 | func (cp *ConvexPolygon) SATAxes() []Vector { 236 | 237 | axes := []Vector{} 238 | for _, line := range cp.Lines() { 239 | axes = append(axes, line.Normal()) 240 | } 241 | return axes 242 | 243 | } 244 | 245 | // Rotation returns the rotation (in radians) of the ConvexPolygon. 246 | func (p *ConvexPolygon) Rotation() float64 { 247 | return p.rotation 248 | } 249 | 250 | // SetRotation sets the rotation for the ConvexPolygon; note that the rotation goes counter-clockwise from 0 to pi, and then from -pi at 180 down, back to 0. 251 | // This rotation scheme follows the way math.Atan2() works. 252 | func (p *ConvexPolygon) SetRotation(radians float64) { 253 | p.rotation = radians 254 | if p.rotation > math.Pi { 255 | p.rotation -= math.Pi * 2 256 | } else if p.rotation < -math.Pi { 257 | p.rotation += math.Pi * 2 258 | } 259 | p.updateBounds() 260 | p.update() 261 | } 262 | 263 | // Rotate is a helper function to rotate a ConvexPolygon by the radians given. 264 | func (p *ConvexPolygon) Rotate(radians float64) { 265 | p.SetRotation(p.Rotation() + radians) 266 | } 267 | 268 | // Scale returns the scale multipliers of the ConvexPolygon. 269 | func (p *ConvexPolygon) Scale() Vector { 270 | return p.scale 271 | } 272 | 273 | // SetScale sets the scale multipliers of the ConvexPolygon. 274 | func (p *ConvexPolygon) SetScale(x, y float64) { 275 | p.scale.X = x 276 | p.scale.Y = y 277 | p.updateBounds() 278 | p.update() 279 | } 280 | 281 | // SetScaleVec sets the scale multipliers of the ConvexPolygon using the provided Vector. 282 | func (p *ConvexPolygon) SetScaleVec(vec Vector) { 283 | p.SetScale(vec.X, vec.Y) 284 | } 285 | 286 | // Intersection returns an IntersectionSet for the other Shape provided. 287 | // If no intersection is detected, the IntersectionSet returned is empty. 288 | func (p *ConvexPolygon) Intersection(other IShape) IntersectionSet { 289 | 290 | switch otherShape := other.(type) { 291 | case *ConvexPolygon: 292 | return convexConvexTest(p, otherShape) 293 | case *Circle: 294 | return convexCircleTest(p, otherShape) 295 | } 296 | 297 | // This should never happen 298 | panic("Unimplemented intersection") 299 | 300 | } 301 | 302 | // ShapeLineTestSettings is a struct of settings to be used when performing shape line tests 303 | // (the equivalent of 3D hitscan ray tests for 2D, but emitted from each vertex of the Shape). 304 | type ShapeLineTestSettings struct { 305 | StartOffset Vector // An offset to use for casting rays from each vertex of the Shape. 306 | Vector Vector // The direction and distance vector to use for casting the lines. 307 | TestAgainst ShapeIterator // The shapes to test against. 308 | // OnIntersect is the callback to be called for each intersection between a line from the given Shape, ranging from its origin off towards the given Vector against each shape given in TestAgainst. 309 | // set is the intersection set that contains information about the intersection, index is the index of the current intersection out of the max number of intersections, 310 | // and count is the total number of intersections detected from the intersection test. 311 | // The boolean the callback returns indicates whether the line test should continue iterating through results or stop at the currently found intersection. 312 | OnIntersect func(set IntersectionSet, index, count int) bool 313 | IncludeAllPoints bool // Whether to cast lines from all points in the Shape (true), or just points from the leading edges (false, and the default). Only takes effect for ConvexPolygons. 314 | Lines []int // Which line indices to cast from. If unset (which is the default), then all vertices from all lines will be used. 315 | } 316 | 317 | var lineTestResults []IntersectionSet 318 | var lineTestVertices = newSet[Vector]() 319 | 320 | // ShapeLineTest conducts a line test from each vertex of the ConvexPolygon using the settings passed. 321 | // By default, lines are cast from each vertex of each leading edge in the ConvexPolygon. 322 | func (cp *ConvexPolygon) ShapeLineTest(settings ShapeLineTestSettings) bool { 323 | 324 | lineTestResults = lineTestResults[:0] 325 | 326 | lineTestVertices.Clear() 327 | 328 | // We only have to test vertices from the leading lines, not all of them 329 | if !settings.IncludeAllPoints { 330 | 331 | for i, l := range cp.Lines() { 332 | 333 | found := true 334 | if len(settings.Lines) > 0 { 335 | found = false 336 | for lineIndex := range settings.Lines { 337 | if i == lineIndex { 338 | found = true 339 | break 340 | } 341 | } 342 | } 343 | 344 | if found { 345 | 346 | // If a line's normal points away from the checking vector, it isn't a leading edge 347 | if l.Normal().Dot(settings.Vector) < 0.01 { 348 | continue 349 | } 350 | 351 | // Kick the vertices in along the lines a bit to ensure they don't get snagged up on borders 352 | v := l.Vector().Scale(0.5).Invert() 353 | lineTestVertices.Add(l.Start.Sub(v)) 354 | lineTestVertices.Add(l.End.Sub(v.Invert())) 355 | 356 | } 357 | 358 | } 359 | 360 | } else { 361 | lineTestVertices.Add(cp.Transformed()...) 362 | } 363 | 364 | vu := settings.Vector.Unit() 365 | 366 | for p := range lineTestVertices { 367 | 368 | start := p.Sub(vu.Add(settings.StartOffset)) 369 | 370 | LineTest(LineTestSettings{ 371 | Start: start, 372 | End: p.Add(settings.Vector), 373 | TestAgainst: settings.TestAgainst, 374 | callingShape: cp, 375 | OnIntersect: func(set IntersectionSet, index, max int) bool { 376 | 377 | // Consolidate hits together across multiple objects 378 | for i := range lineTestResults { 379 | if lineTestResults[i].OtherShape == set.OtherShape { 380 | lineTestResults[i].Intersections = append(lineTestResults[i].Intersections, set.Intersections...) 381 | if set.MTV.MagnitudeSquared() < lineTestResults[i].MTV.MagnitudeSquared() { 382 | lineTestResults[i].MTV = set.MTV 383 | } 384 | return true 385 | } 386 | } 387 | 388 | lineTestResults = append(lineTestResults, set) 389 | 390 | return true 391 | 392 | }, 393 | }) 394 | 395 | } 396 | 397 | // Sort the results by smallest MTV because we can't really easily get the starting points of the ray test results 398 | sort.Slice(lineTestResults, func(i, j int) bool { 399 | return lineTestResults[i].MTV.MagnitudeSquared() < lineTestResults[j].MTV.MagnitudeSquared() 400 | }) 401 | 402 | if settings.OnIntersect != nil { 403 | 404 | for i := range lineTestResults { 405 | 406 | if !settings.OnIntersect(lineTestResults[i], i, len(lineTestResults)) { 407 | break 408 | } 409 | 410 | } 411 | 412 | } 413 | 414 | return len(lineTestResults) > 0 415 | } 416 | 417 | // calculateMTV returns the MTV, if possible, and a bool indicating whether it was possible or not. 418 | func (cp *ConvexPolygon) calculateMTV(otherShape IShape) (Vector, bool) { 419 | 420 | delta := Vector{0, 0} 421 | 422 | smallest := Vector{math.MaxFloat64, 0} 423 | 424 | switch other := otherShape.(type) { 425 | 426 | case *ConvexPolygon: 427 | 428 | for _, axis := range cp.SATAxes() { 429 | pa := cp.Project(axis) 430 | pb := other.Project(axis) 431 | 432 | overlap := pa.Overlap(pb) 433 | 434 | if overlap <= 0 { 435 | return Vector{}, false 436 | } 437 | 438 | if smallest.Magnitude() > overlap { 439 | smallest = axis.Scale(overlap) 440 | } 441 | 442 | } 443 | 444 | for _, axis := range other.SATAxes() { 445 | 446 | pa := cp.Project(axis) 447 | pb := other.Project(axis) 448 | 449 | overlap := pa.Overlap(pb) 450 | 451 | if overlap <= 0 { 452 | return Vector{}, false 453 | } 454 | 455 | if smallest.Magnitude() > overlap { 456 | smallest = axis.Scale(overlap) 457 | } 458 | 459 | } 460 | 461 | // If the direction from target to source points opposite to the separation, invert the separation vector. 462 | if cp.Center().Sub(other.Center()).Dot(smallest) < 0 { 463 | smallest = smallest.Invert() 464 | } 465 | 466 | case *Circle: 467 | 468 | verts := append([]Vector{}, cp.Transformed()...) 469 | center := other.position 470 | sort.Slice(verts, func(i, j int) bool { return verts[i].Sub(center).Magnitude() < verts[j].Sub(center).Magnitude() }) 471 | 472 | axis := Vector{center.X - verts[0].X, center.Y - verts[0].Y} 473 | 474 | pa := cp.Project(axis) 475 | pb := other.Project(axis) 476 | overlap := pa.Overlap(pb) 477 | if overlap <= 0 { 478 | return Vector{}, false 479 | } 480 | smallest = axis.Unit().Scale(overlap) 481 | 482 | for _, axis := range cp.SATAxes() { 483 | pa := cp.Project(axis) 484 | pb := other.Project(axis) 485 | 486 | overlap := pa.Overlap(pb) 487 | 488 | if overlap <= 0 { 489 | return Vector{}, false 490 | } 491 | 492 | if smallest.Magnitude() > overlap { 493 | smallest = axis.Scale(overlap) 494 | } 495 | 496 | } 497 | 498 | // If the direction from target to source points opposite to the separation, invert the separation vector 499 | if cp.Center().Sub(other.position).Dot(smallest) < 0 { 500 | smallest = smallest.Invert() 501 | } 502 | 503 | } 504 | 505 | delta.X = smallest.X 506 | delta.Y = smallest.Y 507 | 508 | pointingDirection := otherShape.Position().Sub(cp.Position()) 509 | if pointingDirection.Dot(delta) > 0 { 510 | delta = delta.Invert() 511 | } 512 | 513 | return delta, true 514 | } 515 | 516 | // IsContainedBy returns if the ConvexPolygon is wholly contained by the other shape provided. 517 | // Note that only testing against ConvexPolygons is implemented currently. 518 | func (cp *ConvexPolygon) IsContainedBy(otherShape IShape) bool { 519 | 520 | switch other := otherShape.(type) { 521 | 522 | case *ConvexPolygon: 523 | 524 | for _, axis := range cp.SATAxes() { 525 | if !cp.Project(axis).IsInside(other.Project(axis)) { 526 | return false 527 | } 528 | } 529 | 530 | for _, axis := range other.SATAxes() { 531 | if !cp.Project(axis).IsInside(other.Project(axis)) { 532 | return false 533 | } 534 | } 535 | 536 | // TODO: Implement this for Circles 537 | 538 | } 539 | 540 | return true 541 | } 542 | 543 | // FlipH flips the ConvexPolygon's vertices horizontally, across the polygon's width, according to their initial offset when adding the points. 544 | func (cp *ConvexPolygon) FlipH() { 545 | 546 | for _, v := range cp.Points { 547 | v.X = -v.X 548 | } 549 | // We have to reverse vertex order after flipping the vertices to ensure the winding order is consistent between 550 | // Objects (so that the normals are consistently outside or inside, which is important when doing Intersection tests). 551 | // If we assume that the normal of a line, going from vertex A to vertex B, is one direction, then the normal would be 552 | // inverted if the vertices were flipped in position, but not in order. This would make Intersection tests drive objects 553 | // into each other, instead of giving the delta to move away. 554 | cp.ReverseVertexOrder() 555 | cp.updateBounds() 556 | cp.update() 557 | 558 | } 559 | 560 | // FlipV flips the ConvexPolygon's vertices vertically according to their initial offset when adding the points. 561 | func (cp *ConvexPolygon) FlipV() { 562 | 563 | for _, v := range cp.Points { 564 | v.Y = -v.Y 565 | } 566 | cp.ReverseVertexOrder() 567 | cp.updateBounds() 568 | cp.update() 569 | 570 | } 571 | 572 | // RecenterPoints recenters the vertices in the polygon, such that they are all equidistant from the center. 573 | // For example, say you had a polygon with the following three points: {0, 0}, {10, 0}, {0, 16}. 574 | // After calling cp.RecenterPoints(), the polygon's points would be at {-5, -8}, {5, -8}, {-5, 8}. 575 | func (cp *ConvexPolygon) RecenterPoints() { 576 | 577 | if len(cp.Points) <= 1 { 578 | return 579 | } 580 | 581 | offset := Vector{0, 0} 582 | for _, p := range cp.Points { 583 | offset = offset.Add(p) 584 | } 585 | 586 | offset = offset.Scale(1.0 / float64(len(cp.Points))).Invert() 587 | 588 | for i := range cp.Points { 589 | cp.Points[i] = cp.Points[i].Add(offset) 590 | } 591 | 592 | cp.position = cp.position.Sub(offset) 593 | 594 | cp.updateBounds() 595 | 596 | cp.update() 597 | 598 | } 599 | 600 | // ReverseVertexOrder reverses the vertex ordering of the ConvexPolygon. 601 | func (cp *ConvexPolygon) ReverseVertexOrder() { 602 | 603 | verts := []Vector{cp.Points[0]} 604 | 605 | for i := len(cp.Points) - 1; i >= 1; i-- { 606 | verts = append(verts, cp.Points[i]) 607 | } 608 | 609 | cp.Points = verts 610 | 611 | } 612 | 613 | // NewRectangle returns a rectangular ConvexPolygon at the position given with the vertices ordered in clockwise order. 614 | // The Rectangle's origin will be the center of its shape (as is recommended for collision testing). 615 | // The {x, y} is the center position of the Rectangle). 616 | func NewRectangle(x, y, w, h float64) *ConvexPolygon { 617 | // TODO: In actuality, an AABBRectangle should be its own "thing" with its own optimized Intersection code check. 618 | 619 | hw := w / 2 620 | hh := h / 2 621 | 622 | return NewConvexPolygon( 623 | x, y, 624 | 625 | []float64{ 626 | -hw, -hh, 627 | hw, -hh, 628 | hw, hh, 629 | -hw, hh, 630 | }, 631 | ) 632 | } 633 | 634 | // NewRectangleFromTopLeft returns a rectangular ConvexPolygon at the position given with the vertices ordered in clockwise order. 635 | // The Rectangle's origin will be the center of its shape (as is recommended for collision testing). 636 | // Note that the rectangle will be positioned such that x, y is the top-left corner, though the center-point is still 637 | // in the center of the ConvexPolygon shape. 638 | func NewRectangleFromTopLeft(x, y, w, h float64) *ConvexPolygon { 639 | 640 | r := NewRectangle(x, y, w, h) 641 | r.Move(w/2, h/2) 642 | return r 643 | } 644 | 645 | // NewRectangleFromCorners returns a rectangluar ConvexPolygon properly centered with its corners at the given { x1, y1 } and { x2, y2 } coordinates. 646 | // The Rectangle's origin will be the center of its shape (as is recommended for collision testing). 647 | func NewRectangleFromCorners(x1, y1, x2, y2 float64) *ConvexPolygon { 648 | 649 | if x2 < x2 { 650 | x1, x2 = x2, x1 651 | } 652 | if y2 < y2 { 653 | y1, y2 = y2, y1 654 | } 655 | 656 | halfWidth := (x2 - x1) / 2 657 | halfHeight := (y2 - y1) / 2 658 | 659 | return NewConvexPolygon( 660 | x1+halfWidth, y1+halfHeight, 661 | 662 | []float64{ 663 | -halfWidth, -halfHeight, 664 | halfWidth, -halfHeight, 665 | halfWidth, halfHeight, 666 | -halfWidth, halfHeight, 667 | }, 668 | ) 669 | } 670 | 671 | func NewLine(x1, y1, x2, y2 float64) *ConvexPolygon { 672 | 673 | cx := x1 + ((x2 - x1) / 2) 674 | cy := y1 + ((y2 - y1) / 2) 675 | 676 | return NewConvexPolygon( 677 | cx, cy, 678 | 679 | []float64{ 680 | cx - x1, cy - y1, 681 | cx - x2, cy - y2, 682 | }, 683 | ) 684 | } 685 | 686 | ///// 687 | 688 | // A collidingLine is a helper shape used to determine if two ConvexPolygon lines intersect; you can't create a collidingLine to use as a Shape. 689 | // Instead, you can create a ConvexPolygon, specify two points, and set its Closed value to false (or use NewLine(), as this does it for you). 690 | type collidingLine struct { 691 | Start, End Vector 692 | } 693 | 694 | func newCollidingLine(x, y, x2, y2 float64) collidingLine { 695 | return collidingLine{ 696 | Start: Vector{x, y}, 697 | End: Vector{x2, y2}, 698 | } 699 | } 700 | 701 | func (line collidingLine) Project(axis Vector) Vector { 702 | return line.Vector().Scale(axis.Dot(line.Start.Sub(line.End))) 703 | } 704 | 705 | func (line collidingLine) Normal() Vector { 706 | v := line.Vector() 707 | return Vector{v.Y, -v.X}.Unit() 708 | } 709 | 710 | func (line collidingLine) Vector() Vector { 711 | return line.End.Sub(line.Start).Unit() 712 | } 713 | 714 | // IntersectionPointsLine returns the intersection point of a Line with another Line as a Vector, and if the intersection was found. 715 | func (line collidingLine) IntersectionPointsLine(other collidingLine) (Vector, bool) { 716 | 717 | det := (line.End.X-line.Start.X)*(other.End.Y-other.Start.Y) - (other.End.X-other.Start.X)*(line.End.Y-line.Start.Y) 718 | 719 | if det != 0 { 720 | 721 | // MAGIC MATH; the extra + 1 here makes it so that corner cases (literally, lines going through corners) works. 722 | 723 | // lambda := (float32(((line.Y-b.Y)*(b.X2-b.X))-((line.X-b.X)*(b.Y2-b.Y))) + 1) / float32(det) 724 | lambda := (((line.Start.Y - other.Start.Y) * (other.End.X - other.Start.X)) - ((line.Start.X - other.Start.X) * (other.End.Y - other.Start.Y)) + 1) / det 725 | 726 | // gamma := (float32(((line.Y-b.Y)*(line.X2-line.X))-((line.X-b.X)*(line.Y2-line.Y))) + 1) / float32(det) 727 | gamma := (((line.Start.Y - other.Start.Y) * (line.End.X - line.Start.X)) - ((line.Start.X - other.Start.X) * (line.End.Y - line.Start.Y)) + 1) / det 728 | 729 | if (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1) { 730 | 731 | // Delta 732 | dx := line.End.X - line.Start.X 733 | dy := line.End.Y - line.Start.Y 734 | 735 | // dx, dy := line.GetDelta() 736 | 737 | return Vector{line.Start.X + (lambda * dx), line.Start.Y + (lambda * dy)}, true 738 | } 739 | 740 | } 741 | 742 | return Vector{}, false 743 | 744 | } 745 | 746 | // IntersectionPointsCircle returns a slice of Vectors, each indicating the intersection point. If no intersection is found, it will return an empty slice. 747 | func (line collidingLine) IntersectionPointsCircle(circle *Circle) []Vector { 748 | 749 | points := []Vector{} 750 | 751 | cp := circle.position 752 | lStart := line.Start.Sub(cp) 753 | lEnd := line.End.Sub(cp) 754 | diff := lEnd.Sub(lStart) 755 | 756 | a := diff.X*diff.X + diff.Y*diff.Y 757 | b := 2 * ((diff.X * lStart.X) + (diff.Y * lStart.Y)) 758 | c := (lStart.X * lStart.X) + (lStart.Y * lStart.Y) - (circle.radius * circle.radius) 759 | 760 | det := b*b - (4 * a * c) 761 | 762 | if det < 0 { 763 | // Do nothing, no intersections 764 | } else if det == 0 { 765 | 766 | t := -b / (2 * a) 767 | 768 | if t >= 0 && t <= 1 { 769 | points = append(points, Vector{line.Start.X + t*diff.X, line.Start.Y + t*diff.Y}) 770 | } 771 | 772 | } else { 773 | 774 | t := (-b + math.Sqrt(det)) / (2 * a) 775 | 776 | // We have to ensure t is between 0 and 1; otherwise, the collision points are on the circle as though the lines were infinite in length. 777 | if t >= 0 && t <= 1 { 778 | points = append(points, Vector{line.Start.X + t*diff.X, line.Start.Y + t*diff.Y}) 779 | } 780 | t = (-b - math.Sqrt(det)) / (2 * a) 781 | if t >= 0 && t <= 1 { 782 | points = append(points, Vector{line.Start.X + t*diff.X, line.Start.Y + t*diff.Y}) 783 | } 784 | 785 | } 786 | 787 | return points 788 | 789 | } 790 | -------------------------------------------------------------------------------- /examples/common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/hajimehoshi/ebiten/v2/vector" 8 | "github.com/solarlune/resolv" 9 | ) 10 | 11 | type WorldInterface interface { 12 | Init() 13 | Update() 14 | Draw(img *ebiten.Image) 15 | Space() *resolv.Space 16 | } 17 | 18 | func CommonDraw(screen *ebiten.Image, world WorldInterface) { 19 | 20 | world.Space().ForEachShape(func(shape resolv.IShape, index, maxCount int) bool { 21 | 22 | var drawColor color.Color = color.White 23 | 24 | tags := shape.Tags() 25 | 26 | if tags.Has(TagPlatform) && !tags.Has(TagSolidWall) { 27 | drawColor = color.RGBA{255, 128, 35, 255} 28 | } 29 | if tags.Has(TagPlayer) { 30 | drawColor = color.RGBA{32, 255, 128, 255} 31 | } 32 | if tags.Has(TagBouncer) { 33 | r := uint8(32) 34 | g := uint8(128) 35 | bouncer := shape.Data().(*Bouncer) 36 | r += uint8((255 - float64(r)) * bouncer.ColorChange) 37 | g += uint8((255 - float64(g)) * bouncer.ColorChange) 38 | drawColor = color.RGBA{r, g, 255, 255} 39 | } 40 | switch o := shape.(type) { 41 | case *resolv.Circle: 42 | vector.StrokeCircle(screen, float32(o.Position().X), float32(o.Position().Y), float32(o.Radius()), 2, drawColor, false) 43 | case *resolv.ConvexPolygon: 44 | 45 | for _, l := range o.Lines() { 46 | vector.StrokeLine(screen, float32(l.Start.X), float32(l.Start.Y), float32(l.End.X), float32(l.End.Y), 2, drawColor, false) 47 | } 48 | } 49 | 50 | return true 51 | 52 | }) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /examples/cpu.pprof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolarLune/resolv/8b4e8c15ba3b6428f976ddb2d56bbe04b719a8fe/examples/cpu.pprof -------------------------------------------------------------------------------- /examples/excel.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolarLune/resolv/8b4e8c15ba3b6428f976ddb2d56bbe04b719a8fe/examples/excel.ttf -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/solarlune/resolv/examples 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/hajimehoshi/ebiten/v2 v2.8.3 9 | github.com/solarlune/resolv v0.7.0 10 | github.com/tanema/gween v0.0.0-20221212145351-621cc8a459d1 11 | golang.org/x/image v0.20.0 12 | ) 13 | 14 | require ( 15 | github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect 16 | github.com/ebitengine/hideconsole v1.0.0 // indirect 17 | github.com/ebitengine/purego v0.8.0 // indirect 18 | github.com/go-text/typesetting v0.2.0 // indirect 19 | github.com/jezek/xgb v1.1.1 // indirect 20 | golang.org/x/sync v0.8.0 // indirect 21 | golang.org/x/sys v0.25.0 // indirect 22 | golang.org/x/text v0.18.0 // indirect 23 | ) 24 | 25 | replace github.com/solarlune/resolv => ../ 26 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= 4 | github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= 5 | github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= 6 | github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= 7 | github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= 8 | github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 9 | github.com/go-text/typesetting v0.2.0 h1:fbzsgbmk04KiWtE+c3ZD4W2nmCRzBqrqQOvYlwAOdho= 10 | github.com/go-text/typesetting v0.2.0/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I= 11 | github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY= 12 | github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= 13 | github.com/hajimehoshi/bitmapfont/v3 v3.2.0 h1:0DISQM/rseKIJhdF29AkhvdzIULqNIIlXAGWit4ez1Q= 14 | github.com/hajimehoshi/bitmapfont/v3 v3.2.0/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg= 15 | github.com/hajimehoshi/ebiten/v2 v2.8.3 h1:AKHqj3QbQMzNEhK33MMJeRwXm9UzftrUUo6AWwFV258= 16 | github.com/hajimehoshi/ebiten/v2 v2.8.3/go.mod h1:SXx/whkvpfsavGo6lvZykprerakl+8Uo1X8d2U5aAnA= 17 | github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= 18 | github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 19 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 20 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 25 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 26 | github.com/tanema/gween v0.0.0-20221212145351-621cc8a459d1 h1:s2Tn3G6rP4VljC5XDN6hARqXogkhr3k/jAsTqawSN5U= 27 | github.com/tanema/gween v0.0.0-20221212145351-621cc8a459d1/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M= 28 | golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= 29 | golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= 30 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 31 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 32 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 33 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 35 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 38 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 39 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "image/color" 9 | "os" 10 | "runtime/pprof" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/hajimehoshi/ebiten/v2" 15 | "github.com/hajimehoshi/ebiten/v2/inpututil" 16 | "github.com/hajimehoshi/ebiten/v2/text/v2" 17 | "github.com/hajimehoshi/ebiten/v2/vector" 18 | "github.com/solarlune/resolv" 19 | "golang.org/x/image/font/gofont/goregular" 20 | ) 21 | 22 | type Game struct { 23 | Worlds []WorldInterface 24 | CurrentWorld int 25 | Width, Height int 26 | DebugSpace bool 27 | ShowHelpText bool 28 | Screen *ebiten.Image 29 | FontFace text.Face 30 | Time float64 31 | } 32 | 33 | func NewGame() *Game { 34 | 35 | ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) 36 | ebiten.SetWindowTitle("resolv test") 37 | 38 | g := &Game{ 39 | Width: 640, 40 | Height: 360, 41 | ShowHelpText: true, 42 | } 43 | 44 | g.Worlds = []WorldInterface{ 45 | NewWorldPlatformer(), 46 | NewWorldBouncer(), 47 | NewWorldCircle(), 48 | } 49 | 50 | // g.Worlds = []WorldInterface{ 51 | // NewWorldBouncer(g), 52 | // NewWorldPlatformer(g), 53 | // NewWorldLineTest(g), 54 | // // NewWorldMultiShape(g), // MultiShapes are still buggy; gotta fix 'em up 55 | // NewWorldShapeTest(g), 56 | // NewWorldDirectTest(g), 57 | // } 58 | 59 | // g.FontFace = truetype.NewFace(fontData, opts) 60 | 61 | faceSrc, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF)) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | g.FontFace = &text.GoTextFace{ 67 | Source: faceSrc, 68 | Size: 15, 69 | } 70 | 71 | // Debug FPS rendering 72 | 73 | go func() { 74 | 75 | for { 76 | 77 | fmt.Println("FPS: ", ebiten.ActualFPS()) 78 | fmt.Println("Ticks: ", ebiten.ActualTPS()) 79 | time.Sleep(time.Second) 80 | 81 | } 82 | 83 | }() 84 | 85 | return g 86 | 87 | } 88 | 89 | func (g *Game) Update() error { 90 | 91 | var quit error 92 | 93 | if inpututil.IsKeyJustPressed(ebiten.KeyF) { 94 | fmt.Println("toggle slow-mo") 95 | if ebiten.TPS() >= 60 { 96 | ebiten.SetTPS(6) 97 | } else { 98 | ebiten.SetTPS(60) 99 | } 100 | } 101 | 102 | if inpututil.IsKeyJustPressed(ebiten.KeyP) { 103 | g.StartProfiling() 104 | } 105 | 106 | if inpututil.IsKeyJustPressed(ebiten.KeyF2) { 107 | g.DebugSpace = !g.DebugSpace 108 | } 109 | 110 | if inpututil.IsKeyJustPressed(ebiten.KeyF1) { 111 | g.ShowHelpText = !g.ShowHelpText 112 | } 113 | 114 | if inpututil.IsKeyJustPressed(ebiten.KeyF4) { 115 | ebiten.SetFullscreen(!ebiten.IsFullscreen()) 116 | } 117 | 118 | if inpututil.IsKeyJustPressed(ebiten.KeyE) { 119 | g.CurrentWorld++ 120 | } 121 | 122 | if inpututil.IsKeyJustPressed(ebiten.KeyQ) { 123 | g.CurrentWorld-- 124 | } 125 | 126 | if g.CurrentWorld >= len(g.Worlds) { 127 | g.CurrentWorld = 0 128 | } else if g.CurrentWorld < 0 { 129 | g.CurrentWorld = len(g.Worlds) - 1 130 | } 131 | 132 | world := g.Worlds[g.CurrentWorld] 133 | 134 | if inpututil.IsKeyJustPressed(ebiten.KeyR) { 135 | fmt.Println("Restart World") 136 | world.Init() 137 | } 138 | 139 | if ebiten.IsKeyPressed(ebiten.KeyEscape) { 140 | quit = errors.New("quit") 141 | } 142 | 143 | world.Update() 144 | 145 | g.Time += 1.0 / 60.0 146 | 147 | return quit 148 | 149 | } 150 | 151 | func (g *Game) Draw(screen *ebiten.Image) { 152 | g.Screen = screen 153 | screen.Fill(color.RGBA{20, 20, 40, 255}) 154 | 155 | world := g.Worlds[g.CurrentWorld] 156 | world.Draw(screen) 157 | 158 | if g.ShowHelpText { 159 | g.DrawText(screen, 0, 0, 160 | "Press F1 to show and hide help text.", 161 | "Press F2 to debug draw the Space.", 162 | "Q and E switch to different Worlds.", 163 | "FPS: "+strconv.FormatFloat(ebiten.ActualFPS(), 'f', 1, 32), 164 | "TPS: "+strconv.FormatFloat(ebiten.ActualTPS(), 'f', 1, 32), 165 | ) 166 | } 167 | if g.DebugSpace { 168 | g.DebugDraw(screen, world.Space()) 169 | } 170 | } 171 | 172 | func (g *Game) DrawText(screen *ebiten.Image, x, y int, textLines ...string) { 173 | metrics := g.FontFace.Metrics() 174 | for _, txt := range textLines { 175 | w, h := text.Measure(txt, g.FontFace, 16) 176 | vector.DrawFilledRect(screen, float32(x+2), float32(y), float32(w), float32(h), color.RGBA{0, 0, 0, 192}, false) 177 | 178 | opt := text.DrawOptions{} 179 | opt.GeoM.Translate(float64(x+2), float64(y+2-int(metrics.VDescent)-4)) 180 | opt.Filter = ebiten.FilterNearest 181 | // opt.ColorScale.ScaleWithColor(color.RGBA{0, 0, 150, 255}) 182 | 183 | text.Draw(screen, txt, g.FontFace, &opt) 184 | // text.Draw(screen, txt, g.FontFace, x, y, color.RGBA{100, 150, 255, 255}) 185 | y += 16 186 | } 187 | } 188 | 189 | func (g *Game) DebugDraw(screen *ebiten.Image, space *resolv.Space) { 190 | 191 | for y := 0; y < space.Height(); y++ { 192 | 193 | for x := 0; x < space.Width(); x++ { 194 | 195 | cell := space.Cell(x, y) 196 | 197 | cw := float32(space.CellWidth()) 198 | ch := float32(space.CellHeight()) 199 | cx := float32(cell.X) * cw 200 | cy := float32(cell.Y) * ch 201 | 202 | drawColor := color.RGBA{20, 20, 20, 255} 203 | 204 | if cell.IsOccupied() { 205 | drawColor = color.RGBA{255, 255, 0, 255} 206 | } 207 | 208 | vector.StrokeRect(screen, cx, cy, cx+cw, cy, 2, drawColor, false) 209 | 210 | vector.StrokeRect(screen, cx+cw, cy, cx+cw, cy+ch, 2, drawColor, false) 211 | 212 | vector.StrokeRect(screen, cx+cw, cy+ch, cx, cy+ch, 2, drawColor, false) 213 | 214 | vector.StrokeRect(screen, cx, cy+ch, cx, cy, 2, drawColor, false) 215 | } 216 | 217 | } 218 | 219 | } 220 | 221 | func (g *Game) StartProfiling() { 222 | outFile, err := os.Create("./cpu.pprof") 223 | if err != nil { 224 | fmt.Println(err.Error()) 225 | return 226 | } 227 | 228 | fmt.Println("Beginning CPU profiling...") 229 | pprof.StartCPUProfile(outFile) 230 | go func() { 231 | time.Sleep(5 * time.Second) 232 | pprof.StopCPUProfile() 233 | fmt.Println("CPU profiling finished.") 234 | }() 235 | } 236 | 237 | func (g *Game) Layout(w, h int) (int, int) { 238 | return g.Width, g.Height 239 | } 240 | 241 | func main() { 242 | GlobalGame = NewGame() 243 | ebiten.RunGame(GlobalGame) 244 | } 245 | 246 | var GlobalGame *Game 247 | -------------------------------------------------------------------------------- /examples/viewPProfInHTML.sh: -------------------------------------------------------------------------------- 1 | go tool pprof -http=localhost:8080 ./cpu.pprof -------------------------------------------------------------------------------- /examples/worldBouncer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand/v2" 6 | "time" 7 | 8 | "github.com/hajimehoshi/ebiten/v2" 9 | "github.com/solarlune/resolv" 10 | ) 11 | 12 | var ( 13 | TagBouncer = resolv.NewTag("bouncer") 14 | ) 15 | 16 | type WorldBouncer struct { 17 | space *resolv.Space 18 | Solids resolv.ShapeCollection 19 | Bouncers []*Bouncer 20 | 21 | BouncerUpdateTime time.Duration 22 | UpdateTimeTick int 23 | } 24 | 25 | func NewWorldBouncer() *WorldBouncer { 26 | w := &WorldBouncer{} 27 | w.Init() 28 | return w 29 | } 30 | 31 | func (w *WorldBouncer) Init() { 32 | 33 | if w.space != nil { 34 | w.space.RemoveAll() 35 | } 36 | 37 | for i := range w.Bouncers { 38 | w.Bouncers[i] = nil 39 | } 40 | 41 | w.Bouncers = w.Bouncers[:0] 42 | 43 | // Create the space. 44 | w.space = resolv.NewSpace(640, 360, 16, 16) 45 | 46 | // Create a selection of shapes that comprise the walls. 47 | w.Solids = resolv.ShapeCollection{ 48 | resolv.NewRectangleFromTopLeft(0, 0, 640, 16), 49 | resolv.NewRectangleFromTopLeft(0, 360-16, 640, 16), 50 | resolv.NewRectangleFromTopLeft(0, 16, 16, 360-16), 51 | resolv.NewRectangleFromTopLeft(640-16, 16, 16, 360-16), 52 | resolv.NewRectangleFromTopLeft(64, 128, 16, 200), 53 | resolv.NewRectangleFromTopLeft(120, 300, 200, 8), 54 | } 55 | 56 | // Set their tags (not strictly necessary here because the bouncers bounce off of everything and anything).. 57 | w.Solids.SetTags(TagSolidWall) 58 | 59 | // Add them to the space. 60 | w.space.Add(w.Solids...) 61 | 62 | } 63 | 64 | func (w *WorldBouncer) Update() { 65 | 66 | t := time.Now() 67 | 68 | for _, b := range w.Bouncers { 69 | b.Update() 70 | } 71 | 72 | if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { 73 | x, y := ebiten.CursorPosition() 74 | b := NewBouncer(float64(x), float64(y), w) 75 | w.Bouncers = append(w.Bouncers, b) 76 | } 77 | 78 | if len(w.Bouncers) > 0 { 79 | if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) { 80 | x, y := ebiten.CursorPosition() 81 | for i := len(w.Bouncers) - 1; i >= 0; i-- { 82 | if w.Bouncers[i].Object.Position().Distance(resolv.NewVector(float64(x), float64(y))) < 64 { 83 | w.space.Remove(w.Bouncers[i].Object) 84 | w.Bouncers[i] = nil 85 | w.Bouncers = append(w.Bouncers[:i], w.Bouncers[i+1:]...) 86 | } 87 | } 88 | } 89 | } 90 | 91 | w.UpdateTimeTick++ 92 | if w.UpdateTimeTick >= 10 { 93 | w.UpdateTimeTick = 0 94 | w.BouncerUpdateTime = time.Since(t) 95 | } 96 | 97 | } 98 | 99 | func (w *WorldBouncer) Draw(screen *ebiten.Image) { 100 | CommonDraw(screen, w) 101 | if GlobalGame.ShowHelpText { 102 | GlobalGame.DrawText(screen, 0, 128, 103 | "Bouncer Test", 104 | "Left click to add spheres", 105 | "Right click to remove spheres", 106 | fmt.Sprintf("%d Bouncers in the world", len(w.Bouncers)), 107 | fmt.Sprintf("Update Time: %s", w.BouncerUpdateTime.String()), 108 | ) 109 | } 110 | } 111 | 112 | // To allow the world's physical state to be drawn using the debug draw function. 113 | func (w *WorldBouncer) Space() *resolv.Space { 114 | return w.space 115 | } 116 | 117 | type Bouncer struct { 118 | Object *resolv.Circle 119 | Movement resolv.Vector 120 | 121 | ColorChange float64 122 | } 123 | 124 | func NewBouncer(x, y float64, world *WorldBouncer) *Bouncer { 125 | 126 | bouncer := &Bouncer{ 127 | Object: resolv.NewCircle(x, y, 8), 128 | Movement: resolv.NewVector(rand.Float64()*2-1, 0), 129 | } 130 | 131 | bouncer.Object.Tags().Set(TagBouncer) 132 | bouncer.Object.SetData(bouncer) 133 | 134 | world.space.Add(bouncer.Object) 135 | return bouncer 136 | 137 | } 138 | 139 | func (b *Bouncer) Update() { 140 | 141 | gravity := 0.25 142 | b.Movement.Y += gravity 143 | 144 | // Clamp movement to the maximum speed of half the size of a ball (so at max speed, it can't go beyond halfway through a surface) 145 | b.Movement = b.Movement.ClampMagnitude(8) 146 | 147 | b.Object.MoveVec(b.Movement) 148 | 149 | b.ColorChange *= 0.98 150 | 151 | totalMTV := resolv.NewVector(0, 0) 152 | 153 | b.Object.IntersectionTest(resolv.IntersectionTestSettings{ 154 | 155 | TestAgainst: b.Object.SelectTouchingCells(1).FilterShapes(), 156 | 157 | OnIntersect: func(set resolv.IntersectionSet) bool { 158 | b.Movement = b.Movement.Reflect(set.Intersections[0].Normal).Scale(0.9) 159 | b.ColorChange = b.Movement.Magnitude() 160 | // Collect all MTV values to apply together (mainly because bouncers might intersect each other and by moving, 161 | // phase into other Bouncers). By collecting all MTV values together and moving at once, it minimizes the chances 162 | // of Bouncers moving into each other and forcing lower ones through walls. 163 | totalMTV = totalMTV.Add(set.MTV) 164 | return true // Keep looping through intersection sets 165 | }, 166 | }) 167 | 168 | b.Object.MoveVec(totalMTV) 169 | 170 | if b.ColorChange > 1 { 171 | b.ColorChange = 1 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /examples/worldCircleTest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/solarlune/resolv" 6 | ) 7 | 8 | type WorldCircle struct { 9 | space *resolv.Space 10 | Circle *Circle 11 | } 12 | 13 | func NewWorldCircle() *WorldCircle { 14 | w := &WorldCircle{} 15 | w.Init() 16 | return w 17 | } 18 | 19 | func (w *WorldCircle) Init() { 20 | 21 | if w.space != nil { 22 | w.space.RemoveAll() 23 | } 24 | 25 | // Create the space. It is 640x360 large (the size of the screen), and divided into 16x16 cells. 26 | // The cell division makes it more efficient to check for shapes. 27 | w.space = resolv.NewSpace(640, 360, 16, 16) 28 | 29 | solids := resolv.ShapeCollection{ 30 | resolv.NewRectangleFromTopLeft(0, 0, 640, 16), 31 | resolv.NewRectangleFromTopLeft(0, 360-16, 640, 16), 32 | resolv.NewRectangleFromTopLeft(0, 16, 16, 360-16), 33 | resolv.NewRectangleFromTopLeft(640-16, 16, 16, 360-16), 34 | resolv.NewRectangleFromTopLeft(64, 128, 16, 200), 35 | resolv.NewRectangleFromTopLeft(120, 300, 200, 8), 36 | 37 | resolv.NewLine(256-32, 180-32, 256+32, 180+32), 38 | resolv.NewLine(256-32, 180+32, 256+32, 180-32), 39 | 40 | resolv.NewCircle(128, 128, 64), 41 | } 42 | 43 | solids.SetTags(TagSolidWall | TagPlatform) 44 | 45 | w.space.Add(solids...) 46 | 47 | w.Circle = NewCircle(w) 48 | 49 | } 50 | 51 | func (w *WorldCircle) Update() { 52 | w.Circle.Update() 53 | } 54 | 55 | func (w *WorldCircle) Draw(screen *ebiten.Image) { 56 | CommonDraw(screen, w) 57 | if GlobalGame.ShowHelpText { 58 | GlobalGame.DrawText(screen, 0, 128, 59 | "Circle Movement Test", 60 | "Arrow keys to move", 61 | ) 62 | } 63 | } 64 | 65 | // To allow the world's physical state to be drawn using the debug draw function. 66 | func (w *WorldCircle) Space() *resolv.Space { 67 | return w.space 68 | } 69 | 70 | type Circle struct { 71 | Object *resolv.Circle 72 | Movement resolv.Vector 73 | } 74 | 75 | func NewCircle(world *WorldCircle) *Circle { 76 | 77 | circle := &Circle{ 78 | Object: resolv.NewCircle(320, 64, 8), 79 | } 80 | circle.Object.Tags().Set(TagPlayer) 81 | circle.Object.SetData(circle) 82 | 83 | world.space.Add(circle.Object) 84 | return circle 85 | 86 | } 87 | 88 | func (c *Circle) Update() { 89 | 90 | movement := resolv.NewVectorZero() 91 | maxSpd := 4.0 92 | friction := 0.5 93 | accel := 0.5 + friction 94 | 95 | if ebiten.IsKeyPressed(ebiten.KeyLeft) { 96 | movement.X -= 1 97 | } 98 | if ebiten.IsKeyPressed(ebiten.KeyRight) { 99 | movement.X += 1 100 | } 101 | 102 | if ebiten.IsKeyPressed(ebiten.KeyUp) { 103 | movement.Y -= 1 104 | } 105 | if ebiten.IsKeyPressed(ebiten.KeyDown) { 106 | movement.Y += 1 107 | } 108 | 109 | c.Movement = c.Movement.Add(movement.Scale(accel)).SubMagnitude(friction).ClampMagnitude(maxSpd) 110 | 111 | c.Object.MoveVec(c.Movement) 112 | 113 | c.Object.IntersectionTest(resolv.IntersectionTestSettings{ 114 | TestAgainst: c.Object.SelectTouchingCells(1).FilterShapes(), 115 | OnIntersect: func(set resolv.IntersectionSet) bool { 116 | c.Object.MoveVec(set.MTV) 117 | return true 118 | }, 119 | }) 120 | 121 | } 122 | -------------------------------------------------------------------------------- /examples/worldPlatformer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/hajimehoshi/ebiten/v2/inpututil" 6 | "github.com/solarlune/resolv" 7 | ) 8 | 9 | var ( 10 | TagPlayer = resolv.NewTag("Player") 11 | TagSolidWall = resolv.NewTag("SolidWall") 12 | TagPlatform = resolv.NewTag("Platform") 13 | ) 14 | 15 | type WorldPlatformer struct { 16 | space *resolv.Space 17 | Player *PlatformingPlayer 18 | MovingPlatform *resolv.ConvexPolygon 19 | PlatformMovingUp bool 20 | } 21 | 22 | func NewWorldPlatformer() *WorldPlatformer { 23 | // Create the world. 24 | w := &WorldPlatformer{} 25 | // Initialize it. 26 | w.Init() 27 | return w 28 | } 29 | 30 | func (w *WorldPlatformer) Init() { 31 | 32 | // Create the space. It is 640x360 large (the size of the screen), and divided into 16x16 cells. 33 | // The cell division makes it more efficient to check for shapes. 34 | w.space = resolv.NewSpace(640, 360, 16, 16) 35 | 36 | solids := resolv.ShapeCollection{ 37 | resolv.NewCircle(128, 128, 32), 38 | 39 | resolv.NewRectangleFromTopLeft(0, 0, 640, 8), 40 | resolv.NewRectangleFromTopLeft(640-8, 8, 8, 360-16), 41 | 42 | resolv.NewRectangleFromTopLeft(0, 8, 8, 360-32), 43 | resolv.NewRectangleFromTopLeft(0, 360-8-16, 8, 8), 44 | resolv.NewRectangleFromTopLeft(0, 360-8-8, 8, 8), 45 | 46 | resolv.NewRectangleFromTopLeft(64, 200, 300, 8), 47 | resolv.NewRectangleFromTopLeft(64, 280, 300, 8), 48 | resolv.NewRectangleFromTopLeft(512, 96, 32, 200), 49 | 50 | resolv.NewRectangleFromTopLeft(0, 360-8, 640, 8), 51 | } 52 | 53 | solids.SetTags(TagSolidWall | TagPlatform) 54 | 55 | w.space.Add(solids...) 56 | 57 | ///// 58 | 59 | platforms := resolv.ShapeCollection{ 60 | resolv.NewRectangleFromTopLeft(400, 200, 32, 16), 61 | resolv.NewRectangleFromTopLeft(400, 240, 32, 16), 62 | resolv.NewRectangleFromTopLeft(400, 280, 32, 16), 63 | resolv.NewRectangleFromTopLeft(400, 320, 32, 16), 64 | } 65 | 66 | platforms.SetTags(TagPlatform) 67 | 68 | w.space.Add(platforms...) 69 | 70 | //// 71 | 72 | w.Player = NewPlayer(w.space) 73 | 74 | ramp := resolv.NewConvexPolygon(180, 175, 75 | []float64{ 76 | -24, 8, 77 | 8, -8, 78 | 48, -8, 79 | 80, 8, 80 | }, 81 | ) 82 | ramp.Tags().Set(TagPlatform | TagSolidWall) 83 | w.space.Add(ramp) 84 | 85 | // Clone and move the Ramp, then place it again 86 | r := ramp.Clone() 87 | r.SetPositionVec(resolv.NewVector(240, 344)) 88 | w.space.Add(r) 89 | 90 | w.MovingPlatform = resolv.NewRectangleFromTopLeft(550, 200, 32, 8) 91 | w.MovingPlatform.Tags().Set(TagPlatform) 92 | w.space.Add(w.MovingPlatform) 93 | 94 | } 95 | 96 | func (w *WorldPlatformer) Update() { 97 | 98 | movingPlatformSpeed := 2.0 99 | 100 | if w.PlatformMovingUp { 101 | w.MovingPlatform.Move(0, -movingPlatformSpeed) 102 | } else { 103 | w.MovingPlatform.Move(0, movingPlatformSpeed) 104 | } 105 | 106 | if w.MovingPlatform.Position().Y <= 200 || w.MovingPlatform.Position().Y > 300 { 107 | w.PlatformMovingUp = !w.PlatformMovingUp 108 | } 109 | 110 | w.Player.Update() 111 | 112 | } 113 | 114 | func (w *WorldPlatformer) Draw(screen *ebiten.Image) { 115 | CommonDraw(screen, w) 116 | if GlobalGame.ShowHelpText { 117 | GlobalGame.DrawText(screen, 0, 128, 118 | "Platformer Test", 119 | "Left and right arrow keys to move", 120 | "X to jump", 121 | "You can walljump", 122 | "Orange platforms can be jumped through", 123 | ) 124 | } 125 | } 126 | 127 | // To allow the world's physical state to be drawn using the debug draw function. 128 | func (w *WorldPlatformer) Space() *resolv.Space { 129 | return w.space 130 | } 131 | 132 | type PlatformingPlayer struct { 133 | Object *resolv.ConvexPolygon 134 | Movement resolv.Vector 135 | Facing resolv.Vector 136 | YSpeed float64 137 | Space *resolv.Space 138 | 139 | OnGround bool 140 | WallSliding bool 141 | } 142 | 143 | func NewPlayer(space *resolv.Space) *PlatformingPlayer { 144 | 145 | player := &PlatformingPlayer{ 146 | Object: resolv.NewRectangle(192, 128, 16, 12), 147 | Space: space, 148 | } 149 | player.Object.Tags().Set(TagPlayer) 150 | space.Add(player.Object) 151 | return player 152 | 153 | } 154 | 155 | func (p *PlatformingPlayer) Update() { 156 | 157 | moveVec := resolv.Vector{} 158 | gravity := 0.5 159 | friction := 0.2 160 | accel := 0.5 + friction 161 | maxSpd := 4.0 162 | jumpSpd := 8.0 163 | 164 | if !p.WallSliding { 165 | 166 | // Only move if you're not on the wall 167 | p.YSpeed += gravity 168 | 169 | if ebiten.IsKeyPressed(ebiten.KeyLeft) { 170 | moveVec.X -= accel 171 | } 172 | 173 | if ebiten.IsKeyPressed(ebiten.KeyRight) { 174 | moveVec.X += accel 175 | } 176 | 177 | } else { 178 | p.YSpeed = 0.2 // Slide down the wall slowly 179 | } 180 | 181 | // Jumping 182 | if inpututil.IsKeyJustPressed(ebiten.KeyX) { 183 | 184 | // Jump either if you're on the ground or on a wall 185 | if p.OnGround || p.WallSliding { 186 | p.YSpeed = -jumpSpd 187 | } 188 | 189 | // Jump away from the wall 190 | if p.WallSliding { 191 | if p.Facing.X < 0 { 192 | moveVec.X = maxSpd 193 | } else { 194 | moveVec.X = -maxSpd 195 | } 196 | p.WallSliding = false 197 | } 198 | 199 | } 200 | 201 | // Set the facing if the player's not on a wall and attempting to move 202 | if !p.WallSliding && !moveVec.IsZero() { 203 | p.Facing = moveVec.Unit() 204 | } 205 | 206 | // Add in the player's movement, clamping it to the maximum speed and incorporating friction. 207 | p.Movement = p.Movement.Add(moveVec).ClampMagnitude(maxSpd).SubMagnitude(friction) 208 | 209 | // Filter out shapes that are nearby the player 210 | nearbyShapes := p.Object.SelectTouchingCells(4).FilterShapes() 211 | 212 | p.OnGround = false 213 | 214 | checkVec := resolv.NewVector(0, p.YSpeed) // Check downwards by the distance of movement speed 215 | 216 | if p.YSpeed >= 0 { 217 | checkVec.Y += 4 // Add in a bit of extra downwards cast to account for running down ramps if we're not jumping 218 | } 219 | 220 | // Snap to ground using a shape-based line test. 221 | p.Object.ShapeLineTest(resolv.ShapeLineTestSettings{ 222 | Vector: checkVec, 223 | TestAgainst: nearbyShapes.ByTags(TagPlatform), // Select the shapes near the player object that are platforms 224 | OnIntersect: func(set resolv.IntersectionSet, index, max int) bool { 225 | 226 | if p.YSpeed >= 0 && set.Intersections[0].Normal.Y < 0 { 227 | // If we're falling and landing on upward facing line 228 | 229 | p.OnGround = true // Then set on ground to true 230 | p.WallSliding = false // And wallsliding to false 231 | p.YSpeed = 0 // Stop vertical movement 232 | p.Object.MoveVec(set.MTV.SubY(2)) // Move to contact plus a bit of floating to not be flush with the ground so running up ramps is easier 233 | return false // We can stop iterating past this 234 | 235 | } else if set.Intersections[0].Normal.Y > 0 && p.YSpeed < 0 && set.OtherShape.Tags().Has(TagSolidWall) { 236 | // Jumping and bonking on downward-facing line and it's solid 237 | p.YSpeed = 0 238 | p.Object.MoveVec(set.MTV) 239 | return false // We can stop iterating past this. 240 | } 241 | 242 | // No ground or ceiling, so keep looking for collisions 243 | return true 244 | 245 | }, 246 | }) 247 | 248 | // Apply movement - Y speed is separate so that gravity can take effect separate from horizontal movement speed 249 | p.Object.Move(p.Movement.X, p.Movement.Y+p.YSpeed) 250 | 251 | // Collision test first 252 | 253 | wallslideSet := false 254 | 255 | p.Object.IntersectionTest(resolv.IntersectionTestSettings{ 256 | 257 | // Check shapes in surrounding cells that have the "TagSolidWall" tag. 258 | TestAgainst: nearbyShapes.ByTags(TagSolidWall), 259 | 260 | OnIntersect: func(set resolv.IntersectionSet) bool { 261 | 262 | // If we're not on the ground and attempting to move and the touched nearest contact in the contact set's normal is opposite 263 | // the direction we're attempting to move (i.e. moving left into a right-facing wall), then it's a wall-slide candidate. 264 | if !wallslideSet && !p.OnGround && !moveVec.IsZero() && set.Intersections[0].Normal.Dot(moveVec) < 0 { 265 | 266 | // If we just barely clip a wall, that shouldn't count. We want to touch at least half the height of the player's worth of wall to enter wall-slide mode 267 | if dist := set.Distance(resolv.WorldUp); dist >= p.Object.Bounds().Height() && p.YSpeed >= -2 { 268 | 269 | // Set the wall as the object we're wallsliding against 270 | p.WallSliding = true 271 | 272 | // Stop movement 273 | p.Movement.X = 0 274 | 275 | // Set wallslide to true so that we don't have to check any other objects. 276 | wallslideSet = true 277 | 278 | } 279 | 280 | } 281 | 282 | // Move away from whatever is struck. 283 | p.Object.MoveVec(set.MTV) 284 | 285 | // Keep iterating in case we're touching something else. 286 | return true 287 | }, 288 | }) 289 | 290 | // We can also use an IntersectionTest / ShapeLineTest / LineTest function as a bool if we don't need to do anything special on any particular "one" 291 | if !p.Object.ShapeLineTest(resolv.ShapeLineTestSettings{ 292 | Vector: p.Facing, 293 | TestAgainst: nearbyShapes.ByTags(TagSolidWall), 294 | }) { 295 | p.WallSliding = false 296 | } 297 | 298 | } 299 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/solarlune/resolv 2 | 3 | go 1.20 // Minimum necessary to use generic set 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolarLune/resolv/8b4e8c15ba3b6428f976ddb2d56bbe04b719a8fe/go.sum -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolarLune/resolv/8b4e8c15ba3b6428f976ddb2d56bbe04b719a8fe/logo.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 | # Resolv v0.8.0 7 | 8 | [pkg.go.dev](https://pkg.go.dev/github.com/solarlune/resolv) 9 | 10 | ## What is Resolv? 11 | 12 | Resolv is a 2D collision detection and resolution library, specifically created for simpler, arcade-y (non-realistic) video games. Resolv is written in pure Go, but the core concepts are fairly straightforward and could be easily adapted for use with other languages or game development frameworks. 13 | 14 | Basically: It allows you to do simple physics easier, without actually _doing_ the physics part - that's still on you and your game's use-case. 15 | 16 | ## Why is it called that? 17 | 18 | Because it's like... You know, collision resolution? To **resolve** a collision? So... That's the name. I juste seem to have misplaced the "e", so I couldn't include it in the name - how odd. 19 | 20 | ## Why did you create Resolv? 21 | 22 | Because I was making games in Go and found that existing frameworks tend to omit collision testing and resolution code. Collision testing isn't too hard, but it's done frequently enough, and most games need simple enough physics that it makes sense to make a library to handle collision testing and resolution for simple, "arcade-y" games; if you need realistic physics, you have other options like [cp](https://github.com/jakecoffman/cp) or [Box2D](https://github.com/ByteArena/box2d). 23 | 24 | ____ 25 | 26 | As an aside, this actually used to be quite different; I decided to rework it a couple of times. This is now the second rework, and should be _significantly_ easier to use and more accurate. (Thanks a lot to everyone who contributed their time to submit PRs and issues!) 27 | 28 | It's still not _totally_ complete, but it should be solid enough for usage in the field. 29 | 30 | ## Dependencies? 31 | 32 | Resolv has no external dependencies. It requires Go 1.20 or above. 33 | 34 | ## How do I get it? 35 | 36 | `go get github.com/solarlune/resolv` 37 | 38 | ## How do I use it? 39 | 40 | There's a couple of ways to use Resolv. One way is to just create Shapes then use functions to check for intersections. 41 | 42 | ```go 43 | func main() { 44 | 45 | // Create a rectangle at 200, 100 with a width and height of 32x32 46 | rect := resolv.NewRectangle(200, 100, 32, 32) 47 | 48 | // Create a circle at 200, 120 with a radius of 8 49 | circle := resolv.NewCircle(200, 120, 8) 50 | 51 | // Check for intersection 52 | if intersection, ok := rect.Intersection(circle); ok { 53 | fmt.Println("They're touching! Here's the data:", intersection) 54 | } 55 | 56 | } 57 | ``` 58 | 59 | You can also get the intersection with `Shape.Intersection(other)`. 60 | 61 | However, you'll probably want to check intersection with a larger group of objects, which you can do with `Spaces` and `ShapeFilters`. You create a Space, add Shapes to the space, and then call `Shape.IntersectionTest()` with more advanced settings: 62 | 63 | ```go 64 | 65 | type Game struct { 66 | Rect *resolv.ConvexPolygon 67 | Space *resolv.Space 68 | } 69 | 70 | func (g *Game) Init() { 71 | 72 | // Create a space that is 640x480 large and that has a cellular size of 16x16. The cell size is mainly used to 73 | // determine internally how close objects are together to qualify for intersection testing. Generally, this should 74 | // be the size of the maximum speed of your objects (i.e. objects shouldn't move faster than 1 cell in size each 75 | // frame). 76 | g.Space = resolv.NewSpace(640, 480, 16, 16) 77 | 78 | // Create a rectangle at 200, 100 with a width and height of 32x32 79 | g.Rect = resolv.NewRectangle(200, 100, 32, 32) 80 | 81 | // Create a circle at 200, 120 with a radius of 8 82 | circle := resolv.NewCircle(200, 120, 8) 83 | 84 | // Add the shapes to allow them to be detected by other Shapes. 85 | g.Space.Add(rect) 86 | g.Space.Add(circle) 87 | } 88 | 89 | func (g *Game) Update() { 90 | 91 | // Check for intersection and do something for each intersection 92 | g.Space.Rect.IntersectionTest(resolv.IntersectionTestSettings{ 93 | TestAgainst: rect.SelectTouchingCells(1).FilterShapes(), // Check only shapes that are near the rectangle (within 1 cell's margin) 94 | OnIntersect: func(set resolv.IntersectionSet, index, max int) bool { 95 | fmt.Println("There was an intersection with some other object! Here's the data:", set) 96 | return true 97 | } 98 | }) 99 | 100 | } 101 | 102 | ``` 103 | 104 | You can also do line tests and shape-based line tests, to see if there would be a collision in a given direction - this is more-so useful for movement and space checking. 105 | ___ 106 | 107 | If you want to see more info, feel free to examine the examples in the `examples` folder; the platformer example is particularly in-depth when it comes to movement and collision response. You can run them and switch between them just by calling `go run .` from the examples folder and pressing Q or E to switch between the example worlds. 108 | 109 | [You can check out the documentation here, as well.](https://pkg.go.dev/github.com/solarlune/resolv) 110 | 111 | ## To-do List 112 | 113 | - [x] Rewrite to be significantly easier and simpler 114 | - [ ] Allow for cells that are less than 1 unit large (and so Spaces can have cell sizes of, say, 0.1 units) 115 | - [x] Custom Vector struct for speed, consistency, and to reduce third-party imports 116 | - [ ] Implement Matrices as well for parenting? 117 | - [ ] Intersection MTV works properly for external normals, but not internal normals of a polygon 118 | - [ ] Properly implement moving around inside a circle (?) -------------------------------------------------------------------------------- /shape.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | ) 7 | 8 | // IShape represents an interface that all Shapes fulfill. 9 | type IShape interface { 10 | ID() uint32 // The unique ID of the Shape 11 | Clone() IShape 12 | Tags() *Tags 13 | 14 | Position() Vector 15 | SetPosition(x, y float64) 16 | SetPositionVec(vec Vector) 17 | 18 | Move(x, y float64) 19 | MoveVec(vec Vector) 20 | 21 | SetX(float64) 22 | SetY(float64) 23 | 24 | SelectTouchingCells(margin int) CellSelection 25 | 26 | update() 27 | 28 | SetData(data any) 29 | Data() any 30 | 31 | setSpace(space *Space) 32 | Space() *Space 33 | 34 | addToTouchingCells() 35 | removeFromTouchingCells() 36 | Bounds() Bounds 37 | 38 | IsLeftOf(other IShape) bool 39 | IsRightOf(other IShape) bool 40 | IsAbove(other IShape) bool 41 | IsBelow(other IShape) bool 42 | 43 | IntersectionTest(settings IntersectionTestSettings) bool 44 | IsIntersecting(other IShape) bool 45 | Intersection(other IShape) IntersectionSet 46 | 47 | VecTo(other IShape) Vector 48 | DistanceTo(other IShape) float64 49 | DistanceSquaredTo(other IShape) float64 50 | } 51 | 52 | // ShapeBase implements many of the common methods that Shapes need to implement to fulfill IShape 53 | // (but not the Shape-specific ones, like rotating for ConvexPolygons or setting the radius for Circles). 54 | type ShapeBase struct { 55 | position Vector 56 | space *Space 57 | touchingCells []*Cell 58 | tags *Tags 59 | data any // Data represents some helper data present on the shape. 60 | owner IShape // The owning shape; this allows ShapeBase to call overridden functions (i.e. owner.Bounds()). 61 | id uint32 62 | } 63 | 64 | var globalShapeID = uint32(0) 65 | 66 | func newShapeBase(x, y float64) ShapeBase { 67 | t := Tags(0) 68 | id := globalShapeID 69 | globalShapeID++ 70 | return ShapeBase{ 71 | position: NewVector(x, y), 72 | tags: &t, 73 | id: id, 74 | } 75 | } 76 | 77 | // ID returns the unique ID of the Shape. 78 | func (s *ShapeBase) ID() uint32 { 79 | return s.id 80 | } 81 | 82 | // Data returns any auxiliary data set on the shape. 83 | func (s *ShapeBase) Data() any { 84 | return s.data 85 | } 86 | 87 | // SetData sets any auxiliary data on the shape. 88 | func (s *ShapeBase) SetData(data any) { 89 | s.data = data 90 | } 91 | 92 | // Tags returns the tags applied to the shape. 93 | func (s *ShapeBase) Tags() *Tags { 94 | return s.tags 95 | } 96 | 97 | // Move translates the Shape by the designated X and Y values. 98 | func (s *ShapeBase) Move(x, y float64) { 99 | s.position.X += x 100 | s.position.Y += y 101 | s.update() 102 | } 103 | 104 | // MoveVec translates the ShapeBase by the designated Vector. 105 | func (s *ShapeBase) MoveVec(vec Vector) { 106 | s.Move(vec.X, vec.Y) 107 | } 108 | 109 | // Position() returns the X and Y position of the ShapeBase. 110 | func (s *ShapeBase) Position() Vector { 111 | return s.position 112 | } 113 | 114 | // SetPosition sets the center position of the ShapeBase using the X and Y values given. 115 | func (s *ShapeBase) SetPosition(x, y float64) { 116 | s.position.X = x 117 | s.position.Y = y 118 | s.update() 119 | } 120 | 121 | // SetPosition sets the center position of the ShapeBase using the Vector given. 122 | func (c *ShapeBase) SetPositionVec(vec Vector) { 123 | c.SetPosition(vec.X, vec.Y) 124 | } 125 | 126 | // SetX sets the X position of the Shape. 127 | func (c *ShapeBase) SetX(x float64) { 128 | pos := c.position 129 | pos.X = x 130 | c.SetPosition(pos.X, pos.Y) 131 | } 132 | 133 | // SetY sets the Y position of the Shape. 134 | func (c *ShapeBase) SetY(y float64) { 135 | pos := c.position 136 | pos.Y = y 137 | c.SetPosition(pos.X, pos.Y) 138 | } 139 | 140 | func (s *ShapeBase) Space() *Space { 141 | return s.space 142 | } 143 | 144 | func (s *ShapeBase) setSpace(space *Space) { 145 | s.space = space 146 | } 147 | 148 | // IsLeftOf returns true if the Shape is to the left of the other shape. 149 | func (s *ShapeBase) IsLeftOf(other IShape) bool { 150 | return s.owner.Bounds().Min.X < other.Bounds().Min.X 151 | } 152 | 153 | // IsRightOf returns true if the Shape is to the right of the other shape. 154 | func (s *ShapeBase) IsRightOf(other IShape) bool { 155 | return s.owner.Bounds().Max.X > other.Bounds().Max.X 156 | } 157 | 158 | // IsAbove returns true if the Shape is above the other shape. 159 | func (s *ShapeBase) IsAbove(other IShape) bool { 160 | return s.owner.Bounds().Min.Y < other.Bounds().Min.Y 161 | } 162 | 163 | // IsBelow returns true if the Shape is below the other shape. 164 | func (s *ShapeBase) IsBelow(other IShape) bool { 165 | return s.owner.Bounds().Max.Y > other.Bounds().Max.Y 166 | } 167 | 168 | // VecTo returns a vector from the given shape to the other Shape. 169 | func (s *ShapeBase) VecTo(other IShape) Vector { 170 | return s.position.Sub(other.Position()) 171 | } 172 | 173 | // DistanceSquaredTo returns the distance from the given shape's center to the other Shape. 174 | func (s *ShapeBase) DistanceTo(other IShape) float64 { 175 | return s.owner.Position().Distance(other.Position()) 176 | } 177 | 178 | // DistanceSquaredTo returns the squared distance from the given shape's center to the other Shape. 179 | func (s *ShapeBase) DistanceSquaredTo(other IShape) float64 { 180 | return s.owner.Position().DistanceSquared(other.Position()) 181 | } 182 | 183 | func (s *ShapeBase) removeFromTouchingCells() { 184 | for _, cell := range s.touchingCells { 185 | cell.unregister(s.owner) 186 | } 187 | 188 | s.touchingCells = s.touchingCells[:0] 189 | } 190 | 191 | func (s *ShapeBase) addToTouchingCells() { 192 | 193 | if s.space != nil { 194 | 195 | cx, cy, ex, ey := s.owner.Bounds().toCellSpace() 196 | 197 | for y := cy; y <= ey; y++ { 198 | 199 | for x := cx; x <= ex; x++ { 200 | 201 | cell := s.space.Cell(x, y) 202 | 203 | if cell != nil { 204 | cell.register(s.owner) 205 | s.touchingCells = append(s.touchingCells, cell) 206 | } 207 | 208 | } 209 | 210 | } 211 | 212 | } 213 | 214 | } 215 | 216 | // SelectTouchingCells returns a CellSelection of the cells in the Space that the Shape is touching. 217 | // margin sets the cellular margin - the higher the margin, the further away candidate Shapes can be to be considered for 218 | // collision. A margin of 1 is a good default. To help visualize which cells contain Shapes, it would be good to implement some kind of debug 219 | // drawing in your game, like can be seen in resolv's examples. 220 | func (s *ShapeBase) SelectTouchingCells(margin int) CellSelection { 221 | 222 | cx, cy, ex, ey := s.owner.Bounds().toCellSpace() 223 | 224 | cx -= margin 225 | cy -= margin 226 | ex += margin 227 | ey += margin 228 | 229 | return CellSelection{ 230 | StartX: cx, 231 | StartY: cy, 232 | EndX: ex, 233 | EndY: ey, 234 | space: s.space, 235 | excludeSelf: s.owner, 236 | } 237 | } 238 | 239 | func (s *ShapeBase) update() { 240 | s.removeFromTouchingCells() 241 | s.addToTouchingCells() 242 | } 243 | 244 | // IsIntersecting returns if the shape is intersecting with the other given Shape. 245 | func (s *ShapeBase) IsIntersecting(other IShape) bool { 246 | return !s.owner.Intersection(other).IsEmpty() 247 | } 248 | 249 | // IntersectionTestSettings is a struct that contains settings to control intersection tests. 250 | type IntersectionTestSettings struct { 251 | TestAgainst ShapeIterator // The collection of shapes to test against 252 | // OnIntersect is a callback to be called for each intersection found between the calling Shape and any of the other shapes given in TestAgainst. 253 | // The callback should be called in order of distance to the testing object. 254 | // Moving the object can influence whether it intersects with future surrounding objects. 255 | // set is the intersection set that contains information about the intersection. 256 | // The boolean the callback returns indicates whether the LineTest function should continue testing or stop at the currently found intersection. 257 | OnIntersect func(set IntersectionSet) bool 258 | } 259 | 260 | type possibleIntersection struct { 261 | Shape IShape 262 | Distance float64 263 | } 264 | 265 | var possibleIntersections []possibleIntersection 266 | 267 | // IntersectionTest tests to see if the calling shape intersects with shapes specified in 268 | // the given settings struct, checked in order of distance to the calling shape's center point. 269 | // Internally, the function checks to see what Shapes are nearby, and tests against them in order 270 | // of distance. If the testing Shape moves, then that will influence the result of testing future 271 | // Shapes in the current game frame. 272 | // If the test succeeds in finding at least one intersection, it returns true. 273 | func (s *ShapeBase) IntersectionTest(settings IntersectionTestSettings) bool { 274 | 275 | possibleIntersections = possibleIntersections[:0] 276 | 277 | settings.TestAgainst.ForEach(func(other IShape) bool { 278 | 279 | if other == s.owner { 280 | return true 281 | } 282 | 283 | possibleIntersections = append(possibleIntersections, possibleIntersection{ 284 | Shape: other, 285 | Distance: other.DistanceSquaredTo(s.owner), 286 | }) 287 | return true 288 | 289 | }) 290 | 291 | sort.Slice(possibleIntersections, func(i, j int) bool { 292 | return possibleIntersections[i].Distance < possibleIntersections[j].Distance 293 | }) 294 | 295 | collided := false 296 | 297 | for _, p := range possibleIntersections { 298 | 299 | result := s.owner.Intersection(p.Shape) 300 | 301 | if !result.IsEmpty() { 302 | collided = true 303 | if settings.OnIntersect != nil { 304 | if !settings.OnIntersect(result) { 305 | break 306 | } 307 | } else { 308 | break 309 | } 310 | } 311 | 312 | } 313 | 314 | return collided 315 | 316 | } 317 | 318 | func circleConvexTest(circle *Circle, convex *ConvexPolygon) IntersectionSet { 319 | 320 | intersectionSet := IntersectionSet{} 321 | 322 | if !convex.owner.Bounds().IsIntersecting(circle.Bounds()) { 323 | return intersectionSet 324 | } 325 | 326 | for _, line := range convex.Lines() { 327 | 328 | if res := line.IntersectionPointsCircle(circle); len(res) > 0 { 329 | 330 | for _, point := range res { 331 | intersectionSet.Intersections = append(intersectionSet.Intersections, Intersection{ 332 | Point: point, 333 | Normal: line.Normal(), 334 | }) 335 | } 336 | } 337 | 338 | } 339 | 340 | if !intersectionSet.IsEmpty() { 341 | 342 | intersectionSet.OtherShape = convex 343 | 344 | // No point in sorting circle -> convex intersection tests because the circle's center is necessarily equidistant from any and all points of intersection 345 | 346 | if mtv, ok := convex.calculateMTV(circle); ok { 347 | intersectionSet.MTV = mtv.Invert() 348 | } 349 | 350 | } 351 | 352 | return intersectionSet 353 | 354 | } 355 | 356 | func convexCircleTest(convex *ConvexPolygon, circle *Circle) IntersectionSet { 357 | 358 | intersectionSet := IntersectionSet{} 359 | 360 | if !convex.owner.Bounds().IsIntersecting(circle.Bounds()) { 361 | return intersectionSet 362 | } 363 | 364 | for _, line := range convex.Lines() { 365 | 366 | res := line.IntersectionPointsCircle(circle) 367 | 368 | if len(res) > 0 { 369 | 370 | for _, point := range res { 371 | intersectionSet.Intersections = append(intersectionSet.Intersections, Intersection{ 372 | Point: point, 373 | Normal: point.Sub(circle.position).Unit(), 374 | }) 375 | } 376 | 377 | } 378 | 379 | } 380 | 381 | if !intersectionSet.IsEmpty() { 382 | 383 | intersectionSet.OtherShape = circle 384 | 385 | sort.Slice(intersectionSet.Intersections, func(i, j int) bool { 386 | return intersectionSet.Intersections[i].Point.DistanceSquared(circle.position) < intersectionSet.Intersections[j].Point.DistanceSquared(circle.position) 387 | }) 388 | 389 | if mtv, ok := convex.calculateMTV(circle); ok { 390 | intersectionSet.MTV = mtv 391 | } 392 | 393 | } 394 | 395 | return intersectionSet 396 | 397 | } 398 | 399 | func circleCircleTest(circleA, circleB *Circle) IntersectionSet { 400 | 401 | intersectionSet := IntersectionSet{} 402 | 403 | if !circleA.owner.Bounds().IsIntersecting(circleB.Bounds()) { 404 | return intersectionSet 405 | } 406 | 407 | d := math.Sqrt(math.Pow(circleB.position.X-circleA.position.X, 2) + math.Pow(circleB.position.Y-circleA.position.Y, 2)) 408 | 409 | if d > circleA.radius+circleB.radius || d < math.Abs(circleA.radius-circleB.radius) || d == 0 { 410 | return intersectionSet 411 | } 412 | 413 | a := (math.Pow(circleA.radius, 2) - math.Pow(circleB.radius, 2) + math.Pow(d, 2)) / (2 * d) 414 | h := math.Sqrt(math.Pow(circleA.radius, 2) - math.Pow(a, 2)) 415 | 416 | x2 := circleA.position.X + a*(circleB.position.X-circleA.position.X)/d 417 | y2 := circleA.position.Y + a*(circleB.position.Y-circleA.position.Y)/d 418 | 419 | intersectionSet.Intersections = []Intersection{ 420 | {Point: Vector{x2 + h*(circleB.position.Y-circleA.position.Y)/d, y2 - h*(circleB.position.X-circleA.position.X)/d}}, 421 | {Point: Vector{x2 - h*(circleB.position.Y-circleA.position.Y)/d, y2 + h*(circleB.position.X-circleA.position.X)/d}}, 422 | } 423 | 424 | for i := range intersectionSet.Intersections { 425 | intersectionSet.Intersections[i].Normal = intersectionSet.Intersections[i].Point.Sub(circleA.position).Unit() 426 | } 427 | 428 | intersectionSet.MTV = Vector{circleA.position.X - circleB.position.X, circleA.position.Y - circleB.position.Y} 429 | dist := intersectionSet.MTV.Magnitude() 430 | intersectionSet.MTV = intersectionSet.MTV.Unit().Scale(circleA.radius + circleB.radius - dist) 431 | 432 | intersectionSet.OtherShape = circleB 433 | 434 | return intersectionSet 435 | 436 | } 437 | 438 | func convexConvexTest(convexA, convexB *ConvexPolygon) IntersectionSet { 439 | 440 | intersectionSet := IntersectionSet{} 441 | 442 | if !convexA.owner.Bounds().IsIntersecting(convexB.Bounds()) { 443 | return intersectionSet 444 | } 445 | 446 | for _, otherLine := range convexB.Lines() { 447 | 448 | for _, line := range convexA.Lines() { 449 | 450 | if point, ok := line.IntersectionPointsLine(otherLine); ok { 451 | intersectionSet.Intersections = append(intersectionSet.Intersections, Intersection{ 452 | Point: point, 453 | Normal: otherLine.Normal(), 454 | }) 455 | } 456 | 457 | } 458 | 459 | } 460 | 461 | if !intersectionSet.IsEmpty() { 462 | 463 | intersectionSet.OtherShape = convexB 464 | 465 | center := convexA.Center() 466 | 467 | sort.Slice(intersectionSet.Intersections, func(i, j int) bool { 468 | return intersectionSet.Intersections[i].Point.DistanceSquared(center) < intersectionSet.Intersections[j].Point.DistanceSquared(center) 469 | }) 470 | 471 | if mtv, ok := convexA.calculateMTV(convexB); ok { 472 | intersectionSet.MTV = mtv 473 | } 474 | 475 | } 476 | 477 | return intersectionSet 478 | 479 | } 480 | -------------------------------------------------------------------------------- /shapefilter.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | ) 7 | 8 | // ShapeIterator is an interface that defines a method to iterate through Shapes. 9 | // Any object that has such a function (e.g. a ShapeFilter or a ShapeCollection (which is essentially just a slice of Shapes)) fulfills the ShapeIterator interface. 10 | type ShapeIterator interface { 11 | // ForEach is a function that can iterate through a collection of Shapes, controlled by a function's return value. 12 | // If the function returns true, the iteration continues to the end. If it returns false, the iteration ends. 13 | ForEach(iterationFunction func(shape IShape) bool) 14 | } 15 | 16 | // ShapeFilter is a selection of Shapes, primarily used to filter them out to select only some (i.e. Shapes with specific tags or placement). 17 | // Usually one would use a ShapeFilter to select Shapes that are near a moving Shape (e.g. the player character). 18 | type ShapeFilter struct { 19 | Filters []func(s IShape) bool 20 | operatingOn ShapeIterator 21 | } 22 | 23 | // ForEach is a function that can run a customizeable function on each Shape contained within the filter. 24 | // If the shape passes the filters, the forEachFunc will run with the shape as an argument. 25 | // If the function returns true, the iteration will continue; if it doesn't, the iteration will end. 26 | func (s ShapeFilter) ForEach(forEachFunc func(shape IShape) bool) { 27 | 28 | s.operatingOn.ForEach(func(shape IShape) bool { 29 | 30 | for _, f := range s.Filters { 31 | if !f(shape) { 32 | return true 33 | } 34 | } 35 | 36 | if !forEachFunc(shape) { 37 | return true 38 | } 39 | 40 | return true 41 | }) 42 | 43 | } 44 | 45 | // ByTags adds a filter to the ShapeFilter that filters out Shapes by tags (so only Shapes that have the specified Tag(s) pass the filter). 46 | // The function returns the ShapeFiler for easy method chaining. 47 | func (s ShapeFilter) ByTags(tags Tags) ShapeFilter { 48 | s.Filters = append(s.Filters, func(s IShape) bool { 49 | return s.Tags().Has(tags) 50 | }) 51 | return s 52 | } 53 | 54 | // NotByTags adds a filter to the ShapeFilter that filters out Shapes by tags (so only Shapes that DO NOT have the specified Tag(s) pass the filter). 55 | // The function returns the ShapeFiler for easy method chaining. 56 | func (s ShapeFilter) NotByTags(tags Tags) ShapeFilter { 57 | s.Filters = append(s.Filters, func(s IShape) bool { 58 | return !s.Tags().Has(tags) 59 | }) 60 | return s 61 | } 62 | 63 | // ByDistance adds a filter to the ShapeFilter that filters out Shapes distance to a given point. 64 | // The shapes have to be at least min and at most max distance from the given point Vector. 65 | // The function returns the ShapeFiler for easy method chaining. 66 | func (s ShapeFilter) ByDistance(point Vector, min, max float64) ShapeFilter { 67 | s.Filters = append(s.Filters, func(s IShape) bool { 68 | d := s.Position().Distance(point) 69 | return d > min && d < max 70 | }) 71 | return s 72 | } 73 | 74 | // ByFunc adds a filter to the ShapeFilter that filters out Shapes using a function if it returns true, the Shape passes the ShapeFilter. 75 | // The function returns the ShapeFiler for easy method chaining. 76 | func (s ShapeFilter) ByFunc(filterFunc func(s IShape) bool) ShapeFilter { 77 | s.Filters = append(s.Filters, filterFunc) 78 | return s 79 | } 80 | 81 | // ByDataType allows you to filter Shapes by their Data pointer's type. You could use this to, for example, filter out Shapes that have 82 | // Data objects that are Updatable, where `Updatable` is an interface that has an `Update()` function call. 83 | // To do this, you would call `s.ByDataType(reflect.TypeFor[Updatable]())` 84 | func (s ShapeFilter) ByDataType(dataType reflect.Type) ShapeFilter { 85 | if dataType == nil { 86 | return s 87 | } 88 | s.Filters = append(s.Filters, func(s IShape) bool { 89 | if s.Data() != nil { 90 | return reflect.TypeOf(s.Data()).Implements(dataType) 91 | } 92 | return false 93 | }) 94 | return s 95 | } 96 | 97 | // Not adds a filter to the ShapeFilter that specifcally does not allow specified Shapes in. 98 | // The function returns the ShapeFiler for easy method chaining. 99 | func (s ShapeFilter) Not(shapes ...IShape) ShapeFilter { 100 | s.Filters = append(s.Filters, func(s IShape) bool { 101 | for _, shape := range shapes { 102 | if shape == s { 103 | return false 104 | } 105 | } 106 | return true 107 | }) 108 | return s 109 | } 110 | 111 | // Shapes returns all shapes that pass the filters as a ShapeCollection. 112 | func (s ShapeFilter) Shapes() ShapeCollection { 113 | 114 | collection := ShapeCollection{} 115 | 116 | s.ForEach(func(shape IShape) bool { 117 | collection = append(collection, shape) 118 | return true 119 | }) 120 | 121 | return collection 122 | } 123 | 124 | // First returns the first shape that passes the ShapeFilter. 125 | func (s ShapeFilter) First() IShape { 126 | var returnShape IShape 127 | 128 | s.ForEach(func(shape IShape) bool { 129 | returnShape = shape 130 | return false 131 | }) 132 | 133 | return returnShape 134 | } 135 | 136 | // Last returns the last shape that passes the ShapeFilter (which means it has to step through all possible options before returning the last one). 137 | func (s ShapeFilter) Last() IShape { 138 | var returnShape IShape 139 | 140 | s.ForEach(func(shape IShape) bool { 141 | returnShape = shape 142 | return true 143 | }) 144 | 145 | return returnShape 146 | } 147 | 148 | // First returns the first shape in the ShapeCollection. 149 | func (s ShapeCollection) First() IShape { 150 | if len(s) > 0 { 151 | return s[0] 152 | } 153 | return nil 154 | } 155 | 156 | // Last returns the last shape in the ShapeCollection. 157 | func (s ShapeCollection) Last() IShape { 158 | if len(s) > 0 { 159 | return s[len(s)-1] 160 | } 161 | return nil 162 | } 163 | 164 | // Count returns the number of shapes that pass the filters as a ShapeCollection. 165 | func (s ShapeFilter) Count() int { 166 | 167 | count := 0 168 | 169 | s.ForEach(func(shape IShape) bool { 170 | count++ 171 | return true 172 | }) 173 | 174 | return count 175 | } 176 | 177 | // ShapeCollection is a slice of Shapes. 178 | type ShapeCollection []IShape 179 | 180 | // ForEach allows you to iterate through each shape in the ShapeCollection; if the function returns false, the iteration ends. 181 | func (s ShapeCollection) ForEach(forEachFunc func(shape IShape) bool) { 182 | for _, shape := range s { 183 | if !forEachFunc(shape) { 184 | break 185 | } 186 | } 187 | } 188 | 189 | // SetTags sets the tag(s) on all Shapes present in the Shapecollection. 190 | func (s ShapeCollection) SetTags(tags Tags) { 191 | for _, shape := range s { 192 | shape.Tags().Set(tags) 193 | } 194 | } 195 | 196 | // UnsetTags unsets the tag(s) on all Shapes present in the Shapecollection. 197 | func (s ShapeCollection) UnsetTags(tags Tags) { 198 | for _, shape := range s { 199 | shape.Tags().Unset(tags) 200 | } 201 | } 202 | 203 | // SortByDistance sorts the ShapeCollection by distance to the given point. 204 | func (s ShapeCollection) SortByDistance(point Vector) { 205 | sort.Slice(s, func(i, j int) bool { 206 | return s[i].Position().DistanceSquared(point) < s[j].Position().DistanceSquared(point) 207 | }) 208 | } 209 | -------------------------------------------------------------------------------- /space.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import "math" 4 | 5 | // Space represents a collision space. Internally, each Space contains a 2D array of Cells, with each Cell being the same size. Cells contain information on which 6 | // Shapes occupy those spaces and are used to speed up intersection testing across multiple Shapes that could be in dynamic locations. 7 | type Space struct { 8 | cells [][]*Cell // The cells present in the Space 9 | shapes ShapeCollection 10 | cellWidth, cellHeight int // Width and Height of each Cell in "world-space" / pixels / whatever 11 | } 12 | 13 | // NewSpace creates a new Space. spaceWidth and spaceHeight is the width and height of the Space (usually in pixels), which is then populated with cells of size 14 | // cellWidth by cellHeight. Generally, you want cells to be the size of a "normal object". 15 | // You want to move Objects at a maximum speed of one cell size per collision check to avoid missing any possible collisions. 16 | func NewSpace(spaceWidth, spaceHeight, cellWidth, cellHeight int) *Space { 17 | 18 | sp := &Space{ 19 | cellWidth: cellWidth, 20 | cellHeight: cellHeight, 21 | } 22 | 23 | sp.Resize(int(math.Ceil(float64(spaceWidth)/float64(cellWidth))), int(math.Ceil(float64(spaceHeight)/float64(cellHeight)))) 24 | 25 | // sp.Resize(int(math.Ceil(float64(spaceWidth)/float64(cellWidth))), 26 | // int(math.Ceil(float64(spaceHeight)/float64(cellHeight)))) 27 | 28 | return sp 29 | 30 | } 31 | 32 | // Add adds the specified Objects to the Space, updating the Space's cells to refer to the Object. 33 | func (s *Space) Add(shapes ...IShape) { 34 | 35 | for _, shape := range shapes { 36 | 37 | shape.setSpace(s) 38 | 39 | // We call Update() once to make sure the object gets its cells added. 40 | shape.update() 41 | 42 | } 43 | 44 | s.shapes = append(s.shapes, shapes...) 45 | 46 | } 47 | 48 | // Remove removes the specified Shapes from the Space. 49 | // This should be done whenever a game object (and its Shape) is removed from the game. 50 | func (s *Space) Remove(shapes ...IShape) { 51 | 52 | for _, shape := range shapes { 53 | 54 | shape.removeFromTouchingCells() 55 | 56 | for i, o := range s.shapes { 57 | if o == shape { 58 | s.shapes[i] = nil 59 | s.shapes = append(s.shapes[:i], s.shapes[i+1:]...) 60 | break 61 | } 62 | } 63 | 64 | } 65 | 66 | } 67 | 68 | // RemoveAll removes all Shapes from the Space (and from its internal Cells). 69 | func (s *Space) RemoveAll() { 70 | 71 | for i := range s.shapes { 72 | s.shapes[i] = nil 73 | } 74 | 75 | s.shapes = s.shapes[:0] 76 | for y := range s.cells { 77 | for x := range s.cells[y] { 78 | for i := range s.cells[y][x].Shapes { 79 | s.cells[y][x].Shapes[i] = nil 80 | } 81 | s.cells[y][x].Shapes = s.cells[y][x].Shapes[:0] 82 | } 83 | } 84 | 85 | } 86 | 87 | // Shapes returns a new slice consisting of all of the shapes present in the Space. 88 | func (s *Space) Shapes() ShapeCollection { 89 | return append(make(ShapeCollection, 0, len(s.shapes)), s.shapes...) 90 | } 91 | 92 | // ForEachShape iterates through each shape in the Space and runs the provided function on them, passing the Shape, its index in the 93 | // Space's shapes slice, and the maximum number of shapes in the space. 94 | // If the function returns false, the iteration ends. If it returns true, it continues. 95 | func (s *Space) ForEachShape(forEach func(shape IShape, index, maxCount int) bool) { 96 | 97 | for i, o := range s.shapes { 98 | if !forEach(o, i, len(s.shapes)) { 99 | break 100 | } 101 | } 102 | 103 | } 104 | 105 | // FilterShapes returns a ShapeFilter consisting of all shapes present in the Space. 106 | func (s *Space) FilterShapes() ShapeFilter { 107 | return ShapeFilter{ 108 | operatingOn: s.shapes, 109 | } 110 | } 111 | 112 | // Resize resizes the internal Cells array. 113 | func (s *Space) Resize(width, height int) { 114 | 115 | s.cells = [][]*Cell{} 116 | 117 | for y := 0; y < height; y++ { 118 | 119 | s.cells = append(s.cells, []*Cell{}) 120 | 121 | for x := 0; x < width; x++ { 122 | s.cells[y] = append(s.cells[y], newCell(x, y)) 123 | } 124 | 125 | } 126 | 127 | for _, s := range s.shapes { 128 | s.update() 129 | } 130 | 131 | } 132 | 133 | // Cell returns the Cell at the given cellular / spatial (not world) X and Y position in the Space. If the X and Y position are 134 | // out of bounds, Cell() will return nil. This does not flush shape vicinities beforehand. 135 | func (s *Space) Cell(cx, cy int) *Cell { 136 | 137 | if cy >= 0 && cy < len(s.cells) && cx >= 0 && cx < len(s.cells[cy]) { 138 | return s.cells[cy][cx] 139 | } 140 | return nil 141 | 142 | } 143 | 144 | // Width returns the spacial width of the Space grid in world coordinates. 145 | func (s *Space) Width() int { 146 | if len(s.cells) > 0 { 147 | return len(s.cells[0]) * s.cellWidth 148 | } 149 | return 0 150 | } 151 | 152 | // Height returns the spacial height of the Space grid in world coordinates. 153 | func (s *Space) Height() int { 154 | return len(s.cells) * s.cellHeight 155 | } 156 | 157 | // HeightInCells returns the height of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a HeightInCells() of 15). 158 | func (s *Space) HeightInCells() int { 159 | return len(s.cells) 160 | } 161 | 162 | // WidthInCells returns the width of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a WidthInCells() of 20). 163 | func (s *Space) WidthInCells() int { 164 | if len(s.cells) > 0 { 165 | return len(s.cells[0]) 166 | } 167 | return 0 168 | } 169 | 170 | // CellWidth returns the width of each cell in the Space. 171 | func (s *Space) CellWidth() int { 172 | return s.cellWidth 173 | } 174 | 175 | // CellHeight returns the height of each cell in the Space. 176 | func (s *Space) CellHeight() int { 177 | return s.cellHeight 178 | } 179 | 180 | // FilterCells selects a selection of cells. 181 | func (s *Space) FilterCells(bounds Bounds) CellSelection { 182 | 183 | bounds.space = s 184 | 185 | fx, fy, fx2, fy2 := bounds.toCellSpace() 186 | 187 | return CellSelection{ 188 | space: s, 189 | StartX: fx, 190 | StartY: fy, 191 | EndX: fx2, 192 | EndY: fy2, 193 | } 194 | 195 | } 196 | 197 | // func (s *Space) FilterCellsInLine(start, end Vector) CellSelection { 198 | 199 | // cells := CellSelection{} 200 | 201 | // startX := int(math.Floor(start.X / float64(s.CellWidth))) 202 | // startY := int(math.Floor(start.Y / float64(s.CellHeight))) 203 | 204 | // endX := int(math.Floor(end.X / float64(s.CellWidth))) 205 | // endY := int(math.Floor(end.Y / float64(s.CellHeight))) 206 | 207 | // cell := s.Cell(startX, startY) 208 | // endCell := s.Cell(endX, endY) 209 | 210 | // if cell != nil && endCell != nil { 211 | 212 | // dv := Vector{float64(endX - startX), float64(endY - startY)}.Unit() 213 | // dv.X *= float64(s.CellWidth / 2) 214 | // dv.Y *= float64(s.CellHeight / 2) 215 | 216 | // pX := float64(startX * s.CellWidth) 217 | // pY := float64(startY * s.CellHeight) 218 | 219 | // p := Vector{pX + float64(s.CellWidth/2), pY + float64(s.CellHeight/2)} 220 | 221 | // alternate := false 222 | 223 | // for cell != nil { 224 | 225 | // if cell == endCell { 226 | // cells = append(cells, cell) 227 | // break 228 | // } 229 | 230 | // cells = append(cells, cell) 231 | 232 | // if alternate { 233 | // p.Y += dv.Y 234 | // } else { 235 | // p.X += dv.X 236 | // } 237 | 238 | // cx := int(math.Floor(p.X / float64(s.CellWidth))) 239 | // cy := int(math.Floor(p.Y / float64(s.CellHeight))) 240 | 241 | // c := s.Cell(cx, cy) 242 | // if c != cell { 243 | // cell = c 244 | // } 245 | // alternate = !alternate 246 | 247 | // } 248 | 249 | // } 250 | 251 | // return cells 252 | 253 | // } 254 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import "strconv" 4 | 5 | // Tags represents one or more bitwise tags contained within a single uint64. 6 | // You can use a tag to easily identify a type of object (e.g. player, solid, ramp, platform, etc). 7 | // The maximum number of tags one can define is 64 (to match the uint size). 8 | type Tags uint64 9 | 10 | // Set sets the tag value indicated to the Tags object. 11 | // Note that you can combine tags using the bitwise operator `|` (e.g. `TagSolidWall | TagPlatform`). 12 | func (t *Tags) Set(tagValue Tags) { 13 | (*t) = (*t) | tagValue 14 | } 15 | 16 | // Unset clears the tag value indicated in the Tags object. 17 | // Note that you can combine tags using the bitwise operator `|` (e.g. `TagSolidWall | TagPlatform`). 18 | func (t *Tags) Unset(tagValue Tags) { 19 | (*t) = (*t) ^ tagValue 20 | } 21 | 22 | // Clear clears the Tags object. 23 | func (t *Tags) Clear() { 24 | (*t) = 0 25 | } 26 | 27 | // Has returns if the Tags object has the tags indicated by tagValue set. 28 | // Note that you can combine tags using the bitwise operator `|` (e.g. `TagSolidWall | TagPlatform`). 29 | func (t Tags) Has(tagValue Tags) bool { 30 | return t&tagValue > 0 31 | } 32 | 33 | // IsEmpty returns if the Tags object has no tags set. 34 | func (t Tags) IsEmpty() bool { 35 | return t == 0 36 | } 37 | 38 | // String prints out the tags set in the Tags object as a human-readable string. 39 | func (t Tags) String() string { 40 | result := "Tags : [ " 41 | 42 | tagIndex := 0 43 | 44 | for i := 0; i < 64; i++ { 45 | possibleTag := Tags(1 << i) 46 | if t.Has(possibleTag) { 47 | if tagIndex > 0 { 48 | result += "| " 49 | } 50 | 51 | value, ok := tagDirectory[possibleTag] 52 | 53 | if !ok { 54 | value = strconv.Itoa(int(possibleTag)) 55 | } 56 | 57 | result += value + " " 58 | tagIndex++ 59 | } 60 | } 61 | result += "]" 62 | 63 | return result 64 | } 65 | 66 | var tagDirectory = map[Tags]string{} 67 | var currentTagIndex = Tags(1) 68 | 69 | // Creates a new tag with the given human-readable name associated with it. 70 | // You can also create tags using bitwise representation directly (`const myTag = resolv.Tags << 1`). 71 | // Be sure to use either method, rather than both; if you do use both, NewTag()'s internal tag index would be mismatched. 72 | // The maximum number of tags one can define is 64. 73 | func NewTag(tagName string) Tags { 74 | t := Tags(currentTagIndex) 75 | tagDirectory[currentTagIndex] = tagName 76 | currentTagIndex = currentTagIndex << 1 77 | return t 78 | } 79 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | ) 7 | 8 | // ToDegrees is a helper function to easily convert radians to degrees for human readability. 9 | func ToDegrees(radians float64) float64 { 10 | return radians / math.Pi * 180 11 | } 12 | 13 | // ToRadians is a helper function to easily convert degrees to radians (which is what the rotation-oriented functions in Tetra3D use). 14 | func ToRadians(degrees float64) float64 { 15 | return math.Pi * degrees / 180 16 | } 17 | 18 | func min(a, b float64) float64 { 19 | if a < b { 20 | return a 21 | } 22 | return b 23 | } 24 | 25 | func max(a, b float64) float64 { 26 | if a > b { 27 | return a 28 | } 29 | return b 30 | } 31 | 32 | func clamp(value, min, max float64) float64 { 33 | if value < min { 34 | return min 35 | } else if value > max { 36 | return max 37 | } 38 | return value 39 | } 40 | 41 | // func pow(value float64, power int) float64 { 42 | // x := value 43 | // for i := 0; i < power; i++ { 44 | // x += x 45 | // } 46 | // return x 47 | // } 48 | 49 | func round(value float64) float64 { 50 | 51 | iv := float64(int(value)) 52 | 53 | if value > iv+0.5 { 54 | return iv + 1 55 | } else if value < iv-0.5 { 56 | return iv - 1 57 | } 58 | 59 | return iv 60 | 61 | } 62 | 63 | // Projection represents the projection of a shape (usually a ConvexPolygon) onto an axis for intersection testing. 64 | // Normally, you wouldn't need to get this information, but it could be useful in some circumstances, I'm sure. 65 | type Projection struct { 66 | Min, Max float64 67 | } 68 | 69 | // IsOverlapping returns whether a Projection is overlapping with the other, provided Projection. Credit to https://www.sevenson.com.au/programming/sat/ 70 | func (projection Projection) IsOverlapping(other Projection) bool { 71 | return projection.Overlap(other) > 0 72 | } 73 | 74 | // Overlap returns the amount that a Projection is overlapping with the other, provided Projection. Credit to https://dyn4j.org/2010/01/sat/#sat-nointer 75 | func (projection Projection) Overlap(other Projection) float64 { 76 | return math.Min(projection.Max-other.Min, other.Max-projection.Min) 77 | } 78 | 79 | // IsInside returns whether the Projection is wholly inside of the other, provided Projection. 80 | func (projection Projection) IsInside(other Projection) bool { 81 | return projection.Min >= other.Min && projection.Max <= other.Max 82 | } 83 | 84 | // Bounds represents the minimum and maximum bounds of a Shape. 85 | type Bounds struct { 86 | Min, Max Vector 87 | space *Space 88 | } 89 | 90 | func (b Bounds) toCellSpace() (int, int, int, int) { 91 | 92 | minX := int(math.Floor(b.Min.X / float64(b.space.cellWidth))) 93 | minY := int(math.Floor(b.Min.Y / float64(b.space.cellHeight))) 94 | maxX := int(math.Floor(b.Max.X / float64(b.space.cellWidth))) 95 | maxY := int(math.Floor(b.Max.Y / float64(b.space.cellHeight))) 96 | 97 | return minX, minY, maxX, maxY 98 | } 99 | 100 | // Center returns the center position of the Bounds. 101 | func (b Bounds) Center() Vector { 102 | return b.Min.Add(b.Max.Sub(b.Min).Scale(0.5)) 103 | } 104 | 105 | // Width returns the width of the Bounds. 106 | func (b Bounds) Width() float64 { 107 | return b.Max.X - b.Min.X 108 | } 109 | 110 | // Height returns the height of the bounds. 111 | func (b Bounds) Height() float64 { 112 | return b.Max.Y - b.Min.Y 113 | } 114 | 115 | // MaxAxis returns the maximum value out of either the Bounds's width or height. 116 | func (b Bounds) MaxAxis() float64 { 117 | if b.Width() > b.Height() { 118 | return b.Width() 119 | } 120 | return b.Height() 121 | } 122 | 123 | // MinAxis returns the minimum value out of either the Bounds's width or height. 124 | func (b Bounds) MinAxis() float64 { 125 | if b.Width() > b.Height() { 126 | return b.Height() 127 | } 128 | return b.Width() 129 | } 130 | 131 | // Move moves the Bounds, such that the center point is offset by {x, y}. 132 | func (b Bounds) Move(x, y float64) Bounds { 133 | b.Min.X += x 134 | b.Min.Y += y 135 | b.Max.X += x 136 | b.Max.Y += y 137 | return b 138 | } 139 | 140 | // MoveVec moves the Bounds by the vector provided, such that the center point is offset by {x, y}. 141 | func (b *Bounds) MoveVec(vec Vector) Bounds { 142 | return b.Move(vec.X, vec.Y) 143 | } 144 | 145 | // IsIntersecting returns if the Bounds is intersecting with the given other Bounds. 146 | func (b Bounds) IsIntersecting(other Bounds) bool { 147 | bounds := b.Intersection(other) 148 | return !bounds.IsEmpty() 149 | } 150 | 151 | // Intersection returns the intersection between the two Bounds objects. 152 | func (b Bounds) Intersection(other Bounds) Bounds { 153 | 154 | overlap := Bounds{} 155 | 156 | if other.Max.X < b.Min.X || other.Min.X > b.Max.X || other.Max.Y < b.Min.Y || other.Min.Y > b.Max.Y { 157 | return overlap 158 | } 159 | 160 | overlap.Max.X = math.Min(other.Max.X, b.Max.X) 161 | overlap.Min.X = math.Max(other.Min.X, b.Min.X) 162 | 163 | overlap.Max.Y = math.Min(other.Max.Y, b.Max.Y) 164 | overlap.Min.Y = math.Max(other.Min.Y, b.Min.Y) 165 | 166 | return overlap 167 | 168 | } 169 | 170 | // IsEmpty returns true if the Bounds's minimum and maximum corners are 0. 171 | func (b Bounds) IsEmpty() bool { 172 | return b.Max.X-b.Min.X == 0 && b.Max.Y-b.Min.X == 0 173 | } 174 | 175 | ///// 176 | 177 | // Set represents a Set of elements. 178 | type Set[E comparable] map[E]struct{} 179 | 180 | // newSet creates a new set. 181 | func newSet[E comparable]() Set[E] { 182 | return Set[E]{} 183 | } 184 | 185 | // Clone clones the Set. 186 | func (s Set[E]) Clone() Set[E] { 187 | newSet := newSet[E]() 188 | newSet.Combine(s) 189 | return newSet 190 | } 191 | 192 | // Set sets the Set to have the same values as in the given other Set. 193 | func (s Set[E]) Set(other Set[E]) { 194 | s.Clear() 195 | s.Combine(other) 196 | } 197 | 198 | // Add adds the given elements to a set. 199 | func (s Set[E]) Add(elements ...E) { 200 | for _, element := range elements { 201 | s[element] = struct{}{} 202 | } 203 | } 204 | 205 | // Combine combines the given other elements to the set. 206 | func (s Set[E]) Combine(otherSet Set[E]) { 207 | for element := range otherSet { 208 | s.Add(element) 209 | } 210 | } 211 | 212 | // Contains returns if the set contains the given element. 213 | func (s Set[E]) Contains(element E) bool { 214 | _, ok := s[element] 215 | return ok 216 | } 217 | 218 | // Remove removes the given element from the set. 219 | func (s Set[E]) Remove(elements ...E) { 220 | for _, element := range elements { 221 | delete(s, element) 222 | } 223 | } 224 | 225 | // Clear clears the set. 226 | func (s Set[E]) Clear() { 227 | for v := range s { 228 | delete(s, v) 229 | } 230 | } 231 | 232 | // ForEach runs the provided function for each element in the set. 233 | func (s Set[E]) ForEach(f func(element E) bool) { 234 | for element := range s { 235 | if !f(element) { 236 | break 237 | } 238 | } 239 | } 240 | 241 | ///// 242 | 243 | // shapeIDSet is an easy way to determine if a shape has been iterated over before (used for filtering through shapes from CellSelections). 244 | type shapeIDSet []uint32 245 | 246 | func (s shapeIDSet) idInSet(id uint32) bool { 247 | for _, v := range s { 248 | if v == id { 249 | return true 250 | } 251 | } 252 | return false 253 | } 254 | 255 | var cellSelectionForEachIDSet = shapeIDSet{} 256 | 257 | ///// 258 | 259 | // LineTestSettings is a struct of settings to be used when performing line tests (the equivalent of 3D hitscan ray tests for 2D) 260 | type LineTestSettings struct { 261 | Start Vector // The start of the line to test shapes against 262 | End Vector // The end of the line to test chapes against 263 | TestAgainst ShapeIterator // The collection of shapes to test against 264 | // The callback to be called for each intersection between the given line, ranging from start to end, and each shape given in TestAgainst. 265 | // set is the intersection set that contains information about the intersection, index is the index of the current index 266 | // and count is the total number of intersections detected from the intersection test. 267 | // The boolean the callback returns indicates whether the LineTest function should continue testing or stop at the currently found intersection. 268 | OnIntersect func(set IntersectionSet, index, max int) bool 269 | callingShape IShape 270 | } 271 | 272 | var intersectionSets []IntersectionSet 273 | 274 | // LineTest instantly tests a selection of shapes against a ray / line. 275 | // Note that there is no MTV for these results. 276 | func LineTest(settings LineTestSettings) bool { 277 | 278 | castMargin := 0.01 // Basically, the line cast starts are a smidge back so that moving to contact doesn't make future line casts fail 279 | vu := settings.End.Sub(settings.Start).Unit() 280 | start := settings.Start.Sub(vu.Scale(castMargin)) 281 | 282 | line := newCollidingLine(start.X, start.Y, settings.End.X, settings.End.Y) 283 | 284 | intersectionSets = intersectionSets[:0] 285 | 286 | i := 0 287 | 288 | settings.TestAgainst.ForEach(func(other IShape) bool { 289 | 290 | if other == settings.callingShape { 291 | return true 292 | } 293 | 294 | i++ 295 | 296 | contactSet := newIntersectionSet() 297 | 298 | switch shape := other.(type) { 299 | 300 | case *Circle: 301 | 302 | res := line.IntersectionPointsCircle(shape) 303 | 304 | if len(res) > 0 { 305 | for _, contactPoint := range res { 306 | contactSet.Intersections = append(contactSet.Intersections, Intersection{ 307 | Point: contactPoint, 308 | Normal: contactPoint.Sub(shape.position).Unit(), 309 | }) 310 | } 311 | } 312 | 313 | case *ConvexPolygon: 314 | 315 | for _, otherLine := range shape.Lines() { 316 | 317 | if point, ok := line.IntersectionPointsLine(otherLine); ok { 318 | contactSet.Intersections = append(contactSet.Intersections, Intersection{ 319 | Point: point, 320 | Normal: otherLine.Normal(), 321 | }) 322 | } 323 | 324 | } 325 | 326 | } 327 | 328 | if len(contactSet.Intersections) > 0 { 329 | 330 | contactSet.OtherShape = other 331 | 332 | for _, contact := range contactSet.Intersections { 333 | contactSet.Center = contactSet.Center.Add(contact.Point) 334 | } 335 | 336 | contactSet.Center.X /= float64(len(contactSet.Intersections)) 337 | contactSet.Center.Y /= float64(len(contactSet.Intersections)) 338 | 339 | // Sort the points by distance to line start 340 | sort.Slice(contactSet.Intersections, func(i, j int) bool { 341 | return contactSet.Intersections[i].Point.DistanceSquared(settings.Start) < contactSet.Intersections[j].Point.DistanceSquared(settings.Start) 342 | }) 343 | 344 | contactSet.MTV = contactSet.Intersections[0].Point.Sub(settings.Start).Sub(vu.Scale(castMargin)) 345 | 346 | intersectionSets = append(intersectionSets, contactSet) 347 | 348 | } 349 | 350 | return true 351 | 352 | }) 353 | 354 | // Sort intersection sets by distance from closest hit to line start 355 | sort.Slice(intersectionSets, func(i, j int) bool { 356 | return intersectionSets[i].Intersections[0].Point.DistanceSquared(line.Start) < intersectionSets[j].Intersections[0].Point.DistanceSquared(line.Start) 357 | }) 358 | 359 | // Loop through all intersections and iterate through them 360 | if settings.OnIntersect != nil { 361 | for i, c := range intersectionSets { 362 | if !settings.OnIntersect(c, i, len(intersectionSets)) { 363 | break 364 | } 365 | } 366 | } 367 | 368 | return len(intersectionSets) > 0 369 | 370 | } 371 | -------------------------------------------------------------------------------- /vector.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // This is essentially a 2D version of my 3D Vectors used in Tetra3D. 9 | 10 | // WorldRight represents a unit vector in the global direction of WorldRight on the right-handed OpenGL / Tetra3D's coordinate system (+X). 11 | var WorldRight = NewVector(1, 0) 12 | 13 | // WorldLeft represents a unit vector in the global direction of WorldLeft on the right-handed OpenGL / Tetra3D's coordinate system (-X). 14 | var WorldLeft = WorldRight.Invert() 15 | 16 | // WorldUp represents a unit vector in the global direction of WorldUp on the right-handed OpenGL / Tetra3D's coordinate system (+Y). 17 | var WorldUp = NewVector(0, 1) 18 | 19 | // WorldDown represents a unit vector in the global direction of WorldDown on the right-handed OpenGL / Tetra3D's coordinate system (+Y). 20 | var WorldDown = WorldUp.Invert() 21 | 22 | // Vector represents a 2D Vector, which can be used for usual 2D applications (position, direction, velocity, etc). 23 | // Any Vector functions that modify the calling Vector return copies of the modified Vector, meaning you can do method-chaining easily. 24 | // Vectors seem to be most efficient when copied (so try not to store pointers to them if possible, as dereferencing pointers 25 | // can be more inefficient than directly acting on data, and storing pointers moves variables to heap). 26 | type Vector struct { 27 | X float64 // The X (1st) component of the Vector 28 | Y float64 // The Y (2nd) component of the Vector 29 | } 30 | 31 | // NewVector creates a new Vector with the specified x, y, and z components. The W component is generally ignored for most purposes. 32 | func NewVector(x, y float64) Vector { 33 | return Vector{X: x, Y: y} 34 | } 35 | 36 | // NewVectorZero creates a new "zero-ed out" Vector, with the values of 0, 0, 0, and 0 (for W). 37 | func NewVectorZero() Vector { 38 | return Vector{} 39 | } 40 | 41 | // Modify returns a ModVector object (a pointer to the original vector). 42 | // Using a ModVector allows you to chain functions without having to reassign it to the original Vector object all of the time. 43 | func (vec *Vector) Modify() ModVector { 44 | ip := ModVector{Vector: vec} 45 | return ip 46 | } 47 | 48 | // String returns a string representation of the Vector, excluding its W component (which is primarily used for internal purposes). 49 | func (vec Vector) String() string { 50 | return fmt.Sprintf("{%.2f, %.2f}", vec.X, vec.Y) 51 | } 52 | 53 | // Add returns a copy of the calling vector, added together with the other Vector provided (ignoring the W component). 54 | func (vec Vector) Add(other Vector) Vector { 55 | vec.X += other.X 56 | vec.Y += other.Y 57 | return vec 58 | } 59 | 60 | // AddX returns a copy of the calling vector with an added value to the X axis. 61 | func (vec Vector) AddX(x float64) Vector { 62 | vec.X += x 63 | return vec 64 | } 65 | 66 | // AddY returns a copy of the calling vector with an added value to the Y axis. 67 | func (vec Vector) AddY(y float64) Vector { 68 | vec.Y += y 69 | return vec 70 | } 71 | 72 | // Sub returns a copy of the calling Vector, with the other Vector subtracted from it (ignoring the W component). 73 | func (vec Vector) Sub(other Vector) Vector { 74 | vec.X -= other.X 75 | vec.Y -= other.Y 76 | return vec 77 | } 78 | 79 | // SubX returns a copy of the calling vector with an added value to the X axis. 80 | func (vec Vector) SubX(x float64) Vector { 81 | vec.X -= x 82 | return vec 83 | } 84 | 85 | // SubY returns a copy of the calling vector with an added value to the Y axis. 86 | func (vec Vector) SubY(y float64) Vector { 87 | vec.Y -= y 88 | return vec 89 | } 90 | 91 | // Expand expands the Vector by the margin specified, in absolute units, if each component is over the minimum argument. 92 | // To illustrate: Given a Vector of {1, 0.1, -0.3}, Vector.Expand(0.5, 0.2) would give you a Vector of {1.5, 0.1, -0.8}. 93 | // This function returns a copy of the Vector with the result. 94 | func (vec Vector) Expand(margin, min float64) Vector { 95 | if vec.X > min || vec.X < -min { 96 | vec.X += math.Copysign(margin, vec.X) 97 | } 98 | if vec.Y > min || vec.Y < -min { 99 | vec.Y += math.Copysign(margin, vec.Y) 100 | } 101 | return vec 102 | } 103 | 104 | // Invert returns a copy of the Vector with all components inverted. 105 | func (vec Vector) Invert() Vector { 106 | vec.X = -vec.X 107 | vec.Y = -vec.Y 108 | return vec 109 | } 110 | 111 | // Magnitude returns the length of the Vector. 112 | func (vec Vector) Magnitude() float64 { 113 | return math.Sqrt(vec.X*vec.X + vec.Y*vec.Y) 114 | } 115 | 116 | // MagnitudeSquared returns the squared length of the Vector; this is faster than Length() as it avoids using math.Sqrt(). 117 | func (vec Vector) MagnitudeSquared() float64 { 118 | return vec.X*vec.X + vec.Y*vec.Y 119 | } 120 | 121 | // ClampMagnitude clamps the overall magnitude of the Vector to the maximum magnitude specified, returning a copy with the result. 122 | func (vec Vector) ClampMagnitude(maxMag float64) Vector { 123 | if vec.Magnitude() > maxMag { 124 | vec = vec.Unit().Scale(maxMag) 125 | } 126 | return vec 127 | } 128 | 129 | // AddMagnitude adds magnitude to the Vector in the direction it's already pointing. 130 | func (vec Vector) AddMagnitude(mag float64) Vector { 131 | return vec.Add(vec.Unit().Scale(mag)) 132 | } 133 | 134 | // SubMagnitude subtracts the given magnitude from the Vector's existing magnitude. 135 | // If the vector's magnitude is less than the given magnitude to subtract, a zero-length Vector will be returned. 136 | func (vec Vector) SubMagnitude(mag float64) Vector { 137 | if vec.Magnitude() > mag { 138 | return vec.Sub(vec.Unit().Scale(mag)) 139 | } 140 | return Vector{0, 0} 141 | 142 | } 143 | 144 | // Distance returns the distance from the calling Vector to the other Vector provided. 145 | func (vec Vector) Distance(other Vector) float64 { 146 | vec.X -= other.X 147 | vec.Y -= other.Y 148 | return math.Sqrt(vec.X*vec.X + vec.Y*vec.Y) 149 | } 150 | 151 | // Distance returns the squared distance from the calling Vector to the other Vector provided. This is faster than Distance(), as it avoids using math.Sqrt(). 152 | func (vec Vector) DistanceSquared(other Vector) float64 { 153 | vec.X -= other.X 154 | vec.Y -= other.Y 155 | return vec.X*vec.X + vec.Y*vec.Y 156 | } 157 | 158 | // Mult performs Hadamard (component-wise) multiplication on the calling Vector with the other Vector provided, returning a copy with the result (and ignoring the Vector's W component). 159 | func (vec Vector) Mult(other Vector) Vector { 160 | vec.X *= other.X 161 | vec.Y *= other.Y 162 | return vec 163 | } 164 | 165 | // Unit returns a copy of the Vector, normalized (set to be of unit length). 166 | // It does not alter the W component of the Vector. 167 | func (vec Vector) Unit() Vector { 168 | l := vec.Magnitude() 169 | if l < 1e-8 || l == 1 { 170 | // If it's 0, then don't modify the vector 171 | return vec 172 | } 173 | vec.X, vec.Y = vec.X/l, vec.Y/l 174 | return vec 175 | } 176 | 177 | // SetX sets the X component in the vector to the value provided. 178 | func (vec Vector) SetX(x float64) Vector { 179 | vec.X = x 180 | return vec 181 | } 182 | 183 | // SetY sets the Y component in the vector to the value provided. 184 | func (vec Vector) SetY(y float64) Vector { 185 | vec.Y = y 186 | return vec 187 | } 188 | 189 | // Set sets the values in the Vector to the x, y, and z values provided. 190 | func (vec Vector) Set(x, y float64) Vector { 191 | vec.X = x 192 | vec.Y = y 193 | return vec 194 | } 195 | 196 | // Reflect reflects the vector against the given surface normal. 197 | func (vec Vector) Reflect(normal Vector) Vector { 198 | n := normal.Unit() 199 | return vec.Sub(n.Scale(2 * n.Dot(vec))) 200 | } 201 | 202 | // Perp returns the right-handed perpendicular of the vector (i.e. the vector rotated 90 degrees to the right, clockwise). 203 | func (vec Vector) Perp() Vector { 204 | return Vector{-vec.Y, vec.X} 205 | } 206 | 207 | // Floats returns a [2]float64 array consisting of the Vector's contents. 208 | func (vec Vector) Floats() [2]float64 { 209 | return [2]float64{vec.X, vec.Y} 210 | } 211 | 212 | // Equals returns true if the two Vectors are close enough in all values (excluding W). 213 | func (vec Vector) Equals(other Vector) bool { 214 | 215 | eps := 1e-4 216 | 217 | if math.Abs(float64(vec.X-other.X)) > eps || math.Abs(float64(vec.Y-other.Y)) > eps { 218 | return false 219 | } 220 | 221 | return true 222 | 223 | } 224 | 225 | // IsZero returns true if the values in the Vector are extremely close to 0 (excluding W). 226 | func (vec Vector) IsZero() bool { 227 | 228 | eps := 1e-4 229 | 230 | if math.Abs(float64(vec.X)) > eps || math.Abs(float64(vec.Y)) > eps { 231 | return false 232 | } 233 | 234 | // if !onlyXYZ && math.Abs(vec.W-other.W) > eps { 235 | // return false 236 | // } 237 | 238 | return true 239 | 240 | } 241 | 242 | // Rotate returns a copy of the Vector, rotated around an axis Vector with the x, y, and z components provided, by the angle 243 | // provided (in radians), counter-clockwise. 244 | // The function is most efficient if passed an orthogonal, normalized axis (i.e. the X, Y, or Z constants). 245 | // Note that this function ignores the W component of both Vectors. 246 | func (vec Vector) Rotate(angle float64) Vector { 247 | x := vec.X 248 | y := vec.Y 249 | vec.X = x*math.Cos(angle) - y*math.Sin(angle) 250 | vec.Y = x*math.Sin(angle) + y*math.Cos(angle) 251 | return vec 252 | } 253 | 254 | // Angle returns the signed angle in radians between the calling Vector and the provided other Vector (ignoring the W component). 255 | func (vec Vector) Angle(other Vector) float64 { 256 | // vec = vec.Unit() 257 | // other = other.Unit() 258 | angle := math.Atan2(other.Y, other.X) - math.Atan2(vec.Y, vec.X) 259 | if angle > math.Pi { 260 | angle -= 2 * math.Pi 261 | } else if angle <= -math.Pi { 262 | angle += 2 * math.Pi 263 | } 264 | return angle 265 | } 266 | 267 | // AngleRotation returns the angle in radians between this Vector and world right 268 | func (vec Vector) AngleRotation() float64 { 269 | return vec.Angle(WorldRight) 270 | } 271 | 272 | // Scale scales a Vector by the given scalar (ignoring the W component), returning a copy with the result. 273 | func (vec Vector) Scale(scalar float64) Vector { 274 | vec.X *= scalar 275 | vec.Y *= scalar 276 | return vec 277 | } 278 | 279 | // Divide divides a Vector by the given scalar (ignoring the W component), returning a copy with the result. 280 | func (vec Vector) Divide(scalar float64) Vector { 281 | vec.X /= scalar 282 | vec.Y /= scalar 283 | return vec 284 | } 285 | 286 | // Dot returns the dot product of a Vector and another Vector (ignoring the W component). 287 | func (vec Vector) Dot(other Vector) float64 { 288 | return vec.X*other.X + vec.Y*other.Y 289 | } 290 | 291 | // Round rounds off the Vector's components to the given space in world unit increments, returning a clone 292 | // (e.g. Vector{0.1, 1.27, 3.33}.Snap(0.25) will return Vector{0, 1.25, 3.25}). 293 | func (vec Vector) Round(snapToUnits float64) Vector { 294 | vec.X = round(vec.X/snapToUnits) * snapToUnits 295 | vec.Y = round(vec.Y/snapToUnits) * snapToUnits 296 | return vec 297 | } 298 | 299 | // ClampAngle clamps the Vector such that it doesn't exceed the angle specified (in radians). 300 | // This function returns a normalized (unit) Vector. 301 | func (vec Vector) ClampAngle(baselineVec Vector, maxAngle float64) Vector { 302 | 303 | mag := vec.Magnitude() 304 | 305 | angle := vec.Angle(baselineVec) 306 | 307 | if angle > maxAngle { 308 | vec = baselineVec.Slerp(vec, maxAngle/angle).Unit() 309 | } 310 | 311 | return vec.Scale(mag) 312 | 313 | } 314 | 315 | // Lerp performs a linear interpolation between the starting Vector and the provided 316 | // other Vector, to the given percentage (ranging from 0 to 1). 317 | func (vec Vector) Lerp(other Vector, percentage float64) Vector { 318 | percentage = clamp(percentage, 0, 1) 319 | vec.X = vec.X + ((other.X - vec.X) * percentage) 320 | vec.Y = vec.Y + ((other.Y - vec.Y) * percentage) 321 | return vec 322 | } 323 | 324 | // Slerp performs a spherical linear interpolation between the starting Vector and the provided 325 | // ending Vector, to the given percentage (ranging from 0 to 1). 326 | // This should be done with directions, usually, rather than positions. 327 | // This being the case, this normalizes both Vectors. 328 | func (vec Vector) Slerp(targetDirection Vector, percentage float64) Vector { 329 | 330 | vec = vec.Unit() 331 | targetDirection = targetDirection.Unit() 332 | 333 | // Thank you StackOverflow, once again! : https://stackoverflow.com/questions/67919193/how-does-unity-implements-vector3-slerp-exactly 334 | percentage = clamp(percentage, 0, 1) 335 | 336 | dot := vec.Dot(targetDirection) 337 | 338 | dot = clamp(dot, -1, 1) 339 | 340 | theta := math.Acos(dot) * percentage 341 | relative := targetDirection.Sub(vec.Scale(dot)).Unit() 342 | 343 | return (vec.Scale(math.Cos(theta)).Add(relative.Scale(math.Sin(theta)))).Unit() 344 | 345 | } 346 | 347 | // IsInside returns if the given Vector is inside of the given Shape. 348 | func (vec Vector) IsInside(shape IShape) bool { 349 | switch other := shape.(type) { 350 | case *ConvexPolygon: 351 | 352 | // Internally, we test for this by just making a line that extends into infinity and then checking for intersection points. 353 | pointLine := newCollidingLine(vec.X, vec.Y, vec.X+999999999999, vec.Y) 354 | 355 | contactCount := 0 356 | 357 | for _, line := range other.Lines() { 358 | 359 | if _, ok := line.IntersectionPointsLine(pointLine); ok { 360 | contactCount++ 361 | } 362 | 363 | } 364 | 365 | return contactCount%2 == 1 366 | 367 | case *Circle: 368 | return vec.DistanceSquared(other.position) <= other.radius*other.radius 369 | } 370 | return false 371 | } 372 | 373 | // ModVector represents a reference to a Vector, made to facilitate easy method-chaining and modifications on that Vector (as you 374 | // don't need to re-assign the results of a chain of operations to the original variable to "save" the results). 375 | // Note that a ModVector is not meant to be used to chain methods on a vector to pass directly into a function; you can just 376 | // use the normal vector functions for that purpose. ModVectors are pointers, which are allocated to the heap. This being the case, 377 | // they should be slower relative to normal Vectors, so use them only in non-performance-critical parts of your application. 378 | type ModVector struct { 379 | *Vector 380 | } 381 | 382 | // Add adds the other Vector provided to the ModVector. 383 | // This function returns the calling ModVector for method chaining. 384 | func (ip ModVector) Add(other Vector) ModVector { 385 | ip.X += other.X 386 | ip.Y += other.Y 387 | return ip 388 | } 389 | 390 | // Sub subtracts the other Vector from the calling ModVector. 391 | // This function returns the calling ModVector for method chaining. 392 | func (ip ModVector) Sub(other Vector) ModVector { 393 | ip.X -= other.X 394 | ip.Y -= other.Y 395 | return ip 396 | } 397 | 398 | // AddX adds a value to the X axis of the given Vector. 399 | // This function returns the calling ModVector for method chaining. 400 | func (ip ModVector) AddX(x float64) ModVector { 401 | ip.X += x 402 | return ip 403 | } 404 | 405 | // SetX sets a value to the X axis of the given Vector. 406 | // This function returns the calling ModVector for method chaining. 407 | func (ip ModVector) SetX(x float64) ModVector { 408 | ip.X = x 409 | return ip 410 | } 411 | 412 | // SubX subtracts a value from the X axis of the given Vector. 413 | // This function returns the calling ModVector for method chaining. 414 | func (ip ModVector) SubX(x float64) ModVector { 415 | ip.X -= x 416 | return ip 417 | } 418 | 419 | // AddY adds a value to the Y axis of the given Vector. 420 | // This function returns the calling ModVector for method chaining. 421 | func (ip ModVector) AddY(y float64) ModVector { 422 | ip.X += y 423 | return ip 424 | } 425 | 426 | // SetY sets a value to the Y axis of the given Vector. 427 | // This function returns the calling ModVector for method chaining. 428 | func (ip ModVector) SetY(y float64) ModVector { 429 | ip.X = y 430 | return ip 431 | } 432 | 433 | // SubY subtracts a value from the Y axis of the given Vector. 434 | // This function returns the calling ModVector for method chaining. 435 | func (ip ModVector) SubY(y float64) ModVector { 436 | ip.X -= y 437 | return ip 438 | } 439 | 440 | func (ip ModVector) SetZero() ModVector { 441 | ip.X = 0 442 | ip.Y = 0 443 | return ip 444 | } 445 | 446 | // Scale scales the Vector by the scalar provided. 447 | // This function returns the calling ModVector for method chaining. 448 | func (ip ModVector) Scale(scalar float64) ModVector { 449 | ip.X *= scalar 450 | ip.Y *= scalar 451 | return ip 452 | } 453 | 454 | // Divide divides a Vector by the given scalar (ignoring the W component). 455 | // This function returns the calling ModVector for method chaining. 456 | func (ip ModVector) Divide(scalar float64) ModVector { 457 | ip.X /= scalar 458 | ip.Y /= scalar 459 | return ip 460 | } 461 | 462 | // Expand expands the ModVector by the margin specified, in absolute units, if each component is over the minimum argument. 463 | // To illustrate: Given a ModVector of {1, 0.1, -0.3}, ModVector.Expand(0.5, 0.2) would give you a ModVector of {1.5, 0.1, -0.8}. 464 | // This function returns the calling ModVector for method chaining. 465 | func (ip ModVector) Expand(margin, min float64) ModVector { 466 | exp := ip.Vector.Expand(margin, min) 467 | ip.X = exp.X 468 | ip.Y = exp.Y 469 | return ip 470 | } 471 | 472 | // Mult performs Hadamard (component-wise) multiplication with the Vector on the other Vector provided. 473 | // This function returns the calling ModVector for method chaining. 474 | func (ip ModVector) Mult(other Vector) ModVector { 475 | ip.X *= other.X 476 | ip.Y *= other.Y 477 | return ip 478 | } 479 | 480 | // Unit normalizes the ModVector (sets it to be of unit length). 481 | // It does not alter the W component of the Vector. 482 | // This function returns the calling ModVector for method chaining. 483 | func (ip ModVector) Unit() ModVector { 484 | l := ip.Magnitude() 485 | if l < 1e-8 || l == 1 { 486 | // If it's 0, then don't modify the vector 487 | return ip 488 | } 489 | ip.X, ip.Y = ip.X/l, ip.Y/l 490 | return ip 491 | } 492 | 493 | // Rotate rotates the calling Vector by the angle provided (in radians). 494 | // This function returns the calling ModVector for method chaining. 495 | func (ip ModVector) Rotate(angle float64) ModVector { 496 | rot := (*ip.Vector).Rotate(angle) 497 | ip.X = rot.X 498 | ip.Y = rot.Y 499 | return ip 500 | } 501 | 502 | // Invert inverts all components of the calling Vector. 503 | // This function returns the calling ModVector for method chaining. 504 | func (ip ModVector) Invert() ModVector { 505 | ip.X = -ip.X 506 | ip.Y = -ip.Y 507 | return ip 508 | } 509 | 510 | // Round snaps the Vector's components to the given space in world units, returning a clone (e.g. Vector{0.1, 1.27, 3.33}.Snap(0.25) will return Vector{0, 1.25, 3.25}). 511 | // This function returns the calling ModVector for method chaining. 512 | func (ip ModVector) Round(snapToUnits float64) ModVector { 513 | snapped := (*ip.Vector).Round(snapToUnits) 514 | ip.X = snapped.X 515 | ip.Y = snapped.Y 516 | return ip 517 | } 518 | 519 | // ClampMagnitude clamps the overall magnitude of the Vector to the maximum magnitude specified. 520 | // This function returns the calling ModVector for method chaining. 521 | func (ip ModVector) ClampMagnitude(maxMag float64) ModVector { 522 | clamped := (*ip.Vector).ClampMagnitude(maxMag) 523 | ip.X = clamped.X 524 | ip.Y = clamped.Y 525 | return ip 526 | } 527 | 528 | // SubMagnitude subtacts the given magnitude from the Vector's. If the vector's magnitude is less than the given magnitude to subtract, 529 | // a zero-length Vector will be returned. 530 | // This function returns the calling ModVector for method chaining. 531 | func (ip ModVector) SubMagnitude(mag float64) ModVector { 532 | if ip.Magnitude() > mag { 533 | ip.Sub(ip.Vector.Unit().Scale(mag)) 534 | } else { 535 | ip.X = 0 536 | ip.Y = 0 537 | } 538 | return ip 539 | 540 | } 541 | 542 | // AddMagnitude adds magnitude to the Vector in the direction it's already pointing. 543 | // This function returns the calling ModVector for method chaining. 544 | func (ip ModVector) AddMagnitude(mag float64) ModVector { 545 | ip.Add(ip.Vector.Unit().Scale(mag)) 546 | return ip 547 | } 548 | 549 | // Lerp performs a linear interpolation between the starting Vector and the provided 550 | // other Vector, to the given percentage (ranging from 0 to 1). 551 | // This function returns the calling ModVector for method chaining. 552 | func (ip ModVector) Lerp(other Vector, percentage float64) ModVector { 553 | lerped := (*ip.Vector).Lerp(other, percentage) 554 | ip.X = lerped.X 555 | ip.Y = lerped.Y 556 | return ip 557 | } 558 | 559 | // Slerp performs a linear interpolation between the starting Vector and the provided 560 | // other Vector, to the given percentage (ranging from 0 to 1). 561 | // This function returns the calling ModVector for method chaining. 562 | func (ip ModVector) Slerp(targetDirection Vector, percentage float64) ModVector { 563 | slerped := (*ip.Vector).Slerp(targetDirection, percentage) 564 | ip.X = slerped.X 565 | ip.Y = slerped.Y 566 | return ip 567 | } 568 | 569 | // ClampAngle clamps the Vector such that it doesn't exceed the angle specified (in radians). 570 | // This function returns the calling ModVector for method chaining. 571 | func (ip ModVector) ClampAngle(baselineVector Vector, maxAngle float64) ModVector { 572 | clamped := (*ip.Vector).ClampAngle(baselineVector, maxAngle) 573 | ip.X = clamped.X 574 | ip.Y = clamped.Y 575 | return ip 576 | } 577 | 578 | // String converts the ModVector to a string. Because it's a ModVector, it's represented with a *. 579 | func (ip ModVector) String() string { 580 | return fmt.Sprintf("*{%.2f, %.2f}", ip.X, ip.Y) 581 | } 582 | 583 | // Clone returns a ModVector of a clone of its backing Vector. 584 | // This function returns the calling ModVector for method chaining. 585 | func (ip ModVector) Clone() ModVector { 586 | v := *ip.Vector 587 | return v.Modify() 588 | } 589 | 590 | func (ip ModVector) ToVector() Vector { 591 | return *ip.Vector 592 | } 593 | 594 | // Reflect reflects the vector against the given surface normal. 595 | // This function returns the calling ModVector for method chaining. 596 | func (ip ModVector) Reflect(normal Vector) ModVector { 597 | reflected := (*ip.Vector).Reflect(normal) 598 | ip.X = reflected.X 599 | ip.Y = reflected.Y 600 | return ip 601 | } 602 | 603 | // Perp returns the right-handed perpendicular of the input vector (i.e. the Vector rotated 90 degrees to the right, clockwise). 604 | // This function returns the calling ModVector for method chaining. 605 | func (ip ModVector) Perp() ModVector { 606 | crossed := (*ip.Vector).Perp() 607 | ip.X = crossed.X 608 | ip.Y = crossed.Y 609 | return ip 610 | } 611 | 612 | // Distance returns the distance from the calling ModVector to the other Vector provided. 613 | func (ip ModVector) Distance(other Vector) float64 { 614 | return ip.Vector.Distance(other) 615 | } 616 | 617 | // DistanceSquared returns the distance from the calling ModVector to the other Vector provided. 618 | func (ip ModVector) DistanceSquared(other Vector) float64 { 619 | return ip.Vector.DistanceSquared(other) 620 | } 621 | --------------------------------------------------------------------------------