| media | ||
| src | ||
| tests | ||
| .gitignore | ||
| config.nims | ||
| kirpi.nimble | ||
| LICENSE | ||
| minshell.html | ||
| nimble.paths | ||
| readme.md | ||
A lightweight 2D game framework featuring a clean, Löve2D-inspired graphics API (Löve2D developers will feel right at home). It uses Naylib (Raylib) as its well-maintained cross-platform backend within the Nim ecosystem.
Supported Build Targets: Web(Wasm),Linux,Windows,Macos,Android
Why kirpi?
- Very small Web builds for a WASM-based runtime. Performance-first builds of an empty project ship at ~750 KB uncompressed and ~350 KB zipped, comparable to many JavaScript game frameworks.
- Easy to learn with a minimal, well-placed abstraction layer. Want to use an ECS library? Bring your own. Need a physics engine or just a simple collider library? Your call.
- Straightforward multi-platform builds thanks to the configuration in the template project, including Android. Each platform also gets a clean, organized folder structure.
- You write your game in Nim, a pleasant and elegant language that’s easy to pick up and often delivers near-C performance. Nim uses ARC (Automatic Reference Counting), a deterministic, low-overhead memory model similar to C++’s RAII.
And really, the motivation behind Kirpi explains it best: tiny web builds, a fun and elegant language to work in, great performance, a minimal API you can build an ecosystem around, and fully compiled (non-VM) games.
Getting Started
Install kirpi easily with nimble install kirpi.
To compile a Kirpi project for your desktop platform, the Nim toolchain and compiler are sufficient. The command nim c -r game.nim will do the job.
#game.nim
import kirpi
var sampleText:Text
proc load() =
sampleText=newText("Hello World!",getDefaultFont())
proc update(dt: float) =
if isKeyPressed(KeyboardKey.Escape):
quit()
proc draw() =
clear(Black)
setColor(White)
draw(sampleText,100,100)
run( "sample game",load,update,draw)
However, for smooth builds across all supported platforms and an ideal folder structure, we recommend using the kirpi_app_template repository. This repo also includes extensively customized compiler configuration files, thanks in part to the Naylib community, which automate nearly everything for Android builds. Additionally, we provide customized Emscripten configurations for Web builds. In short, you can deploy your project to Android with just a few commands, and to other platforms with a single command. Detailed build instructions are available in our template repository.
Learning Samples
Flappy Bird
🦔 Repo | 🎮 Play Now |
Snake
🦔 Repo | 🎮 Play Now |
Simple Platformer
🦔 Repo | 🎮 Play Now |
Documentation
The examples repo is here.
Kirpi doesn’t have fancy tutorials yet. But honestly, the entire API is basically the cheatsheet below. We weren’t joking about the simplicity of the API.
Cheatsheet
### CALLBACK FUNCTIONS
load() # to do one-time setup of your game
update(dt:float) # which is used to manage your game's state frame-to-frame
draw() # which is used to render the game state onto the screen
config(settings:AppSettings) = # which is used to config the game app
#all properties with default values.
settings.fps=60
settings.printFPS=false
settings.printFrameTime=false
settings.defaultTextureFilter=TextureFilterSettings.Linear
settings.antialias=true
setting.iconPath="" # Should be RGBA 32bit PNG
settings.window.width=800
settings.window.height=600
settings.window.borderless=false
settings.window.resizeable= false
settings.window.minWidth=1
settings.window.minHeight=1
settings.window.fullscreen=false
settings.window.alwaysOnTop=false
getFramesPerSecond() # Returns FPS
getFrameMiliseconds() # Returns time in seconds for last frame (delta time)
getTime() # Returns the elapsed time in seconds since the application started.
# opens a window and runs the game
run(title:string,load: proc(), update: proc(dt:float), draw: proc(), config : proc (settings : var AppSettings)=nil)
### GRAPHICS
#Coordinate System
applyTransform(t: Transform) #applies the given Transform object to the current coordinate transformation.
origin() #resets the current coordinate transformation.
push() #copies and pushes the current coordinate transformation to the transformation stack.
pop() #pops the current coordinate transformation from the transformation stack.
translate(dx:float,dy:float) #translates the coordinate system in two dimensions.
rotate(angle:float) #rotates the coordinate system in two dimensions.
scale(sx:float,sy:float) #scales the coordinate system in two dimensions.
shear(shx:float,shy:float) #shears the coordinate system.
transformPoint(x:float,y:float) #converts the given 2D position from global coordinates into screen-space.
inverseTransformPoint(x:float,y:float) #converts the given 2D position from screen-space into global coordinates.
replaceTransform(t: Transform) #replaces the current coordinate transformation with the given Transform object.
#Object Creation
newTexture(filename:string):Texture #creates a new Texture.
newFont(filename:string, antialias:bool=true, rasterSize:int=32):Font #creates a new Font
newShader(vertexShaderFile: string, fragmentShaderFile: string) #creates a new shader. If you don't want to use a vertex shader, set the vertexShaderFile argument to an empty string("")
newText(text:string, font:ptr rl.Font):Text #creates a new drawable Text object.
newQuad(x,y,width,height,sw,sh:int):Quad #creates a new Quad.
newQuad(x,y,width,height:int, texture:var Texture):Quad #creates a new Quad (it just uses the texture to get width&height properties).
newSpriteBatch(texture:var Texture, maxSprites:int=1000): SpriteBatch #creates a new SpriteBatch
#Drawing State
setColor (r:uint8, g:uint8,b:uint8, a:uint8) #sets the color used for drawing.
setColor (color:Color) #sets the color used for drawing.
getColor () :Color #gets the current color
setLine(width:float,joinType:JoinTypes=JoinTypes.Miter,beginCap:CapTypes,endCap:CapTypes=beginCap) # sets the line stroke parameters (width, join type, and cap styles).
setLineWidth(width:float) #sets the line stroke width.
getLineWidth():float #returns the current line stroke width.
setLineJoin(joinType:JoinTypes) # sets the line join style.
getLineJoin() # returns the current line join style.
setLineCaps(beginCap:CapTypes,endCap:CapTypes=beginCap) # sets the line cap style for the start and end of strokes.
getLineBeginCap():CapTypes # returns the line cap style used at the beginning of strokes.
getLineEndCap():CapTypes # returns the line cap style used at the end of strokes.
setFont(font:rl.Font) #sets the font
getFont() :Font #returns the current font
getDefaultFont() :Font #returns the framework's default font.
pushState() #copies the current drawing state (color, line settings, font, etc.) and pushes it onto the state stack.
popState() #restores the previous drawing state by popping it from the state stack.
resetState() # resets the current drawing state (color, line settings, font, etc.) back to its default values.Does not affect previously saved states in the stack.
#Drawing
polygon(mode:DrawModes,points:varargs[float]) #draws a polygon
line(points:varargs[float]) #draws lines between points
arc(mode:DrawModes,arcType:ArcType, x:float,y:float,radius:float,angle1:float,angle2:float,segments:int=16) #draws an arc
circle(mode:DrawModes,x:float,y:float,radius:float) #draws a circle
ellipse(mode:DrawModes,x:float,y:float,radiusX:float,radiusY:float) #draws an ellipse.
rectangle(mode:DrawModes,x:float,y:float,width:float,height:float,rx:float=0,ry:float=rx,segments:int=12) #draws a rectangle
quad(mode:DrawModes,x1:float,y1:float,x2:float,y2:float,x3:float,y3:float,x4:float,y4:float) #draws a quadrilateral shape.
pixel(x:float,y:float) #Draws a pixel.
draw(texture:Texture, x:float=0.0,y:float=0.0) #draws a texture
draw(texture:Texture, quad:Quad, x:float=0.0,y:float=0.0) #draws a texture with the specified quad
draw(spriteBatch:SpriteBatch, x:float=0,y:float=0) #draws a spritebatch
draw(text:Text ,x:float=0.0,y:float=0.0, size:float=16, spacing:float=1.0 ) #draws a text
clear() #clears the screen with the active color.
clear(color:Color) #clears the screen with the specified color.
#Text Methods
getSizeWith(text:Text,fontSize:float,spacing:float=1.0) :tuple[x:float,y:float] # returns the text size using the specified font size and spacing.
#SpriteBatch Methods
#adds an instance to SpriteBatch
add(spriteBatch: var SpriteBatch,x,y:float,r:float=0,sx:float=1,sy:float=1,ox:float=0,oy:float=0,kx:float=0,ky:float=0):int
#adds an instance to SpriteBatch with Quad
add(spriteBatch: var SpriteBatch, quad:Quad, x,y:float,r:float=0,sx:float=1,sy:float=1,ox:float=0,oy:float=0,kx:float=0,ky:float=0):int
#Shader Methods
setShader(shader:Shader) # sets the specified shader for subsequent drawing operations.
setShader() # Unsets the currently active shader.
setValue(shader: Shader, uniformName: string, value: float) # sets the value of a float-typed uniform.
setValue(shader: Shader, uniformName: string, value: int) #sets the value of a int-typed uniform.
setValue(shader: Shader, uniformName: string, value: (float,float)) #sets the value of a vec2-typed uniform.
setValue(shader: Shader, uniformName: string, value: (float,float,float)) #sets the value of a vec3-typed uniform.
setValue(shader: Shader, uniformName: string, value: (float,float,float,float)) #sets the value of a vec4-typed uniform.
setValue(shader: Shader, uniformName: string, value: (int,int)) #sets the value of a Ivec2-typed uniform.
setValue(shader: Shader, uniformName: string, value: (int,int,int)) #sets the value of a Ivec3-typed uniform.
setValue(shader: Shader, uniformName: string, value: (int,int,int,int)) #sets the value of a Ivec4-typed uniform.
setValue(shader: Shader, uniformName: string, value:Texture) #sets the value of a texture-typed uniform.
### SOUND
newSound(fileName:string, soundType:SoundType) #creates a sound.
play(sound:Sound) #plays the specified sound.
stop(sound:Sound) #stops the specified sound.
pause(sound:Sound) #Pauses the specific sound.
resume(sound:Sound) #Resumes the specific sound.
isPlaying(sound:Sound) #returns whether the specific sound is playing
isValid(sound:Sound) #returns whether the specific sound is valid
setVolume(sound:Sound) #sets the volume of the specified sound
setPitch(sound:Sound) #sets the pitch of the specified sound
setPan(sound:Sound) #sets the pan of the specified sound
### INPUTS
#Keyboard
isKeyPressed(key:rl.KeyboardKey):bool #checks if a key has been pressed once
isKeyReleased(key:rl.KeyboardKey):bool #checks if a key has been released once
isKeyDown(key:rl.KeyboardKey):bool #checks if a key is being pressed
isKeyUp(key:rl.KeyboardKey):bool #checks if a key is not being pressed
getKeyPressed():KeyboardKey #get key pressed (keycode), call it multiple times for keys queued, returns KeyboardKey.Null when the queue is empty
getCharPressed():Rune #get char pressed (unicode), call it multiple times for chars queued, returns 0.Rune when the queue is empty
#Mouse
isMouseButtonPressed(button:rl.MouseButton):bool #checks if a mouse button has been pressed once
isMouseButtonReleased(button:rl.MouseButton):bool #checks if a mouse button has been released once
isMouseButtonDown(button:rl.MouseButton):bool #checks if a mouse button is being pressed
isMouseButtonUp(button:rl.MouseButton):bool #checks if a mouse button is not being pressed
getMouseX():int #returns mouse position x
getMouseY():int #returns mouse position y
#Gamepad
isGamepadAvailable(gamepad:int):bool #checks if a gamepad is available
getGamepadName(gamepad:int):string #returns gamepad internal name id
isGamepadButtonPressed(gamepad:int, button:rl.GamepadButton):bool #checks if a gamepad button has been pressed once
isGamepadButtonReleased(gamepad:int, button:rl.GamepadButton):bool #checks if a gamepad button has been released once
isGamepadButtonDown(gamepad:int, button:rl.GamepadButton):bool #checks if a gamepad button is being pressed
isGamepadButtonUp(gamepad:int, button:rl.GamepadButton):bool #checks if a gamepad button is not being pressed
getGamepadAxisCount(gamepad:int):int #returns gamepad axis count for a gamepad
getGamepadAxisMovement(gamepad:int, axis:rl.GamepadAxis):float #returns axis movement value for a gamepad axis
setGamepadMappings(mappings:string): int32 #set internal gamepad mappings (SDL_GameControllerDB)
setGamepadVibration(gamepad:int, leftMotor:float, rightMotor:float, duration:float) #set gamepad vibration for both motors (duration in seconds)
#Touch
getTouchX():int #get touch position x for touch point 0 (relative to screen size)
getTouchY():int #get touch position Y for touch point 0 (relative to screen size)
getTouchPointId(index:int):int #get touch point identifier for given index
getTouchPointCount():int #get number of touch points
getTouchHoldDuration(index:int):float #get touch hold time in seconds
getTouchDragX(index:int):float #get touch drag vector x
getTouchDragY(index:int):float #get touch drag vector y
getTouchDragAngle():float #get touch drag angle
getTouchPinchX() :float #get gesture pinch delta x
getTouchPinchY() :float #get gesture pinch delta y
getTouchPinchAngle():float #get gesture pinch angle
#WINDOW
setFullScreenMode(value:bool) #sets window state: fullscreen/windowed, resizes monitor to match window resolution
getFullScreenMode(): bool #checks if window is currently fullscreen
setBorderlessMode(value: bool ) #sets window state: borderless windowed, resizes window to match monitor resolution
getBorderlessMode() :bool #checks if window state is currently borderless
setMinSize(width:int=1,height:int=1) #sets window minimum dimensions (for resizeable windows)
setFocused() #sets window focused
isFocused(): bool #checks if window is currently focused
isResized(): bool #checks if window has been resized last frame
setTitle(title:string) #sets title for window
getWidth() : int #returns current window width
getHeight() : int #returns current window height
#JAVASCRIPT
eval(code: string): string # Executes JS code and returns result as string.
createCallback(cb: JSCallback; jsEvent: cstring) # Registers a callback that receives JS event data as JSON. (JSCallback type is: proc(arg: cstring){.cdecl.} )
createCallback(cb: JSCallbackVoid; jsEvent: cstring) # Registers a more performant callback thanks to no event data arguments (JSCallbackVoid type is: proc(){.cdecl.} )
Contributing
To contribute code to the project, please try to follow Nim’s standard library style guide. We generally strive to maintain consistency with it throughout this project.(You might not like this guide, but after all, it’s a prepared style guide that everyone can access and use collectively.)
Core principles
- Do not use macros or metaprogramming. This project does not need them and should not rely on them. It is important that the framework and its internal code remain understandable with zero learning curve, using only basic Nim knowledge.
- Avoid unnecessary abstractions. Prefer primitive types in API method arguments (e.g. float, int, bool). Only group values using tuple when grouping is actually meaningful. Use seq or array for collections. For example, defining a vertex format explicitly as
tuple[x, y, u, v: float]is preferred over introducing custom types likeVertex2D. The intent is for an intermediate Nim developer to understand the expected data layout immediately using Nim language knowledge alone, without learning framework-specific types or relying on detailed API documentation. - Preserve minimalism. We aim to keep the API small and stable, and to avoid expanding or changing it unless it is truly necessary. Suggestions are always welcome, but please don’t take offense if an idea is declined. If what you are building can live as a separate module, plugin, or library in an external repository, prefer that approach. This framework is intentionally designed as a small core API meant to be surrounded by an ecosystem of extensions. If you believe a missing capability in the core API genuinely prevents this, then open a PR and let’s discuss it.
- Treat the backend as a replaceable layer. This project currently uses Naylib (Nim’s Raylib wrapper) as its backend. However, if you inspect the source code, you’ll see that Raylib is used at an arm’s length, making it entirely feasible to add alternative backends alongside it. The graphics API is largely built on top of Raylib’s rlgl layer and its OpenGL-oriented calls. Because of this, ideas or experiments around different backends are very welcome. As long as the approach remains pragmatic, the backend layer is intentionally the most flexible part of this project.
Other ways to contribute
- Write and share plugins or modules that are either fully dependent on the framework or can be used independently while working well with it (ECS, physics, GUI, etc.)
- Open issues to report bugs or missing features.
- Create tutorials or documentation related to the framework.
- If you enjoy kirpi, share your thoughts on social media to help others discover it :) Don’t forget to share your work(game, prototype, creative coding projects ) with the
#madewithkirpitag.
What's mean kirpi?
"Hedgehog” in my native language Turkish is “kirpi.” Its pronunciation is /keer-pee/.
