resolv

package module
v0.0.0-...-f22ef61 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 6, 2020 License: MIT Imports: 3 Imported by: 0

README

resolv

Bouncing

Other gifs

GoDocs

What is resolv?

resolv is a library specifically created for simple arcade (non-realistic) collision detection and resolution for video games. resolv is created in the Go language, but the core concepts are very straightforward and could be easily adapted for use with other languages or game engines and frameworks.

Basically: It allows you to do simple physics easier, without it doing the physics part - that's still on you.

Why is it called that?

Because it's like... You know, collision resolution? To resolve a collision? So... That's the name. I juste took an e off because I misplaced it somewhere.

Why did you create resolv?

Because I was making games and frequently found that most 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. Note that Resolv is generally recommended for use for simpler games with non-grid-based objects. If your game has objects already contained in or otherwise aligned to a 2D array, then it would most likely be more efficient to use that array for collision detection instead of using Resolv.

How do I install it?

It should be as simple as just go getting it and importing it in your game application.

go get github.com/SolarLune/resolv

How do I use it?

There's two ways to use resolv. One way is to simply create two Shapes, and then check for a collision between them, or attempt to resolve a movement of one into the other, like below:


// Here, we keep a pointer to the shapes we want to check, since we want to create them just once
// in the Init() function, and then check them every frame in the Update() function.

var shape1 *resolv.Rectangle
var shape2 *resolv.Rectangle

func Init() {

    // Create one rectangle, with an X and Y of 10 each, and a Width and Height of 16 each.
    shape1 = resolv.NewRectangle(10, 10, 16, 16)

    // Create another rectangle, as well.
    shape2 = resolv.NewRectangle(11, 100, 16, 16)

}

func Update() {

    // Later on, in the game's update loop...

    // Let's say we were trying to move the shape to the right by 2 pixels. We'll create a delta X movement variable
    // that stores a value of 2.
    dx := 2

    // Here, we check to see if there's a collision should shape1 try to move to the right by the delta movement. The Resolve()
    // functions return a Collision object that has information about whether the attempted movement resulted in a collision 
    // or not.
    resolution := resolv.Resolve(shape1, shape2, dx, 0)

    if resolution.Colliding() {
        
        // If there was a collision, then shape1 couldn't move fully to the right. It came into contact with shape2,
        // and the variable "resolution" now holds a Collision struct with helpful information, like how far to move to be touching.
        
        // Here we just move the shape over to the right by the distance reported by the Collision struct so it'll come into contact 
        // with shape2.
        shape1.X += resolution.ResolveX

    } else {

        // If there wasn't a collision, shape1 should be able to move fully to the right, so we move it.
        shape1.X += dx

    }

    // We can also do collision testing only pretty simply:

    colliding := shape1.IsColliding(shape2)

    if colliding {
        fmt.Println("WHOA! shape1 and shape2 are colliding.")
    }

}

// That's it!

This is fine for simple testing, but if you have even a slightly more complex game with a lot more Shapes, then you would have to check each Shape against each other Shape. This is a bit awkward for the developer to code, so I also added Spaces.

A Space represents a container for Shapes to exist in and test against. The fundamentals are the same, but it scales up more easily, since you don't have to do manual checking everywhere you want to test a Shape against any others.

A Space is just a pointer to a slice of Shapes, so feel free to use as many as you need to. You could split up a level into multiple Spaces, or have everything in one Space if it works for your game. Spaces also contain functions to filter them out as necessary to easily test a smaller selection of Shapes when desired.

Here's an example using a Space to check one Shape against others:


var space *resolv.Space
var playerRect *resolv.Rectangle

// Here, in the game's init loop...

func Init() {

    // Create a space for Shapes to occupy.
    space = resolv.NewSpace()

    // Create one rectangle - we'll say this one represents our player.
    playerRect = resolv.NewRectangle(40, 40, 16, 16)

    /* Note that we don't HAVE to add the Player Rectangle to the Space; this is only if we
    want it to also be checked for collision testing and resolution within the Space by 
    other Shapes.*/
    space.Add(playerRect)

    // Now we're going to create some more Rectangles to represent level bounds.
    
    // We can also add multiple shapes in a single Add() call...

    space.Add(resolv.NewRectangle(16, 0, 320, 16),
        resolv.NewRectangle(16, 16, 320, 16),
        resolv.NewRectangle(16, 240-16, 320, 16),
        resolv.NewRectangle(320-16, 16, 16, 240-16)
    )

    // Note that this is a bit verbose - in reality, you'd probably be loading the necessary data 
    // to construct the Shapes by looping through a for-loop when reading data in from a 
    // level format, like Tiled's TMX format. Anyway...

    // A Space also has the ability to easily add tags to its Shapes.
    space.AddTags("solid")
    
}

func Update() {

    // This time, we want to see if we're going to collide with something solid when 
    // moving down-right by 4 pixels on each axis.

    dx := 4
    dy := 4

    // To check for Shapes with a specific tag, we can filter out the Space they exist 
    // in with either the Space.FilterByTags() or Space.Filter() functions. Space.Filter() 
    // allows us to provide a function to filter out the Shapes; Space.FilterByTags() 
    // takes tags themselves to filter out the Shapes by.

    // This gives us just the Shapes with the "solid" tag.
    solids := space.FilterByTags("solid")

    // You can provide multiple tags in the same function to filter by all of them at the same
    // time, as well. ( i.e. deathZones := space.FilterByTags("danger", "zone") )

    // Now we check each axis individually against the Space (or, in other words, against
    // all Shapes conatined within the Space). This is done to allow a collision on one 
    // axis to not stop movement on the other as necessary. Note that Space.Resolve() 
    // takes the checking Shape as the first argument, and returns the first collision 
    // that it comes into contact with.

    collision := solids.Resolve(playerRect, dx, 0)

    if collision.Colliding() {
        playerRect.X += collision.ResolveX
    } else {
        playerRect.X += dx
    }

    collision = solids.Resolve(playerRect, 0, dy)

    if collision.Colliding() {
        playerRect.Y += collision.ResolveY
    } else {
        playerRect.Y += dy
    }

}

// Done-zo!

Shapes also have functions to get and set a pointer to a data object (of type interface{} ), which can be used to point to a user-created object, like a GameObject or Player struct.

type Player struct {
    Rect *resolv.Rectangle
}

func Init() {
    player := &Player{ Rect: resolv.newRectangle(0, 0, 16, 16) }
    player.Rect.SetData(player) 
    // Now if we ever need to get a reference to the Player from its Rect, we 
    // can do so by using the Rectangle's GetData() function. 
}

Also of interest, a Space itself satisfies the requirements for a Shape, so they can be checked against like any other Shape. This works like a complex Shape composed of smaller Shapes, where doing collision testing and resolution simply does the equivalent functions for each Shape contained within the Space. This means that you can make complex Shapes out of simple Shapes easily by adding them to a Space, and then using that Space wherever you would use a normal Shape.


var ship *resolv.Space
var world *resolv.Space

func Init() {

    world = resolv.NewSpace()

    ship = resolv.NewSpace()
    
    // Construct the ship!
    ship.Add(
        resolv.NewRectangle(0, 0, 16, 16), 
        resolv.NewLine(16, 0, 32, 16), 
        resolv.NewLine(32, 16, 16, 16))

    // Add the Ship to the game world!
    world.Add(ship)

    // Make something to dodge!
    bullet := resolv.NewRectangle(64, 8, 2, 2)
    bullet.SetTags("bullet")
    world.Add(bullet)

}

func Update() {

    // To make using Spaces as compound Shapes easier, you can use the Space's Move() 
    // function to move all Shapes contained within the Space by the specified delta
    // X and Y values.

    ship.Move(2, 0)

    bullets := world.FilterByTags("bullet")

    // Now this line will run if any bullet touches any part of our ship Space.
    if bullets.IsColliding(ship) {
        fmt.Println("OW! I ran into a bullet!")
    }

    // There are additional functions present in the Space struct to assist with finding
    // colliding objects, as well:
    for _, bullet := range bullets.getCollidingShapes(ship) {
        fmt.Println(bullet)
    }

}

Welp, that's about it. If you want to see more info, feel free to examine the main.go and world#.go tests to see how a couple of quick example tests are set up.

You can check out the GoDoc link here, as well.

Dependencies?

For using resolv with your projects, there are no external dependencies. resolv just uses the built-in "fmt" and "math" packages.

For the resolv tests, resolv requires raylib-go to handle input and draw the screen.

Shout-out Time!

Massive thanks to the developer of raylib; it's an awesome framework, and simplifies things CONSIDERABLY.

Thanks to gen2brain for maintaining raylib-go, as well!

Props to whoever made arcadepi.ttf! It's a nice font.

Thanks a lot to the SDL2 team for development, and thanks to veandco for maintaining the Golang SDL2 port, as well!

Thanks to the people who stopped by on my stream - they helped out a lot with a couple of the technical aspects of getting Go to do what I needed to, haha.

Documentation

Overview

Package resolv is a simple collision detection and resolution library mainly geared towards simpler 2D arcade-style games. Its goal is to be lightweight, fast, simple, and easy-to-use for game development. Its goal is to also not become a physics engine or physics library itself, but to always leave the actual physics implementation and "game feel" to the developer, while making it very easy to do so.

Usage of resolv essentially centers around two main concepts: Spaces and Shapes.

A Shape can be used to test for collisions against another Shape. That's really all they have to do, but that capability is powerful when paired with the resolv.Resolve() function. You can then check to see if a Shape would have a collision if it attempted to move in a specified direction. If so, the Resolve() function would return a Collision object, which tells you some information about the Collision, like how far the checking Shape would have to move to come into contact with the other, and which Shape it comes into contact with.

A Space is just a slice that holds Shapes for detection. It doesn't represent any real physical space, and so there aren't any units of measurement to remember when using Spaces. Similar to Shapes, Spaces are simple, but also very powerful. Spaces allow you to easily check for collision with, and resolve collision against multiple Shapes within that Space. A Space being just a collection of Shapes means that you can manipulate and filter them as necessary.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Distance

func Distance(x, y, x2, y2 int32) int32

Distance returns the distance from one pair of X and Y values to another.

Types

type BasicShape

type BasicShape struct {
	X, Y int32

	Data interface{}
	// contains filtered or unexported fields
}

BasicShape isn't to be used directly; it just has some basic functions and data, common to all structs that embed it, like position and tags. It is embedded in other Shapes.

func (*BasicShape) AddTags

func (b *BasicShape) AddTags(tags ...string)

AddTags adds the specified tags to the BasicShape.

func (*BasicShape) ClearTags

func (b *BasicShape) ClearTags()

ClearTags clears the tags active on the BasicShape.

func (*BasicShape) GetData

func (b *BasicShape) GetData() interface{}

GetData returns the data on the Shape.

func (*BasicShape) GetTags

func (b *BasicShape) GetTags() []string

GetTags returns a reference to the the string array representing the tags on the BasicShape.

func (*BasicShape) GetXY

func (b *BasicShape) GetXY() (int32, int32)

GetXY returns the position of the Shape.

func (*BasicShape) HasTags

func (b *BasicShape) HasTags(tags ...string) bool

HasTags returns true if the Shape has all of the tags provided.

func (*BasicShape) Move

func (b *BasicShape) Move(x, y int32)

Move moves the Shape by the delta X and Y values provided.

func (*BasicShape) RemoveTags

func (b *BasicShape) RemoveTags(tags ...string)

RemoveTags removes the specified tags from the BasicShape.

func (*BasicShape) SetData

func (b *BasicShape) SetData(data interface{})

SetData sets the data on the Shape.

func (*BasicShape) SetXY

func (b *BasicShape) SetXY(x, y int32)

SetXY sets the position of the Shape.

type Circle

type Circle struct {
	BasicShape
	Radius int32
}

A Circle represents an ordinary circle, and has a radius, in addition to normal shape properties.

func NewCircle

func NewCircle(x, y, radius int32) *Circle

NewCircle returns a pointer to a new Circle object.

func (*Circle) GetBoundingRect

func (c *Circle) GetBoundingRect() *Rectangle

GetBoundingRect returns a Rectangle which has a width and height of 2*Radius.

func (*Circle) IsColliding

func (c *Circle) IsColliding(other Shape) bool

IsColliding returns true if the Circle is colliding with the specified other Shape, including the other Shape being wholly within the Circle.

func (*Circle) WouldBeColliding

func (c *Circle) WouldBeColliding(other Shape, dx, dy int32) bool

WouldBeColliding returns whether the Circle would be colliding with the specified other Shape if it were to move in the specified direction.

type Collision

type Collision struct {
	ResolveX, ResolveY int32
	Teleporting        bool
	ShapeA             Shape
	ShapeB             Shape
}

Collision describes the collision found when a Shape attempted to resolve a movement into another Shape in an isolated check, or when within the same Space as other existing Shapes. ResolveX and ResolveY represent the displacement of the Shape to the point of collision. How far along the Shape got when attempting to move along the direction given by deltaX and deltaY in the Resolve() function before touching another Shape. Teleporting is if moving according to ResolveX and ResolveY might be considered teleporting, which is moving greater than the deltaX or deltaY provided to the Resolve function * 1.5 (this is arbitrary, but can be useful when attempting to see if a movement would be ). ShapeA is a pointer to the Shape that initiated the resolution check. ShapeB is a pointer to the Shape that the colliding object collided with, if the Collision was successful.

func Resolve

func Resolve(firstShape Shape, other Shape, deltaX, deltaY int32) Collision

Resolve attempts to move the checking Shape with the specified X and Y values, returning a Collision object if it collides with the specified other Shape. The deltaX and deltaY arguments are the movement displacement in pixels. For platformers in particular, you would probably want to resolve on the X and Y axes separately.

func (*Collision) Colliding

func (c *Collision) Colliding() bool

Colliding returns whether the Collision actually was valid because of a collision against another Shape.

type IntersectionPoint

type IntersectionPoint struct {
	X, Y  int32
	Shape Shape
}

IntersectionPoint represents a point of intersection from a Line with another Shape.

type Line

type Line struct {
	BasicShape
	X2, Y2 int32
}

Line represents a line, from one point to another.

func NewLine

func NewLine(x, y, x2, y2 int32) *Line

NewLine returns a new Line instance.

func (*Line) Center

func (l *Line) Center() (int32, int32)

Center returns the center X and Y values of the Line.

func (*Line) GetBoundingCircle

func (l *Line) GetBoundingCircle() *Circle

GetBoundingCircle returns a circle centered on the Line's central point that would fully contain the Line.

func (*Line) GetBoundingRectangle

func (l *Line) GetBoundingRectangle() *Rectangle

GetBoundingRectangle returns a rectangle centered on the center point of the Line that would fully contain the Line.

func (*Line) GetDelta

func (l *Line) GetDelta() (int32, int32)

GetDelta returns the delta (or difference) between the start and end point of a Line.

func (*Line) GetIntersectionPoints

func (l *Line) GetIntersectionPoints(other Shape) []IntersectionPoint

GetIntersectionPoints returns the intersection points of a Line with another Shape as an array of IntersectionPoints. The returned list of intersection points are always sorted in order of distance from the start of the casting Line to each intersection. Currently, Circle-Line collision is missing.

func (*Line) GetLength

func (l *Line) GetLength() int32

GetLength returns the length of the Line.

func (*Line) IsColliding

func (l *Line) IsColliding(other Shape) bool

IsColliding returns if the Line is colliding with the other Shape. Currently, Circle-Line collision is missing.

func (*Line) Move

func (l *Line) Move(x, y int32)

Move moves the Line by the values specified.

func (*Line) SetLength

func (l *Line) SetLength(length int32)

SetLength sets the length of the Line to the value provided.

func (*Line) SetXY

func (l *Line) SetXY(x, y int32)

SetXY sets the position of the Line, also moving the end point of the line (so it wholly moves the line to the specified position).

func (*Line) WouldBeColliding

func (l *Line) WouldBeColliding(other Shape, dx, dy int32) bool

WouldBeColliding returns if the Line would be colliding if it were moved by the designated delta X and Y values.

type Rectangle

type Rectangle struct {
	BasicShape
	W, H int32
}

Rectangle represents a rectangle.

func NewRectangle

func NewRectangle(x, y, w, h int32) *Rectangle

NewRectangle creates a new Rectangle and returns a pointer to it.

func (*Rectangle) Center

func (r *Rectangle) Center() (int32, int32)

Center returns the center point of the Rectangle.

func (*Rectangle) GetBoundingCircle

func (r *Rectangle) GetBoundingCircle() *Circle

GetBoundingCircle returns a circle that wholly contains the Rectangle.

func (*Rectangle) IsColliding

func (r *Rectangle) IsColliding(other Shape) bool

IsColliding returns whether the Rectangle is colliding with the specified other Shape or not, including the other Shape being wholly contained within the Rectangle.

func (*Rectangle) WouldBeColliding

func (r *Rectangle) WouldBeColliding(other Shape, dx, dy int32) bool

WouldBeColliding returns whether the Rectangle would be colliding with the other Shape if it were to move in the specified direction.

type Shape

type Shape interface {
	IsColliding(Shape) bool
	WouldBeColliding(Shape, int32, int32) bool
	GetTags() []string
	ClearTags()
	AddTags(...string)
	RemoveTags(...string)
	HasTags(...string) bool
	GetData() interface{}
	SetData(interface{})
	GetXY() (int32, int32)
	SetXY(int32, int32)
	Move(int32, int32)
}

Shape is a basic interface that describes a Shape that can be passed to collision testing and resolution functions and exist in the same Space.

type Space

type Space []Shape

A Space represents a collection that holds Shapes for collision detection in the same common space. A Space is arbitrarily large - you can use one Space for a single level, room, or area in your game, or split it up if it makes more sense for your game design. Technically, a Space is just a slice of Shapes. Spaces fulfill the required functions for Shapes, which means you can also use them as compound shapes themselves. In these cases, the first Shape is the "root" or pivot from which attempts to move the Shape will be focused. In other words, Space.SetXY(40, 40) will move all Shapes in the Space in such a way that the first Shape will be at 40, 40, and all other Shapes retain their original spacing relative to it.

func NewSpace

func NewSpace() *Space

NewSpace creates a new Space for shapes to exist in and be tested against in.

func (*Space) Add

func (sp *Space) Add(shapes ...Shape)

Add adds the designated Shapes to the Space. You cannot add the Space to itself.

func (*Space) AddTags

func (sp *Space) AddTags(tags ...string)

AddTags sets the provided tags on all Shapes contained within the Space.

func (*Space) Clear

func (sp *Space) Clear()

Clear "resets" the Space, cleaning out the Space of references to Shapes.

func (*Space) ClearTags

func (sp *Space) ClearTags()

ClearTags removes all tags from all Shapes within the Space.

func (*Space) Contains

func (sp *Space) Contains(shape Shape) bool

Contains returns true if the Shape provided exists within the Space.

func (*Space) Filter

func (sp *Space) Filter(filterFunc func(Shape) bool) *Space

Filter filters out a Space, returning a new Space comprised of Shapes that return true for the boolean function you provide. This can be used to focus on a set of object for collision testing or resolution, or lower the number of Shapes to test by filtering some out beforehand.

func (*Space) FilterByTags

func (sp *Space) FilterByTags(tags ...string) *Space

FilterByTags filters a Space out, creating a new Space that has just the Shapes that have all of the specified tags.

func (*Space) FilterOutByTags

func (sp *Space) FilterOutByTags(tags ...string) *Space

FilterOutByTags filters a Space out, creating a new Space that has just the Shapes that don't have all of the specified tags.

func (*Space) Get

func (sp *Space) Get(index int) Shape

Get allows you to get a Shape by index from the Space easily. This is a convenience function, standing in for (*space)[index].

func (*Space) GetCollidingShapes

func (sp *Space) GetCollidingShapes(shape Shape) *Space

GetCollidingShapes returns a Space comprised of Shapes that collide with the checking Shape.

func (*Space) GetData

func (sp *Space) GetData() interface{}

GetData returns the pointer to the object contained in the Data field of the first Shape within the Space. If there aren't any Shapes within the Space, it returns nil.

func (*Space) GetTags

func (sp *Space) GetTags() []string

GetTags returns the tag list of the first Shape within the Space. If there are no Shapes within the Space, it returns an empty array of string type.

func (*Space) GetXY

func (sp *Space) GetXY() (int32, int32)

GetXY returns the X and Y position of the first Shape in the Space. If there aren't any Shapes within the Space, it returns 0, 0.

func (*Space) HasTags

func (sp *Space) HasTags(tags ...string) bool

HasTags returns true if all of the Shapes contained within the Space have the tags specified.

func (*Space) IsColliding

func (sp *Space) IsColliding(shape Shape) bool

IsColliding returns whether the provided Shape is colliding with something in this Space.

func (*Space) Length

func (sp *Space) Length() int

Length returns the length of the Space (number of Shapes contained within the Space). This is a convenience function, standing in for len(*space).

func (*Space) Move

func (sp *Space) Move(dx, dy int32)

Move moves all Shapes in the Space by the displacement provided.

func (*Space) Remove

func (sp *Space) Remove(shapes ...Shape)

Remove removes the designated Shapes from the Space.

func (*Space) RemoveTags

func (sp *Space) RemoveTags(tags ...string)

RemoveTags removes the provided tags from all Shapes contained within the Space.

func (*Space) Resolve

func (sp *Space) Resolve(checkingShape Shape, deltaX, deltaY int32) Collision

Resolve runs Resolve() using the checking Shape, checking against all other Shapes in the Space. The first Collision that returns true is the Collision that gets returned.

func (*Space) SetData

func (sp *Space) SetData(data interface{})

SetData sets the pointer provided to the Data field of all Shapes within the Space.

func (*Space) SetXY

func (sp *Space) SetXY(x, y int32)

SetXY sets the X and Y position of all Shapes within the Space to the position provided using the first Shape's position as reference. Basically, it moves the first Shape within the Space to the target location and then moves all other Shapes by the same delta movement.

func (*Space) String

func (sp *Space) String() string

func (*Space) WouldBeColliding

func (sp *Space) WouldBeColliding(other Shape, dx, dy int32) bool

WouldBeColliding returns true if any of the Shapes within the Space would be colliding should they move along the delta X and Y values provided (dx and dy).

Notes

Bugs

  • Line.IsColliding() and Line.GetIntersectionPoints() doesn't work with Circles.

  • Line.IsColliding() and Line.GetIntersectionPoints() fail if testing two lines that intersect along the exact same slope.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL