package resolv import ( "math" ) // Object represents an object that can be spread across one or more Cells in a Space. An Object is essentially an AABB (Axis-Aligned Bounding Box) Rectangle. type Object struct { Shape Shape // A shape for more specific collision-checking. Space *Space // Reference to the Space the Object exists within X, Y, W, H float64 // Position and size of the Object in the Space TouchingCells *RingBuffer // An array of Cells the Object is touching Data interface{} // A pointer to a user-definable object ignoreList map[*Object]bool // Set of Objects to ignore when checking for collisions tags []string // A list of tags the Object has } // NewObject returns a new Object of the specified position and size. func NewObjectSingleTag(x, y, w, h float64, tag string) *Object { o := &Object{ X: x, Y: y, W: w, H: h, TouchingCells: NewRingBuffer(512), // [WARNING] Should make N large enough to cover all "TouchingCells", otherwise some cells would fail to unregister an object, resulting in memory corruption and incorrect detection result! tags: []string{tag}, ignoreList: map[*Object]bool{}, } return o } func NewObject(x, y, w, h float64, tags ...string) *Object { o := &Object{ X: x, Y: y, W: w, H: h, TouchingCells: NewRingBuffer(512), tags: []string{}, ignoreList: map[*Object]bool{}, } if len(tags) > 0 { o.AddTags(tags...) } return o } // Clone clones the Object with its properties into another Object. It also clones the Object's Shape (if it has one). func (obj *Object) Clone() *Object { newObj := NewObject(obj.X, obj.Y, obj.W, obj.H, obj.Tags()...) newObj.Data = obj.Data if obj.Shape != nil { newObj.SetShape(obj.Shape.Clone()) } for k := range obj.ignoreList { newObj.AddToIgnoreList(k) } return newObj } // Update updates the object's association to the Cells in the Space. This should be called whenever an Object is moved. // This is automatically called once when creating the Object, so you don't have to call it for static objects. func (obj *Object) Update() { if obj.Space != nil { // Object.Space.Remove() sets the removed object's Space to nil, indicating it's been removed. Because we're updating // the Object (which is essentially removing it from its previous Cells / position and re-adding it to the new Cells / // position), we store the original Space to re-set it. space := obj.Space obj.Space.RemoveSingle(obj) obj.Space = space cx, cy, ex, ey := obj.BoundsToSpace(0, 0) for y := cy; y <= ey; y++ { for x := cx; x <= ex; x++ { c := obj.Space.Cell(x, y) if c != nil { c.register(obj) obj.TouchingCells.Put(c) } } } } if obj.Shape != nil { obj.Shape.SetPosition(obj.X, obj.Y) } } // AddTags adds tags to the Object. func (obj *Object) AddTags(tags ...string) { obj.tags = append(obj.tags, tags...) } // RemoveTags removes tags from the Object. func (obj *Object) RemoveTags(tags ...string) { for _, tag := range tags { for i, t := range obj.tags { if t == tag { obj.tags = append(obj.tags[:i], obj.tags[i+1:]...) break } } } } // HasTags indicates if an Object has any of the tags indicated. func (obj *Object) HasTags(tags ...string) bool { for _, tag := range tags { for _, t := range obj.tags { if t == tag { return true } } } return false } // Tags returns the tags an Object has. func (obj *Object) Tags() []string { return append([]string{}, obj.tags...) } // SetShape sets the Shape on the Object, in case you need to use precise per-Shape intersection detection. SetShape calls Object.Update() as well, so that it's able to // update the Shape's position to match its Object as necessary. (If you don't use this, the Shape's position might not match the Object's, depending on if you set the Shape // after you added the Object to a Space and if you don't call Object.Update() yourself afterwards.) func (obj *Object) SetShape(shape Shape) { if obj.Shape != shape { obj.Shape = shape obj.Update() } } // BoundsToSpace returns the Space coordinates of the shape (x, y, w, and h), given its world position and size, and a supposed movement of dx and dy. func (obj *Object) BoundsToSpace(dx, dy float64) (int, int, int, int) { cx, cy := obj.Space.WorldToSpace(obj.X+dx, obj.Y+dy) ex, ey := obj.Space.WorldToSpace(obj.X+obj.W+dx-1, obj.Y+obj.H+dy-1) return cx, cy, ex, ey } // SharesCells returns whether the Object occupies a cell shared by the specified other Object. func (obj *Object) SharesCells(other *Object) bool { rb := obj.TouchingCells for i := rb.StFrameId; i < rb.EdFrameId; i++ { cell := rb.GetByFrameId(i).(*Cell) if cell.Contains(other) { return true } } return false } // SharesCellsTags returns if the Cells the Object occupies have an object with the specified tags. func (obj *Object) SharesCellsTags(tags ...string) bool { rb := obj.TouchingCells for i := rb.StFrameId; i < rb.EdFrameId; i++ { cell := rb.GetByFrameId(i).(*Cell) if cell.ContainsTags(tags...) { return true } } return false } // Center returns the center position of the Object. func (obj *Object) Center() (float64, float64) { return obj.X + (obj.W / 2.0), obj.Y + (obj.H / 2.0) } // SetCenter sets the Object such that its center is at the X and Y position given. func (obj *Object) SetCenter(x, y float64) { obj.X = x - (obj.W / 2) obj.Y = y - (obj.H / 2) } // CellPosition returns the cellular position of the Object's center in the Space. func (obj *Object) CellPosition() (int, int) { return obj.Space.WorldToSpace(obj.Center()) } // SetRight sets the X position of the Object so the right edge is at the X position given. func (obj *Object) SetRight(x float64) { obj.X = x - obj.W } // SetBottom sets the Y position of the Object so that the bottom edge is at the Y position given. func (obj *Object) SetBottom(y float64) { obj.Y = y - obj.H } // Bottom returns the bottom Y coordinate of the Object (i.e. object.Y + object.H). func (obj *Object) Bottom() float64 { return obj.Y + obj.H } // Right returns the right X coordinate of the Object (i.e. object.X + object.W). func (obj *Object) Right() float64 { return obj.X + obj.W } func (obj *Object) SetBounds(topLeft, bottomRight Vector) { obj.X = topLeft[0] obj.Y = topLeft[1] obj.W = bottomRight[0] - obj.X obj.H = bottomRight[1] - obj.Y } // Check checks the space around the object using the designated delta movement (dx and dy). This is done by querying the containing Space's Cells // so that it can see if moving it would coincide with a cell that houses another Object (filtered using the given selection of tag strings). If so, // Check returns a Collision. If no objects are found or the Object does not exist within a Space, this function returns nil. func (obj *Object) CheckAllWithHolder(dx, dy float64, cc *Collision) bool { if obj.Space == nil { return false } cc.checkingObject = obj if dx < 0 { dx = math.Min(dx, -1) } else if dx > 0 { dx = math.Max(dx, 1) } if dy < 0 { dy = math.Min(dy, -1) } else if dy > 0 { dy = math.Max(dy, 1) } cc.dx = dx cc.dy = dy cx, cy, ex, ey := obj.BoundsToSpace(dx, dy) objectsAdded := map[*Object]bool{} cellsAdded := map[*Cell]bool{} for y := cy; y <= ey; y++ { for x := cx; x <= ex; x++ { if c := obj.Space.Cell(x, y); c != nil { rb := c.Objects for i := rb.StFrameId; i < rb.EdFrameId; i++ { o := rb.GetByFrameId(i).(*Object) // We only want cells that have objects other than the checking object, or that aren't on the ignore list. if ignored := obj.ignoreList[o]; o == obj || ignored { continue } if _, added := objectsAdded[o]; !added { cc.Objects.Put(o) objectsAdded[o] = true if _, added := cellsAdded[c]; !added { cc.Cells.Put(c) cellsAdded[c] = true } continue } } } } } if 0 >= cc.Objects.Cnt { return false } return true } // Overlaps returns if an Object overlaps another Object. func (obj *Object) Overlaps(other *Object) bool { return other.X <= obj.X+obj.W && other.X+other.W >= obj.X && other.Y <= obj.Y+obj.H && other.Y+other.H >= obj.Y } // AddToIgnoreList adds the specified Object to the Object's internal collision ignoral list. Cells that contain the specified Object will not be counted when calling Check(). func (obj *Object) AddToIgnoreList(ignoreObj *Object) { obj.ignoreList[ignoreObj] = true } // RemoveFromIgnoreList removes the specified Object from the Object's internal collision ignoral list. Objects removed from this list will once again be counted for Check(). func (obj *Object) RemoveFromIgnoreList(ignoreObj *Object) { delete(obj.ignoreList, ignoreObj) }