Helper Utilities for a Multitude of Problems is a set of lightweight helpers for the awesome LÖVE Engine.
It features
hump differs from other libraries in that every component is independent of the remaining ones (apart from camera.lua, which does depends on vector.lua). hump's footprint is very small and thus should fit nicely into your projects.
Below is the documentation of the various modules. You can directly jump to a module by clicking these:
hump.gamestate = require "Gamestate"
A gamestate encapsulates independent data an behaviour into a single entity.A typical game could consist of a menu-state, a level-state and a game-over-state.
A gamestate can define (nearly) all callbacks that LÖVE defines. In addition, there are callbacks for entering and leaving a state.:
init()
switch()
.
enter(previous, ...)
switch()
.
leave()
switch()
.
update()
draw()
focus()
keypressed()
keyreleased()
mousepressed()
mousereleased()
joystickpressed()
joystickreleased()
quit()
When using registerEvents()
, all these callbacks will receive the same
arguments as the LÖVE callbacks do.
menu = Gamestate.new()
function menu:init() -- run only once
self.background = love.graphics.newImage('bg.jpg')
Buttons.initialize()
end
function menu:enter(previous) -- run every time the state is entered
Buttons.setActive(Buttons.start)
end
function menu:update(dt)
Buttons.update(dt)
end
function menu:draw()
love.graphics.draw(self.background, 0, 0)
Buttons.draw()
end
function menu:keyreleased(key)
if key == 'up' then
Buttons.selectPrevious()
elseif key == 'down' then
Buttons.selectNext()
elseif
Buttons.active:onClick()
end
end
function menu:mousereleased(x,y, mouse_btn)
local button = Buttons.hovered(x,y)
if button then
Button.select(button)
if mouse_btn == 'l' then
button:onClick()
end
end
end
Declare a new gamestate. A gamestate can define several callbacks
menu = Gamestate.new()
Switch to a gamestate, with any additional arguments passed to the new state.
Switching a gamestate will call the leave() callback on the current gamestate, replace the current gamestate with to, call the init() function if the state was not yet inialized and finally call enter(old_state, ...) on the new gamestate.
to
...
Gamestate.switch(game, level_two)
Calls the corresponding function on the current gamestate (see callbacks).
Only needed when not using registerEvents().
...
function love.update(dt)
Gamestate.update(dt)
end
function love.draw()
local mx,my = love.mouse.getPosition()
Gamestate.draw(mx, my)
end
function love.keypressed(key, code)
Gamestate.keypressed(key, code)
end
Register all love callbacks to call Gamestate.update(), Gamestate.draw(), etc. automatically.
This is by done by overwriting the love callbacks, e.g.:
local old_update = love.update
function love.update(dt)
old_update(dt)
Gamestate.current:update(dt)
end
Note: Only works when called in love.load()
or any other function that is executed
after the whole file is loaded.
function love.load()
Gamestate.registerEvents()
Gamestate.switch(menu)
end
hump.timer = require "Timer"
hump.timer provides a simple interface to use delayed functions, i.e. functions
that will be executed after some amount time has passed. For example, you can use
a timer to set the player invincible for a short amount of time.In addition, the module offers facilities to create functions that interpolate or oscillate over time. An interpolator could fade the color or a text message, whereas an oscillator could be used for the movement of foes in a shmup.
Add a timed function. The function will be executed after delay
seconds
have elapsed, given that update() is called every frame.
Note that there is no guarantee that the delay will not be exceeded, it is only guaranteed that the function will not be executed before the delay has passed.
func
will receive itself as only parameter. This is useful to implement
periodic behavior (see the example).
delay
func
-- grant the player 5 seconds of immortality
player.isInvincible = true
Timer.add(5, function() player.isInvincible = false end)
-- print "foo" every second. See addPeriodic.
Timer.add(1, function(func) print("foo") Timer.add(1, func) end)
Add a function that will be called count
times every delay
seconds.
If count
is omitted, the function will be called until it returns false
or clear() is called.
delay
func
count
(optional)Timer.addPeriodic(1, function() lamp:toggleLight() end)
Timer.addPeriodic(0.3, function() mothership:spawnFighter() end, 5)
-- flicker player's image as long as he is invincible
Timer.addPeriodic(0.1, function()
player:flipImage()
return player.isInvincible
end)
Remove all timed and periodic functions. Functions that have not yet been executed will discarded.
Timer.clear
Update timers and execute functions if the deadline is reached. Use this in love.update(dt).
dt
function love.update(dt)
do_stuff()
Timer.update(dt)
end
Create a wrapper for an interpolating function, i.e. a function that acts depending on how much time has passed.
The wrapper will have the prototype:
function wrapper(dt, ...)
where dt
is the time that has passed since the last call of the wrapper
and ...
are arguments passed to the interpolating function. It will return
whatever the interpolating functions returns if the interpolation is not yet
finished or nil if the interpolation is done.The prototype of the interpolating function is:
function interpolator(fraction, ...)
where fraction
is a number between 0 and 1 depending on how much time has
passed and ...
are additional arguments supplied to the wrapper.length
func
fader = Timer.Interpolator(5, function(frac, r,g,b)
love.graphics.setBackgroundColor(frac*r,frac*g,frac*b)
end)
function love.update(dt)
fader(dt, 255,255,255)
end
Create a wrapper for an oscillating function, which is basically a looping interpolating function.
The function prototypes are the same as with Interpolator():
function wrapper(dt, ...)
function oscillator(fraction, ...)
As with Interpolator, the wrapper will return whatever oscillator()
returns.
length
func
mover = Timer.Oscillator(10, function(frac)
return 400 + 300 * math.sin(2*math.pi*frac)
end)
local xpos = 100
function love.update(dt)
xpos = mover(dt)
end
function love.draw()
love.graphics.circle('fill', xpos, 300, 80, 36)
end
hump.vector = require "vector"
A handy 2D vector class providing most of the things you do with vectors. You can access the individual coordinates by using vec.x
and vec.y
.
Vector arithmetic is implemented by using __add
, __mul
and other metamethods:
vector + vector = vector
vector - vector = vector
vector * vector = number
number * vector = vector
vector * number = vector
vector / number = vector
Relational operators are defined, too:
true
, if a.x == b.x
and a.y == b.y
.
true
, if a.x <= b.x
and a.y <= b.y
.
true
, if a.x < b.x
or a.x == b.x
and a.y < b.y
.
-- acceleration, player.velocity and player.position are vectors
acceleration = vector(0,-9)
player.velocity = player.velocity + acceleration * dt
player.position = player.position + player.velocity * dt
Create a new vector.
x,y
a = vector.new(10,10)
-- as a shortcut, you can call the module like a function:
vector = require "hump.vector"
a = vector(10,10)
Test whether a variable is a vector.
v
true
if v
is a vector, false
otherwiseif not vector.isvector(v) then
v = vector(v,0)
end
Copy a vector. Simply assigning a vector a vector to a variable will create a reference, so when modifying the vector referenced by the new variable would also change the old one:
a = vector(1,1) -- create vector
b = a -- b references a
c = a:clone() -- c is a copy of a
b.x = 0 -- changes a,b and c
print(a,b,c) -- prints '(1,0), (1,0), (1,1)'
copy = original:clone
Extract coordinates.
x,y = pos:unpack()
love.graphics.draw(self.image, self.pos:unpack())
Multiplies vectors coordinate wise, i.e. result = vector(a.x * b.x, a.y * b.y)
.
This does not change either argument vectors, but creates a new one.
other
scaled = original:permul(vector(1,1.5))
Get length of a vector, i.e. math.sqrt(vec.x * vec.x + vec.y * vec.y)
.
distance = (a - b):len()
Get squared length of a vector, i.e. vec.x * vec.x + vec.y * vec.y
.
-- get closest vertex to a given vector
closest, dsq = vertices[1], (pos - vertices[1]):len2()
for i = 2,#vertices do
local temp = (pos - vertices[i]):len2()
if temp < dsq then
closest, dsq = vertices[i], temp
end
end
Get distance of two vectors. The same as (a - b):len()
.
other
-- get closest vertex to a given vector
-- slightly slower than the example using len2()
closest, dist = vertices[1], pos:dist(vertices[1])
for i = 2,#vertices do
local temp = pos:dist(vertices[i])
if temp < dist then
closest, dist = vertices[i], temp
end
end
Get normalized vector, i.e. a vector with the same direction as the input vector, but with length 1.
This does not change the input vector, but creates a new vector.
direction = velocity:normalized()
Normalize a vector, i.e. make the vector unit length. Great to use on intermediate results.
This modifies the vector. If in doubt, use vector:normalized()
.
normal = (b - a):perpendicular():normalize_inplace()
Get a rotated vector.
This does not change the input vector, but creates a new vector.
phi
-- approximate a circle
circle = {}
for i = 1,30 do
local phi = 2 * math.pi * i / 30
circle[#circle+1] = vector(0,1):rotated(phi)
end
Rotate a vector in-place. Great to use on intermediate results.
This modifies the vector. If in doubt, use vector:rotate()
phi
-- ongoing rotation
spawner.direction:rotate_inplace(dt)
Quick rotation by 90°. Creates a new vector. The same as (but faster):
vec:rotate(math.pi/2)
normal = (b - a):perpendicular():normalize_inplace()
Project vector onto another vector (see sketch).
v
velocity_component = velocity:projectOn(axis)
Mirrors vector on the axis defined by the other vector.
v
deflected_velocity = ball.velocity:mirrorOn(surface_normal)
Get cross product of both vectors. Equals the area of the parallelogram spanned by both vectors.
other
parallelogram_area = a:cross(b)
hump.class = require "Class"
A small, fast class implementation with multiple inheritance supportDeclare a new class.
The constructor will receive the newly create object as first argument.
If no name is given, the module will try to guess the name based on the variable name the class is assigned to. Note that this can only work for global variables.
You can check if an object is an instance of a class using object:is_a()
.
The name of the variable that holds the module can be used as a shortcut to
new()
(see example).
constructor
(optional)theclass.construct(object, ...)
the_name
(optional)super
(optional)Class = require 'hump.class' -- `Class' is now a shortcut to new()
-- define class with implicit name 'Feline'
Feline = Class{function(self, size, weight)
self.size = size
self.weight = weight
end}
print(Feline) -- prints 'Feline'
-- define class method
function Feline:stats()
return string.format("size: %.02f, weight %.02f", self.size, self.weight)
end
-- create two objects
garfield = Feline(.7, 45)
felix = Feline(.8, 12)
print("Garfield: " .. garfield:stats(), "Felix: " .. felix:stats())
Class = require 'hump.class'
-- define class with explicit name 'Feline'
Feline = Class{name = "Feline", function(self, size, weight)
self.size = size
self.weight = weight
end}
garfield = Feline(.7, 45)
print(Feline, garfield) -- prints 'Feline <instance of Feline>'
Class = require 'hump.class'
A = Class{}
function A:foo()
print('foo')
end
B = Class{}
function B:bar()
print('bar')
end
-- single inheritance
C = Class{inherits = A}
instance = C()
instance:foo() -- prints 'foo'
-- multiple inheritance
D = Class{inherits = {A,B}}
instance = D()
instance:foo() -- prints 'foo'
instance:bar() -- prints 'bar'
Calls class constructor of a class on an object
Derived classes use this function their constructors to initialize the parent class(es) portions of the object.
object
self
....
Class = require 'hump.class'
Shape = Class{function(self, area)
self.area = area
end}
function Shape:__tostring()
return "area = " .. self.area
end
Rectangle = Class{inherits = Shape, function(self, width, height)
Shape.construct(self, width * height)
self.width = width
self.height = height
end}
function Rectangle:__tostring()
local strs = {
"width = " .. self.width,
"height = " .. self.height,
Shape.__tostring(self)
},
return table.concat(strs, ", ")
end
print( Rectangle(2,4) ) -- prints 'width = 2, height = 4, area = 8'
Menu = Class{function(self)
self.entries = {}
end}
function Menu:add(title, entry)
self.entries[#self.entries + 1] = entry
end
function Menu:display()
-- ...
end
Entry = Class{function(self, title, command)
self.title = title
self.command = command
end}
function Entry:execute()
return self.command()
end
Submenu = Class{inherits = {Menu, Entry}, function(self, title)
Menu.construct(self)
-- redirect self:execute() to self:display()
Entry.construct(self, title, Menu.display)
end}
Inherit functions and variables of another class, if they are not already defined for the class. This is done by simply copying the functions and variables over to the subclass. The Lua rules for copying apply (i.e. tables are referenced, functions and primitive types are copied by value).
Be careful with changing table values in a subclass: This will change the value in the parent class too.
If more than one parent class is specified, inherit from all of these, in order of occurrence. That means that when two parent classes define the same method, the one from the first class will be inherited.
Note: class:inherit()
doesn't actually care if the arguments supplied are
hump classes. Just any table will work.
...
Class = require 'hump.class'
Entity = Class{function(self)
GameObjects.register(self)
end}
Collidable = {
dispatch_collision = function(self, other, dx, dy)
if self.collision_handler[other.type])
return collision_handler[other.type](self, other, dx, dy)
end
return collision_handler["*"](self, other, dx, dy)
end,
collision_handler = {["*"] = function() end},
}
Spaceship = Class{function(self)
self.type = "Spaceship"
-- ...
end}
-- make Spaceship collidable
Spaceship:inherit(Collidable)
function Spaceship:collision_handler["Spaceship"](other, dx, dy)
-- ...
end
Tests whether an object is an instance of a class.
cls
true
if the object is an instance of the class, false
otherwiseClass = require 'hump.class'
A = Class{}
B = Class{inherits=A}
C = Class{inherits=B}
a, b, c = A(), B(), C()
print(a:is_a(A), a:is_a(B), a:is_a(C)) --> true false false
print(b:is_a(A), b:is_a(B), b:is_a(C)) --> true true false
print(c:is_a(A), c:is_a(B), c:is_a(C)) --> true true true
D = Class{}
E = Class{inherits={B,D}}
d, e = D(), E()
print(d:is_a(A), d:is_a(B), d:is_a(D)) --> false false true
print(e:is_a(A), e:is_a(B), e:is_a(D)) --> true true true
Be careful when using metamethods like __add
or __mul
: If subclass
inherits those methods from a superclass, but does not overwrite them, the
result of the operation may be of the type superclass. Consider the following:
Class = require 'hump.class'A = Class{function(self, x) self.x = x end}
function A:__add(other) return A(self.x + other.x) end
function A:show() print("A:", self.x) end
B = Class{inherits = A, function(self, x, y) A.construct(self, x) self.y = y end}
function B:show() print("B:", self.x, self.y) end
function B:foo() print("foo") end
one, two = B(1,2), B(3,4)
result = one + two
result:show() -- prints "A: 4"
result:foo() -- error: method does not exist
Note that while you can define the __index
metamethod of the class, this
is not a good idea: It will break the class. To add a custom __index
metamethod without breaking the class system, you have to use rawget().
But beware that this won't affect subclasses:
Class = require 'hump.class'A = Class{}
function A:foo() print('bar') end
function A:__index(key)
print(key)
return rawget(A, key)
end
instance = A()
instance:foo() -- prints foo <newline> bar
B = Class{inherits = A}
instance = B()
instance:foo() -- prints only foo
hump.camera = require "Camera"
Depends on hump.vectorA camera utility for LÖVE. A camera can "look" at a position. It can zoom in and out and it can rotate it's view. In the background, this is done by actually moving, scaling and rotating everything in the game world. But don't worry about that.
Creates a new camera object. You can access the camera position using
camera.pos
, the zoom using camera.zoom
and the rotation using
camera.rot
.
The module variable name can be used at a shortcut to new()
.
pos
(screen center)zoom
(1)rot
(0)camera = require 'hump.camera'
vector = require 'hump.vector'
-- camera looking at (100,100) with zoom 2 and rotated by 45 degrees
cam = camera(vector(100,100), 2, math.pi/2)
Rotate the camera by some angle. To set the angle use
camera.rot = new_angle
.
This function is shortcut to camera.rot = camera.rot + angle
.
angle
function love.update(dt)
camera:rotate(dt)
end
function love.update(dt)
camera:rotate(dt):move(dt)
end
Move the camera by some vector. To set the position, use
camera.pos = some_vector
or camera.pos.x, camera.pos.y = new_x, new_y
.
This function is shortcut to camera.pos = camera.pos + v
.
v
x, y
function love.update(dt)
camera:move(dt * velocity)
end
function love.update(dt)
camera:move(dt * 5, dt * 6):rotate(dt)
end
Start looking through the camera.
Apply camera transformations, i.e. move, scale and rotate everything until
camera:detach()
as if looking through the camera.
function love.draw()
camera:attach()
draw_world()
cam:detach()
draw_hud()
end
Stop looking through the camera.
function love.draw()
camera:attach()
draw_world()
cam:detach()
draw_hud()
end
Wrap a function between a camera:attach()
/camera:detach()
pair:
cam:attach()
func()
cam:detach()
func
function love.draw()
camera:draw(draw_world)
draw_hud()
end
Because a camera has a point it looks at, a rotation and a zoom factor, it defines a coordinate system. A point now has two sets of coordinates: One defines where the point is to be found in the game world, and the other describes the position on the computer screen. The first set of coordinates is called world coordinates, the second one camera coordinates. Sometimes it is needed to convert between the two coordinate systems, for example to get the position of a mouse click in the game world in a strategy game, or to see if an object is visible on the screen.
These two functions convert a point between these two coordinate systems.
v
x, y
target = camera:worldCoords( vector(love.mouse.getPosition()) )
selectedUnit:plotPath(target)
pos = cam:toCameraCoords(player.pos)
love.graphics.line(pos.x, pos.y, love.mouse.getPosition())
Shortcut to camera:worldCoords(vector(love.mouse.getPosition()))
.
target = camera:mousepos()
selectedUnit:plotPath(target)
hump.ringbuffer = require "Ringbuffer"
A ring-buffer is a circular array: It does not have a first nor a last item,
but it has a selected or current element.A ring-buffer can be used to implement Tomb Raider style inventories, looping play-lists, recurring dialogs (like a unit's answers when selecting it multiple times in Warcraft) and generally everything that has a circular or looping structure.
Create new ring-buffer.
The module name is a shortcut to this function.
...
Ringbuffer = require 'hump.ringbuffer'
rb = ringbuffer(1,2,3)
Insert items behind current element.
...
rb = RingbuffeR(1,5,6) -- content: 1,5,6
rb:insert(2,3,4) -- content: 1,2,3,4,5,6
Remove current item, return it and select next element.
rb = Ringbuffer(1,2,3,4) -- content: 1,2,3,4
val = rb:remove() -- content: 2,3,4
print(val) -- prints `1'
Remove the item at a position relative to the current element.
pos
rb = Ringbuffer(1,2,3,4,5) -- content: 1,2,3,4,5
rb:removeAt(2) -- content: 1,2,4,5
rb:removeAt(-1) -- content: 1,2,4
Select and return the next element.
rb = Ringbuffer(1,2,3)
rb:next() -- content: 2,3,1
rb:next() -- content: 3,1,2
x = rb:next() -- content: 1,2,3
print(x) -- prints `1'
Select and return the previous item.
rb = Ringbuffer(1,2,3)
rb:prev()) -- content: 3,1,2
rb:prev()) -- content: 2,3,1
x = rb:prev() -- content: 1,2,3
print(x) -- prints `1'
Return the current element.
rb = Ringbuffer(1,2,3)
rb:next() -- content: 2,3,1
print(rb:get()) -- prints '2'
Get number of items in the buffer
rb = Ringbuffer(1,2,3)
print(rb:size()) -- prints '3'
rb:remove()
print(rb:size()) -- prints '2'
Yay, free software:
Copyright (c) 2010 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.