Skip to content

forge18/love-effekseer

Repository files navigation

love-effekseer

A LÖVE 2D binding for the Effekseer particle/VFX engine via LuaJIT FFI.

⚠️ Alpha. The binding is feature-complete for 2D runtime VFX — the full Effekseer surface relevant to 2D is bound (transforms, color, dynamic parameters, layers, sound, callbacks, …) — with a test suite (47 tests). It's validated on macOS; Linux/Windows build via CI but haven't been battle-tested in real projects yet. Expect rough edges; please report anything you hit.

2D only. This drives Effekseer as a 2D screen-space overlay — effects are placed in pixel coordinates and composited over the LÖVE frame. No 3D scenes, cameras, or perspective. (Effekseer is internally a 3D engine; we constrain it to a 2D orthographic setup on purpose.)

Overview

Effekseer runs as a native shim (a small C++ shared lib over Effekseer + EffekseerRendererGL, OpenGL only); the Lua side is a thin, idiomatic wrapper. Effects are authored in the Effekseer editor (.efk) and played at runtime — placed by pixel coordinate, recolored, scaled, and parameterized live.

The reason to use it: LÖVE's built-in ParticleSystem is a basic single-emitter sprayer. Effekseer gives you ribbons, rings, tracks, distortion, and multi-emitter effect trees authored in a dedicated editor — the primitives real ability/impact VFX need — driven at runtime. The central engineering problem a LÖVE binding must solve is GL-state coexistence (Effekseer issues raw GL into LÖVE's context); this binding handles it (Effekseer's state-restoration flag + a GL state guard + premultiplied-alpha canvas compositing), so LÖVE renders cleanly before and after each effect draw. The shim pins Effekseer 1.80.4.

Run love example from this repo for a demo (click to spawn tinted effects).

Functionality

The Effekseer surface relevant to 2D runtime VFX is bound in full. By area:

  • Lifecycle — create manager (single- or multi-threaded worker pool), load effects from memory, hot-reload, free; GC-finalized resources.
  • Playback — play / stop / stop-root / exists; pause (per-handle and all); spawn-disable; shown; random seed; auto-drawing.
  • Transforms — location, add-location, target-location, rotation (Euler + axis-angle), scale, full matrix and base matrix — with location/matrix getters.
  • Appearance — color, speed (+ get), per-handle and per-group time-scale.
  • Dynamic parameters & triggerssetDynamicInput / getDynamicInput, sendTrigger: the author-once / drive-at-runtime mechanism.
  • Layers & groups — layer, group mask (+ getters), per-group time-scale.
  • Update & render — frame-based update (per-manager, per-handle, seek-to-frame), camera + projection matrices, guarded draw, the 2D ortho/camera convention.
  • Queries & profiling — instance counts (total / per-handle / remaining), location, matrix, current LOD, is-culled, update / draw time.
  • Sound — effect-authored sounds bridged to love.audio, in sync with the effect timeline (respects time-scale; volume / pan / pitch from the effect).
  • Callbacks — removing callback (fires on the main thread; safe).
  • Resources — textures, models, materials, and curves, via a custom file interface.

The complete method-by-method reference is in docs/api.md; a quickstart + asset prep + troubleshooting is in docs/getting-started.md.

Deliberately not bound: GPU-particle compute path, 3D collision callback, Effekseer sound's built-in players (we bridge to love.audio instead), custom effect-file loader (we load from memory), node-tree internals, and non-GL renderer backends. See docs/api.md for the full list and rationale — these are scope decisions, not technical limits.

Setup

It's a native binding, so a consuming game needs two things:

  1. The Lua module — copy effekseer/ into your project so require("effekseer.system") resolves. It's a folder module, so your require path must include ?/init.lua (LÖVE's default already does).

  2. The native library for each platform you ship, placed at lib/native/<platform>/:

    • macos-arm64/libeffekseer_shim.dylib, macos-x86_64/libeffekseer_shim.dylib
    • linux-x64/libeffekseer_shim.so
    • windows-x64/effekseer_shim.dll

    Prebuilt binaries are attached to each GitHub Release (and committed under lib/native/). The loader (effekseer/ffi.lua) resolves <source>/lib/native/<platform>/<libname> — under LÖVE that's love.filesystem.getSource(), otherwise the cwd (or $EFFEKSEER_ROOT).

Usage

The effekseer.system adapter owns the LÖVE 2D render integration (canvas isolation, the pixel-space projection, premultiplied compositing, and the love.audio sound bridge):

local System = require("effekseer.system")

function love.load()
  local w, h = love.graphics.getDimensions()
  sys = System.new{ width = w, height = h, workerThreads = 2 }
  local bytes = love.filesystem.read("assets/fireball.efk")           -- exported .efk, not .efkefc
  fireball = sys:loadEffect(bytes, love.filesystem.getSource() .. "/assets/", 20)
end

function love.update(dt) sys:update(dt) end

function love.mousepressed(x, y)
  local h = sys:play(fireball, x, y)     -- pixel coords; (x,y) is where it appears
  h:setColor(80, 200, 255, 255)          -- runtime tint (optional)
end

function love.draw()
  sys:draw()                             -- composites effects over the current target
end

Assets must be exported .efk (not the editor's .efkefc), with textures/sounds at the relative paths the effect references under your materialPath. See docs/getting-started.md for asset layout and troubleshooting, and docs/rendering.md for the render model, coordinate/blend conventions, and performance tuning (multithreading + culling).

Building

The shim is a C++/CMake project (native/effekseer_shim) over the Effekseer submodule, built OpenGL-only (Metal/Vulkan/DirectX renderers and editor/tools are disabled). scripts/build-native.sh builds the host platform into lib/native/<platform>/. C++ with GL deps doesn't cross-compile cleanly, so each platform builds natively on its own CI runner rather than from one host.

Prerequisite — Git LFS. The Effekseer submodule stores textures/test assets in Git LFS. After git clone --recursive, run git lfs install && git lfs pull in the submodule — otherwise those assets are pointer stubs and textures silently fail to load.

cd native/effekseer_shim/third_party/Effekseer && git lfs install && git lfs pull && cd -
scripts/build-native.sh           # builds the host shim into lib/native/<platform>/

Tests (tests/) are split by whether they need a GL context. The no-GL tier (matrix math, the FFI boundary) runs under LuaJIT; the GL tier (lifecycle, load, transforms, queries, sound, leak checks) runs under LÖVE:

EFFEKSEER_ROOT="$PWD" luajit tests/run.lua     # no-GL tier (dependency-free runner)
EFFEKSEER_ROOT="$PWD" love tests/gl            # GL tier (needs a context; exits non-zero on failure)

CI (.github/workflows/build.yml) builds each platform natively, runs the no-GL tier everywhere and the GL tier on Linux under xvfb, and gates the tagged Release on it.

License

love-effekseer (the wrapper + shim) is MIT — see LICENSE. The compiled binary links Effekseer, which is MIT-licensed; retain its notice when redistributing the binaries.

About

A LÖVE binding for the Effekseer particle engine — 2D screen-space VFX, played and parameterized at runtime via LuaJIT FFI.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors