-
Notifications
You must be signed in to change notification settings - Fork 2
Tutorial 01: Basic collisions
Welcome to the Cirno tutorial series that teaches how to use the library to detect and resolve collisions between geometric primitives (aka hitboxes).
I will use Pixel for graphics rendering because it's convenient for me. Below is the template that will be used in this tutorial and in the further ones.
package main
import (
"fmt"
"time"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
colors "golang.org/x/image/colornames"
)
const (
width = 1280
height = 720
)
func run() {
// Create a new window.
cfg := pixelgl.WindowConfig{
Title: "Cirno tutorial",
Bounds: pixel.R(0, 0, width, height),
}
win, err := pixelgl.NewWindow(cfg)
handleError(err)
// Setup metrics.
last := time.Now()
fps := 0
perSecond := time.Tick(time.Second)
for !win.Closed() {
deltaTime := time.Since(last).Seconds()
last = time.Now()
win.Clear(colors.White)
win.Update()
fps++
select {
case <-perSecond:
win.SetTitle(fmt.Sprintf("%s | FPS: %d | dt: %f",
cfg.Title, fps, deltaTime))
fps = 0
default:
}
}
}
func main() {
pixelgl.Run(run)
}
func handleError(err error) {
if err != nil {
panic(err)
}
}
This code just casually creates a 1280x720 window to draw graphics objects in it.
All the geometric primitives for creating hitboxes are called shapes in Cirno. There are three types of shapes:
Each of these types implements the Shape interface so here is a list of actions you can perform on shapes:
- some affine transformations (translation and rotation only, no scaling);
- assigning and checking tags for collision (the tag system will be explained later in the series);
- assigning and getting data (usually it's a game object the shape is connected to);
- computing normals between shapes (will be explained further as well);
- checking for collisions (overlaps) between shapes;
- checking for contact points between shapes;
- adding shapes to a space.
The first shape we are going to create is a rectangle:
rect, err := cirno.NewRectangle(
cirno.NewVector(540, 460), 120, 60, 30.0)
handleError(err)
NewRectangle
takes 4 arguments:
- the coordinates of the rectangle center of type Vector that is a representation of geometric 2D vector;
- rectangle width of type
float64
; - rectangle height of type
float64
; - rotation angle in degrees of type
float64
.
The distinctive feature of OBB is that it has an orientation and can be rotated. Unlike OBBs, AABBs (Axis-Aligned bounding boxes) cannot be rotated and their sides are always aligned with the xOy coordinate axes. But they have faster collision checking as an advantage so that's why AABBs are used in Cirno for quad tree quadrants. As for the API, it only provides access to OBB (which is cirno.Rectangle
).
Now let's create a circle:
circle, err := cirno.NewCircle(
cirno.NewVector(350, 250), 50)
handleError(err)
It's constructor takes only 2 arguments:
- the coordinates of the center as a vector;
- the radius of the circle as a
float64
.
There is no sense to rotate circle so rotation methods do nothing on it.
The last thing we'd want to create is a line:
line, err := cirno.NewLine(
cirno.NewVector(800, 200),
cirno.NewVector(1200, 400))
handleError(err)
The line constructor takes coordinates of the line segment ends as arguments.
In games shapes are connected to game objects. For example, a round bullet has a circle type hitbox so the hitbox should have a link to the object it's connected to. But in our case there are no game objects. What we really need is to draw shapes themselves. So let's just specify the color:
rect.SetData(colors.Red)
circle.SetData(colors.Red)
line.SetData(colors.Red)
You can specify literally anything as shape data but of course you'll have to do type assertion upon data extraction:
data, ok := rect.Data().(color.RGBA)
So it's better to be consistent in what you store as shape data.
Now let's add all the newly created shapes to a slice:
shapes := []cirno.Shape{rect, circle, line}
To draw them, we'll use a special utility from Pixel called IMDraw (pixel/imdraw
needs to be imported).
imd := imdraw.New(nil)
Here's the code of the procedure that asserts the type of the shape and obtains all the necessary information required for the shape to be drawn on the window surface:
// drawShapes draws all the specified shapes
// on the IMDraw canvas.
func drawShapes(imd *imdraw.IMDraw, shapes []cirno.Shape) {
for _, shape := range shapes {
imd.Color = shape.Data().(color.RGBA)
switch obj := shape.(type) {
case *cirno.Line:
// P is the one end of the line segment,
// and Q is the other.
imd.Push(
pixel.V(obj.P().X, obj.P().Y),
pixel.V(obj.Q().X, obj.Q().Y),
)
imd.Line(2)
case *cirno.Circle:
imd.Push(pixel.V(obj.Center().X,
obj.Center().Y))
imd.Circle(obj.Radius(), 0)
case *cirno.Rectangle:
// Obtain the vertices of the rectangle.
vertices := obj.Vertices()
imd.Push(
pixel.V(vertices[0].X, vertices[0].Y),
pixel.V(vertices[1].X, vertices[1].Y),
pixel.V(vertices[2].X, vertices[2].Y),
pixel.V(vertices[3].X, vertices[3].Y),
)
imd.Polygon(0)
}
}
}
I won't tell how IMDraw works. It's out of the tutorial's scope. If you want to know more about it, just check the corresponding article on Pixel wiki. Finally, we can draw our shapes.
win.Clear(colors.White)
imd.Clear()
drawShapes(imd, shapes)
imd.Draw(win)
win.Update()
If you did everything properly, you should see something like this on the screen: