Performance timeline-based object animation engine for SA-MP and open.mp.
Watch the ObjectAnimator demonstration on YouTube
ObjectAnimator is a C++ server plugin and Pawn library that extends the native object movement system of SA-MP 0.3.7 and open.mp.
Instead of relying on chained MoveObject calls, Pawn timers, and OnObjectMoved callbacks, ObjectAnimator provides a deterministic timeline engine capable of controlling object position, rotation, easing, delays, events, looping, playback direction, seeking, and playback speed.
Animations are evaluated using elapsed-time interpolation inside a centralized C++ update loop, allowing complex sequences to remain concise on the Pawn side while keeping runtime overhead low.
- Timeline-based animation sequences
- Smooth position and rotation interpolation
- 34 easing equations
- Simultaneous 6-DOF position and rotation transforms
- Absolute and relative movement
- Shortest-path Euler rotation
- Continuous multi-turn rotations
- Timeline waits and custom events
- Pause, resume, stop, reset, and seek
- Dynamic playback speed
- Infinite and bounded looping
- Ping-pong playback
- Reusable immutable animation templates
- Global and player object support
- Generational safe handles
- Centralized C++ update loop
- No heap allocations during normal animation ticks
- Pawn callbacks for animation lifecycle and timeline events
The native SA-MP object movement API is intentionally simple.
Functions such as MoveObject and MovePlayerObject are useful for basic movement, but complex animations normally require additional state management in Pawn.
Common limitations include:
- constant-speed movement;
- limited easing control;
- difficult synchronization between movement and rotation;
- callback chaining for multi-step sequences;
- Pawn timers for pauses and delayed actions;
- manual state tracking;
- no built-in seeking;
- no playback speed control;
- no native ping-pong playback;
- difficult animation reuse across multiple objects.
ObjectAnimator moves this logic into a dedicated animation engine.
A complete sequence can be declared once and then controlled as a single timeline.
| Target | Support |
|---|---|
| SA-MP 0.3.7 R1–R5 | Supported |
| open.mp | Supported through legacy plugin compatibility |
| Global Objects | Supported |
| Player Objects | Supported |
| Windows x86 | Supported |
| Linux x86 | Supported |
| Windows x64 | Planned |
| Linux x64 | Planned |
| Official Pawn Compiler | Supported |
| Zeex Pawn Compiler | Supported |
ObjectAnimator operates entirely on the server. Players do not need to install any client-side modification.
Download the appropriate archive from the Releases page.
Extract:
ObjectAnimator.dll
into:
plugins/
Then place:
object_animator.inc
inside:
pawno/include/
Add the plugin to server.cfg:
plugins ObjectAnimator.dll
Extract:
ObjectAnimator.so
into:
plugins/
Place:
object_animator.inc
inside:
pawno/include/
Then add:
plugins ObjectAnimator.so
to server.cfg.
For additional information, see docs/installation.md.
Include ObjectAnimator after your SA-MP or open.mp includes:
#include <a_samp>
#include <object_animator>Create an object and attach an animator:
new Animator:gGateAnim;
public OnGameModeInit()
{
new gate = CreateObject(
980,
1544.5, -1630.8, 13.0,
0.0, 0.0, 90.0
);
gGateAnim = OA_Create(gate);
OA_MoveBy(
gGateAnim,
0.0, 0.0, 5.0,
1500,
EASE_OUT_CUBIC
);
OA_Wait(gGateAnim, 2000);
OA_MoveBy(
gGateAnim,
0.0, 0.0, -5.0,
1500,
EASE_IN_CUBIC
);
OA_Play(gGateAnim);
return 1;
}
public OnGameModeExit()
{
if (OA_IsValid(gGateAnim))
{
OA_Destroy(gGateAnim);
}
return 1;
}The resulting timeline is:
0 ms 1500 ms 3500 ms 5000 ms
│ │ │ │
├──── Move Up ──────────┤────── Wait ──────┤──── Move Down ──────┤
│ EASE_OUT_CUBIC │ 2000 ms │ EASE_IN_CUBIC │
No Pawn timers or movement callback chains are required.
An animator owns an ordered sequence of timeline nodes.
Each node begins immediately after the previous node finishes.
For example:
OA_MoveBy(anim, 0.0, 0.0, 5.0, 1000, EASE_OUT_CUBIC);
OA_Wait(anim, 2000);
OA_RotateBy(anim, 0.0, 0.0, 90.0, 750, EASE_IN_OUT_QUAD);
OA_Event(anim, "finished_rotation");Conceptually:
Timeline
│
├── MoveBy
│ └── 1000 ms
│
├── Wait
│ └── 2000 ms
│
├── RotateBy
│ └── 750 ms
│
└── Event
└── 0 ms
Total duration:
1000 + 2000 + 750 = 3750 ms
Instant nodes such as SetPosition, SetRotation, and Event consume no timeline duration.
See docs/timeline.md for details.
Move an object toward a fixed world-space coordinate:
OA_MoveTo(
anim,
1500.0,
-1700.0,
20.0,
2000,
EASE_IN_OUT_CUBIC
);Move relative to the position produced by the previous timeline node:
OA_MoveBy(
anim,
0.0,
0.0,
5.0,
1000,
EASE_OUT_CUBIC
);Relative nodes are resolved deterministically from the timeline state rather than depending on when the node happens to execute.
This allows seeking and replaying to produce consistent results.
OA_RotateTo(
anim,
0.0,
0.0,
180.0,
1000,
EASE_IN_OUT_QUAD
);RotateTo uses shortest-path angular interpolation.
For example:
350° → 10°
is interpreted as:
+20°
instead of:
-340°
This prevents unnecessary full rotations when interpolating Euler angles.
OA_RotateBy(
anim,
0.0,
0.0,
720.0,
3000,
EASE_LINEAR
);Unlike shortest-path absolute rotation, relative rotations preserve the requested angular delta.
Therefore:
720°
produces two complete rotations.
Similarly:
1080°
produces three complete rotations.
Position and rotation can be interpolated simultaneously.
OA_TransformTo(
anim,
1500.0,
-1700.0,
20.0,
0.0,
0.0,
180.0,
2000,
EASE_IN_OUT_CUBIC
);OA_TransformBy(
anim,
5.0,
0.0,
2.0,
0.0,
0.0,
90.0,
1500,
EASE_OUT_QUAD
);Both position and rotation use the same normalized timeline progress.
ObjectAnimator provides 34 easing equations based on the commonly used Robert Penner easing families.
EASE_LINEAR
EASE_IN_QUAD
EASE_OUT_QUAD
EASE_IN_OUT_QUAD
EASE_IN_CUBIC
EASE_OUT_CUBIC
EASE_IN_OUT_CUBIC
EASE_IN_QUART
EASE_OUT_QUART
EASE_IN_OUT_QUART
EASE_IN_QUINT
EASE_OUT_QUINT
EASE_IN_OUT_QUINT
EASE_IN_SINE
EASE_OUT_SINE
EASE_IN_OUT_SINE
EASE_IN_EXPO
EASE_OUT_EXPO
EASE_IN_OUT_EXPO
EASE_IN_CIRC
EASE_OUT_CIRC
EASE_IN_OUT_CIRC
EASE_IN_BACK
EASE_OUT_BACK
EASE_IN_OUT_BACK
EASE_IN_ELASTIC
EASE_OUT_ELASTIC
EASE_IN_OUT_ELASTIC
EASE_IN_BOUNCE
EASE_OUT_BOUNCE
EASE_IN_OUT_BOUNCE
Refer to docs/easing.md for formulas, graphs, and recommendations.
ObjectAnimator provides independent runtime control over every animator.
OA_Play(anim);OA_Pause(anim);OA_Resume(anim);OA_Stop(anim);OA_Reset(anim);if (OA_IsPlaying(anim))
{
// Animator is running.
}
if (OA_IsPaused(anim))
{
// Animator is paused.
}Animation speed can be modified while the animator is running.
OA_SetSpeed(anim, 2.0);Playback now runs at twice the normal rate.
OA_SetSpeed(anim, 0.5);Playback now runs at half speed.
Retrieve the current rate with:
new Float:speed = OA_GetSpeed(anim);An animator can jump directly to an exact timeline position.
OA_Seek(anim, 1500);ObjectAnimator evaluates the timeline at 1500 ms and immediately applies the calculated transform.
Timeline information can also be queried:
new current = OA_GetTime(anim);
new duration = OA_GetDuration(anim);This allows systems such as:
- synchronized machinery;
- animation debugging;
- timeline scrubbing;
- progress indicators;
- state restoration;
- scripted cinematics.
Enable looping:
OA_SetLoop(anim, true);Infinite looping:
OA_SetLoopCount(anim, 0);Bounded looping:
OA_SetLoopCount(anim, 3);The animation runs three iterations before finishing.
Ping-pong playback reverses direction each time the timeline reaches an endpoint.
OA_SetPingPong(anim, true);
OA_Play(anim);Playback becomes:
START ──────────────────► END
◄──────────────────
START ──────────────────► END
This is useful for:
- moving platforms;
- pistons;
- industrial machinery;
- barriers;
- environmental motion;
- repetitive mechanical systems.
Animations that share the same timeline can be stored as reusable templates.
new Animation:gDoorRoutine;
gDoorRoutine = OA_AnimationCreate();
OA_AnimationMoveBy(
gDoorRoutine,
0.0,
0.0,
3.0,
1000,
EASE_OUT_CUBIC
);
OA_AnimationWait(
gDoorRoutine,
2000
);
OA_AnimationMoveBy(
gDoorRoutine,
0.0,
0.0,
-3.0,
1000,
EASE_IN_CUBIC
);The same animation can then be applied to multiple animators:
OA_ApplyAnimation(anim1, gDoorRoutine);
OA_ApplyAnimation(anim2, gDoorRoutine);
OA_ApplyAnimation(anim3, gDoorRoutine);Each animator receives an independent playback state while referencing the reusable timeline definition.
This is particularly useful for large systems containing many objects that share identical movement behavior.
See docs/reusable-animations.md.
ObjectAnimator exposes lifecycle and timeline callbacks to Pawn.
forward OnOAStart(Animator:animator);
forward OnOAPause(Animator:animator);
forward OnOAResume(Animator:animator);
forward OnOAStop(Animator:animator);
forward OnOAFinish(Animator:animator);
forward OnOALoop(
Animator:animator,
loop_index
);
forward OnOANodeStart(
Animator:animator,
node_index
);
forward OnOANodeFinish(
Animator:animator,
node_index
);
forward OnOAEvent(
Animator:animator,
const event[]
);Example:
public OnOAEvent(Animator:animator, const event[])
{
if (!strcmp(event, "elevator_arrived"))
{
SendClientMessageToAll(
-1,
"The elevator has arrived."
);
}
return 1;
}More information is available in docs/callbacks.md.
OA_Create
OA_CreatePlayerObject
OA_Destroy
OA_IsValidOA_MoveTo
OA_MoveBy
OA_RotateTo
OA_RotateBy
OA_TransformTo
OA_TransformBy
OA_SetPosition
OA_SetRotation
OA_Wait
OA_EventOA_Play
OA_Pause
OA_Resume
OA_Stop
OA_Reset
OA_IsPlaying
OA_IsPausedOA_SetLoop
OA_SetLoopCount
OA_SetPingPong
OA_SetSpeed
OA_GetSpeed
OA_Seek
OA_GetTime
OA_GetDurationOA_AnimationCreate
OA_AnimationDestroy
OA_AnimationClear
OA_AnimationMoveTo
OA_AnimationMoveBy
OA_AnimationRotateTo
OA_AnimationRotateBy
OA_AnimationTransformTo
OA_AnimationTransformBy
OA_AnimationWait
OA_AnimationEvent
OA_ApplyAnimationOA_SetDebug
OA_GetLastErrorThe complete native reference, including parameters, return values, and examples, is available in docs/api-reference.md.
ObjectAnimator uses generational handles rather than directly exposing internal pointers or array indexes.
A handle identifies:
┌──────────────────────────────┐
│ Animator Handle │
├──────────────┬───────────────┤
│ Generation │ Slot │
└──────────────┴───────────────┘
When an animator is destroyed, its generation changes before its slot can be reused.
A stale handle therefore cannot accidentally reference a newly created animator occupying the same internal slot.
Example:
new Animator:anim = OA_Create(objectid);
OA_Destroy(anim);
if (!OA_IsValid(anim))
{
// The stale handle is safely rejected.
}Invalid input is rejected without intentionally dereferencing invalid animator handles.
The most recent error can be retrieved using:
new error = OA_GetLastError();Available error codes:
| Code | Name | Description |
|---|---|---|
0 |
OA_ERROR_NONE |
No error |
1 |
OA_ERROR_INVALID_HANDLE |
Animator handle is invalid or stale |
2 |
OA_ERROR_OBJECT_NOT_FOUND |
Target object does not exist |
3 |
OA_ERROR_EMPTY_TIMELINE |
Playback requested without timeline nodes |
4 |
OA_ERROR_ALREADY_PLAYING |
Animator is already running |
5 |
OA_ERROR_NOT_PLAYING |
Operation requires active playback |
6 |
OA_ERROR_INVALID_PARAMETER |
One or more parameters are invalid |
7 |
OA_ERROR_MAX_CAPACITY |
Animator capacity has been reached |
ObjectAnimator uses a centralized update loop and only processes active animators.
Runtime complexity is approximately:
O(active animators)
rather than:
O(all allocated animators)
Normal animation ticks perform no heap allocation.
The following benchmark results were collected using tests/benchmarks.cpp over 1,000 consecutive engine ticks on the documented Linux x86 test environment.
| Active Animators | Mean / Tick | P95 | Approx. 20 ms Tick Budget |
|---|---|---|---|
| 100 | 8.94 µs | 6.48 µs | 0.045% |
| 500 | 27.88 µs | 31.42 µs | 0.139% |
| 1,000 | 46.58 µs | 61.56 µs | 0.233% |
Benchmark results depend on processor, compiler, optimization flags, operating system, timeline complexity, and plugin configuration. They should not be interpreted as universal performance guarantees.
Memory usage scales primarily with:
allocated animators + timeline nodes + animation templates
Detailed benchmark methodology is available in docs/performance.md.
At a high level:
Pawn Script
│
│ OA_* natives
▼
┌─────────────────────────┐
│ ObjectAnimator │
│ Native Layer │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Animator Manager │
│ │
│ Generational Handles │
│ Active Animator List │
│ Playback State │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Timeline Engine │
│ │
│ Nodes │
│ Easing │
│ Seeking │
│ Looping │
│ Ping-Pong │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ SA-MP / open.mp Objects │
└─────────────────────────┘
The Pawn API handles configuration while interpolation and playback state are maintained by the C++ engine.
Ready-to-compile examples are available under examples/.
Demonstrates:
- relative movement;
- easing;
- timeline waits;
- completion callbacks.
Demonstrates:
- multi-stage movement;
- acceleration and deceleration;
- timeline events;
- arrival handling.
Demonstrates:
- looping;
- continuous rotation;
- synchronized movement and rotation;
- relative transforms.
Demonstrates:
- reusable animation templates;
- multiple independent animators;
- complex timeline sequencing;
- event-driven machinery.
- CMake 3.16 or newer
- C++17-compatible compiler
- SA-MP plugin SDK / compatible plugin interface
- 32-bit build toolchain for SA-MP
Install the required multilib toolchain for your distribution.
Configure:
cmake -B build \
-DCMAKE_BUILD_TYPE=ReleaseBuild:
cmake --build buildThe resulting plugin is generated as:
build/ObjectAnimator.so
ObjectAnimator can be cross-compiled using MinGW-w64.
cmake -B build-windows \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-x86.cmake \
-DCMAKE_BUILD_TYPE=ReleaseBuild:
cmake --build build-windowsOutput:
build-windows/ObjectAnimator.dll
Release archives can be generated through CMake targets.
cmake --build build --target package-linuxOutput:
release/ObjectAnimator-linux-x86.tar.gz
cmake --build build --target package-windowsOutput:
release/ObjectAnimator-windows-x86.zip
A release package should contain at minimum:
ObjectAnimator/
├── plugins/
│ └── ObjectAnimator.dll / ObjectAnimator.so
│
├── pawno/
│ └── include/
│ └── object_animator.inc
│
├── examples/
│ ├── basic_gate.pwn
│ ├── elevator.pwn
│ ├── rotating_platform.pwn
│ └── complex_machine.pwn
│
├── README.md
└── LICENSE
Run the unit test suite:
./build/object_animator_testsRun the benchmark suite:
./build/object_animator_benchmarksContributions should pass the relevant tests before being submitted.
Additional documentation is available under docs/.
| Document | Description |
|---|---|
installation.md |
Installation and server configuration |
api-reference.md |
Complete Pawn native reference |
timeline.md |
Timeline behavior and node evaluation |
easing.md |
Easing equations and usage |
callbacks.md |
Callback reference |
reusable-animations.md |
Animation template system |
performance.md |
Benchmarks and methodology |
troubleshooting.md |
Common installation and runtime issues |
- Rotation uses Euler angles to remain compatible with the SA-MP object API.
- Object visibility remains subject to SA-MP/open.mp streaming behavior.
- Network update smoothness ultimately depends on the underlying server/client object synchronization.
- ObjectAnimator does not modify the game client.
- Native 64-bit plugin builds are not currently part of the standard release targets.
Possible future extensions include:
- vehicle animation adapter;
- actor animation adapter;
- interpolated camera tracks;
- additional timeline composition utilities;
- native open.mp 64-bit builds;
- expanded profiling and diagnostics.
Potential roadmap items are not guaranteed until implemented and released.
Contributions, bug reports, and pull requests are welcome.
When submitting code:
- Keep changes focused and documented.
- Preserve backward compatibility where practical.
- Add or update tests for behavioral changes.
- Run the unit test suite.
- Run relevant benchmarks for performance-sensitive changes.
- Avoid introducing allocations into the animation tick path without justification.
ObjectAnimator is released under the MIT License.
Copyright © ObjectAnimator contributors.