hump is a collection of lightweight, yet powerful modules to speed up game development with LÖVE. It offers solutions to various small but annoyingly reccurent problems.
You can download the latest packaged version of hump as zip- or tar-archive directly from github. You can also view and download the sourcecode of individual modules here.
If you use the Git command line client, you can clone the repository by running:
git clone git://github.com/vrld/hump
Once done, you can check for updates by running:
git pull
from inside the directory.
Copyright (c) 2010-2011 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.
Gamestate = require "hump.gamestate"
A gamestate encapsulates independant 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.:
When using registerEvents(), all these callbacks will receive the same arguments as the LÖVE callbacks do.
menu = Gamestate.new()
function menu:enter(previous, background_image)
self.background = background_image
Buttons.initialize()
end
function menu:leave()
Buttons.cleanup()
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
and finally call
enter(old_state, ...)
on the new gamestate.
to
...
to:enter()
.to:enter()
.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 achieved by overwriting the love callbacks, e.g.:
local _update = love.update
function love.update(dt)
_update(dt)
Gamestate.current:update(dt)
end
function love.load()
Gamestate.registerEvents()
Gamestate.switch(menu)
end
Timer = require "hump.timer"
hump.timer provides a simple interface to use delayed functions, i.e. functions that will only be executed after some amount time.
In addition, the module offers facilities to create functions that interpolate or oscillate over time.
Add a timed function. The function will be executed when delay
seconds have elapsed.
Note that there is no guarantee that the delay will be exceeded. It is, however, guaranteed that the function will not be executed before the delay has passed.
If the function is called, it will receive itself as only parameter. This may be useful to implement
the periodic behavior of Timer.addPeriodic
.
delay
func
-- grant the player 5 seconds of immortality
player:setInvincible(true)
Timer.add(5, function() player:setInvincible(false) end)
-- print "foo" every second
Timer.add(1, function(func) print("foo") Timer.add(1, func) end)
Add a periodic timed function, i.e. a function that will be called count
times every delay
seconds.
If count
is omitted, the function loops until clear()
is called.
delay
func
count
Timer.addPeriodic(1, function() lamp:toggleLight() end)
Timer.addPeriodic(0.3, function() mothership:spawnFighter() end, 5)
Update timers and execute functions if the deadline is reached. Use this in love.update()
.
dt
update()
.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, ...)
dt
is the time that has passed since the last call 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 inter(fraction, ...)
where fraction
is a number between 0 and 1 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, ...)
The wrapper function 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
vector = require "hump.vector"
A handy 2D vector class defining the most common 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:
a == b
true
, if a.x == b.x
and a.y == b.y
.a <= b
true
, if a.x <= b.x
and a.y <= b.y
.a < b
true
, if a.x < b.x
or a.x == b.x
and a.y < b.y
.acceleration = vector(0,-9)
player.velocity = player.velocity + acceleration * dt
player.position = player.position + player.velocity * dt
Create new vector
x, y
a = vector.new(10,10)
As a shortcut, you can call the module like a function, i.e.:
vector = require 'hump.vector'
a = vector(10,10)
Test whether a variable is a vector.
v
true
if v
is a vector, false
otherwise.if 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 you modify the vector referenced by the new variable, will 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 length 1.
This does not change the input vector, but creates a new vector.
normal = edge:normalized()
Normalize a vector, i.e. make the vector unit length.
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.
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.
v
velocity_component = velocity:projectOn(axis)
Get cross product of both vectors.
For the math geeks:
The cross product not be defined for 2D vectors. To nonetheless get a result, treat the vectors as being 3D vectors (x,y,0). The cross product of both vectors has just a z-component, and this is what this function returns. It's also the determinant of both vectors: d = det(a,b).
other
parallelogram_area = a:cross(b)
Class = require "hump.class"
A small, handy class implementation with multiple inheritance.
Define a new class.
The constructor will receive the newly created object as first argument.
If you require
d the module to a variable, you can use the variable
as a shortcut to new()
.
constructor
the_name
tostring()
on the class.super
Class = require 'hump.class'
-- define unnamed class
Feline = Class(function(self, size, weight)
self.size = size
self.weight = weight
end)
-- 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 named class
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{name = 'A'}
function A:foo()
print('foo')
end
B = Class{name = 'B'}
function C:bar()
print('bar')
end
-- single inheritance
C = Class{name = 'C', inherits = A}
instance = C()
instance:foo() -- prints 'foo'
-- multiple inheritance
D = Class{name = 'D', inherits = {A,B}}
instance = D()
instance:foo() -- prints 'foo'
instance:bar() -- prints 'bar'
Calls class constructor of a class.
Needed in constructors of child classes to initialize parts of the object that the parent classes define.
object
self
....
Class = require 'hump.class'
Feline = Class{name = "Feline", function(self, size, weight)
self.size = size
self.weight = weight
end}
Cat = Class{name = "Cat", function(self, name, size, weight)
Feline.construct(self, size, weight)
self.name = name
end}
Cat:inherit(Feline)
Inherit functions (but not class variables).
If multiple super-classes are defined inherit from all of these. If two super-classes define a method of the same name, inherit it from the one mentioned first.
class
super, ...
Class = require 'hump.class'
Feline = Class{name = "Feline", function(self, size, weight)
self.size = size
self.weight = weight
end}
function Feline:stats()
return string.format("size: %.02f, weight %.02f", self.size, self.weight)
end
function Feline:speak() print("meow") end
Cat = Class{name = "Cat", function(self, name, size, weight)
Feline.construct(self, size, weight)
self.name = name
end}
Cat:inherit(Feline)
function Cat:stats()
return string.format("name: %s, %s", self.name, Feline.stats(self))
end
Tiger = Class{name = "Tiger", function(self, size, weight)
Feline.construct(self, size, weight)
end}
Tiger:Inherit(Feline)
function Tiger:speak() print("ROAR!") end
felix = Cat("Felix", .8, 12)
hobbes = Tiger(2.2, 68)
print(felix:stats()) -- "name: Felix, size: 0.80, weight 12.00"
print(hobbes:stats()) -- "size: 2.20, weight 68.00"
felix:speak() -- "meow"
hobbes:speak() -- "ROAR!"
Be careful when using metamethods like __add
or __mul
:
When 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(function(self, x, y) A.construct(self, x) self.y = y end)
B:Inherit(A)
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
camera = require "hump.camera"
Depends on vector
Camera abstraction for LÖVE. A camera "looks" at a position and can be moved, zoomed and rotated.
A camera defines it's own coordinate system, meaning that an object shown on the screen likely has different coordinates in the game world than it has on the screen.
For example, the mouse position could be at pixel 400,400
on the screen
(= camera coordinates), but the camera looks at the point 100,100
and
is rotated by 90°
. The world coordinates of the mouse cursor are 200,100
.
The camera class defines methods to convert between both coordinate systems.
Create new camera object.
You can access and modify the camera parameters using camera.pos
, camera.zoom
and
camera.rot
.
pos
(screen center)zoom
(1)rot
(0)cam = hump.camera.new(vector(400,300), 2)
If you assigned the module to a variable, you can call the module as a
shortcut to hump.camera.new()
:
camera = require 'hump.camera'
vector = require 'hump.vector'
cam = camera(vector(400,300), 2)
Rotate the camera. Same as cam.rot = cam.rot + phi
.
phi
cam:rotate(dt)
Move the camera. Same as cam.pos = cam.pos + direction
direction
cam:translate(vector(100,0) * dt)
Apply camera transformations, i.e. move, scale and rotate everything so that you see what you would see when looking through the camera.
Everything until the next camera:postdraw()
will be transformed.
function love.draw()
cam:predraw()
draw_world()
cam:postdraw()
draw_hud()
end
Revert camera transformations done by camera:predraw()
.
function love.draw()
cam:predraw()
draw_world()
cam:postdraw()
draw_hud()
end
Wrap a function between predraw()
and postdraw()
:
cam:predraw()
func()
cam:postdraw()
func
function love.draw()
cam:draw( draw_world )
draw_hud()
end
Convert world coordinates to camera coordinates.
pos
screen_pos = cam:toCameraCoords( player.pos )
love.graphics.line(love.mouse.getX(), love.mouse.getY(), screen_pos:unpack()
Convert camera coordinates to world coordinates.
pos
target = cam:toWorldCoords( vector(love.mouse.getPosition()) )
unit:plotPathTo(target)
ringbuffer = require "hump.ringbuffer"
A ring-buffer is a circular array. That means it does not have a first nor a last, but only a selected/current element.
You can use this to implement Tomb Raider style inventories, looping playlists, 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 ringbuffer.
...
Insert items behind current element.
item, ...
rb = ringbuffer(1,5,6) -- content: 1,5,6
rb:insert(2,3,4) -- content: 1,2,3,4,5,6
Remove currently selected item and select next item.
rb = ringbuffer(1,2,3,4) -- content: 1,2,3,4
rb:next() -- content: 2,3,4,1
rb:remove() -- content: 3,4,1
Remove item at position relative to current item.
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 next item.
rb = ringbuffer(1,2,3)
print(rb:next()) -- prints '2'
print(rb:next()) -- prints '3'
print(rb:next()) -- prints '1'
Select and return previous item.
rb = ringbuffer(1,2,3)
print(rb:prev()) -- prints '3'
print(rb:prev()) -- prints '2'
print(rb:prev()) -- prints '1'
Get currently selected item.
rb = ringbuffer(1,2,3)
rb:next()
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'