diff options
| author | KonAki <konaki@anokonaki.org> | 2026-10-12 03:03:33 +0800 |
|---|---|---|
| committer | KonAki <konaki@anokonaki.org> | 2026-10-12 03:03:33 +0800 |
| commit | b77cac581ac41d069f20a24e07211cc200c653eb (patch) | |
| tree | d237ac74320d242b8488110560cdd9c47d7d34fe | |
| parent | 5e2858a4c8b298149bc01ea5dd215cb1f702c8e3 (diff) | |
add 2d module: platformer body, camera, frame animation
| -rw-r--r-- | README.md | 1 | ||||
| -rwxr-xr-x | build.sh | 5 | ||||
| -rw-r--r-- | kon/kon2d.c | 196 | ||||
| -rw-r--r-- | kon/kon2d.h | 112 |
4 files changed, 312 insertions, 2 deletions
@@ -31,6 +31,7 @@ The engine modules are a `.h` and a `.c` pair, they depend on each other a bit, | font | konfont | | toml | kontoml | | tile | tilemaps and sprite sheets | +| 2d | platformer body with coyote time and jump buffering, a scrolling camera, frame animation (adds tile) | | 3d | first person kit: mouse look body, walls from a tilemap, editable hill terrain (adds tile) | | sector | maps made of rooms (vertices, floors, ceilings, walls) with entities like doors and buttons, in a text format | | world | one place to move bodies through sector maps, terrain, tilemaps and your own boxes, cylinders, spheres and planes (needs 3d, sector) | @@ -12,7 +12,7 @@ developer=0 usage() { echo "usage: build.sh [-d] [-D] [-p platform] [-m module,...] [-o objdir] [target ...]" - echo "modules: font toml tile pack ui debug assets image audio mic model 3d sector world" + echo "modules: font toml tile pack ui debug assets image audio mic model 2d 3d sector world" echo "platforms: native linux bsd windows" echo "targets: all run clean tools pack examples" exit 1 @@ -77,7 +77,7 @@ addobj() for m in $modules; do DEFS="$DEFS -DKON_MODULE_$(echo "$m" | tr 'a-z' 'A-Z')" case $m in - font|toml|tile|pack|ui|debug|3d|sector|world) addobj "kon$m" ;; + font|toml|tile|pack|ui|debug|2d|3d|sector|world) addobj "kon$m" ;; assets) addobj konassets ;; model) addobj konmodel; addobj konassets ;; image) addobj stb_image ;; @@ -106,6 +106,7 @@ need() case " $modules " in *" debug "*) need font; need assets ;; esac case " $modules " in *" ui "*) need font ;; esac case " $modules " in *" world "*) need 3d; need sector; need tile ;; esac +case " $modules " in *" 2d "*) need tile ;; esac case " $modules " in *" 3d "*) need tile ;; esac case " $modules " in *" model "*) need assets ;; esac case " $DEFS " in *" -DKON_MODULE_ASSETS "*) need pack ;; esac diff --git a/kon/kon2d.c b/kon/kon2d.c new file mode 100644 index 0000000..2cc9fdd --- /dev/null +++ b/kon/kon2d.c @@ -0,0 +1,196 @@ +/* SPDX-License-Identifier: MIT */ +/* + * kon2d.c + * + * Copyright (c) 2026 KonAki + * + * 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. + * + * 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. + */ + +#include "kon2d.h" + +#include <math.h> + +#define STEP (1.0 / 120.0) +#define MAX_FRAME 0.25 + +/*** platformer ***/ + +void kon_platformerInit(kon_platformer_t *p, kon_box2_t box) +{ + kon_platformer_t zero = {0}; + + *p = zero; + p->box = box; + p->runSpeed = 120.0f; + p->groundAccel = 18.0f; + p->airAccel = 8.0f; + p->gravity = 700.0f; + p->jumpSpeed = 260.0f; + p->jumpCut = 60.0f; + p->maxFall = 400.0f; + p->coyoteTime = 0.08f; + p->bufferTime = 0.1f; +} + +int kon_platformerStep(kon_platformer_t *p, const kon_tilemap_t *map, const uint8_t *solid, int solidCount, float dt) +{ + float run = (float)p->moveX * p->runSpeed; + float ease = (p->onGround ? p->groundAccel : p->airAccel) * dt; + int flags; + + if (ease > 1.0f) ease = 1.0f; + p->velocity.x += (run - p->velocity.x) * ease; + p->velocity.y += p->gravity * dt; + if (p->velocity.y > p->maxFall) p->velocity.y = p->maxFall; + + if (p->jumpHeld && !p->jumpWas) p->buffer = p->bufferTime; + p->jumpWas = p->jumpHeld; + if (p->buffer > 0.0f) p->buffer -= dt; + if (p->coyote > 0.0f) p->coyote -= dt; + + if (p->buffer > 0.0f && p->coyote > 0.0f) { + p->velocity.y = -p->jumpSpeed; + p->buffer = 0.0f; + p->coyote = 0.0f; + } + if (!p->jumpHeld && p->velocity.y < -p->jumpCut) p->velocity.y = -p->jumpCut; + + flags = kon_tilemapMove(map, &p->box, p->velocity.x * dt, p->velocity.y * dt, solid, solidCount); + + if (flags & (KON_HIT_LEFT | KON_HIT_RIGHT)) p->velocity.x = 0.0f; + if (flags & (KON_HIT_UP | KON_HIT_DOWN)) p->velocity.y = 0.0f; + p->onGround = (flags & KON_HIT_DOWN) != 0; + if (p->onGround) p->coyote = p->coyoteTime; + return flags; +} + +void kon_platformerUpdate(kon_platformer_t *p, const kon_tilemap_t *map, const uint8_t *solid, int solidCount, double dt) +{ + if (!(dt > 0.0)) return; + if (dt > MAX_FRAME) dt = MAX_FRAME; + + p->leftover += (float)dt; + while (p->leftover >= (float)STEP) { + kon_platformerStep(p, map, solid, solidCount, (float)STEP); + p->leftover -= (float)STEP; + } +} + +/*** camera ***/ + +static float clampAxis(float value, float view, float bounds) +{ + if (bounds <= view) return (bounds - view) * 0.5f; + if (value < 0.0f) return 0.0f; + if (value > bounds - view) return bounds - view; + return value; +} + +void kon_camera2Init(kon_camera2_t *cam, int viewWidth, int viewHeight, float boundsWidth, float boundsHeight) +{ + cam->x = cam->y = 0.0f; + cam->viewWidth = viewWidth; + cam->viewHeight = viewHeight; + cam->boundsWidth = boundsWidth; + cam->boundsHeight = boundsHeight; + cam->stiffness = 8.0f; + cam->x = clampAxis(0.0f, (float)viewWidth, boundsWidth); + cam->y = clampAxis(0.0f, (float)viewHeight, boundsHeight); +} + +void kon_camera2Snap(kon_camera2_t *cam, float targetX, float targetY) +{ + cam->x = clampAxis(targetX - (float)cam->viewWidth * 0.5f, (float)cam->viewWidth, cam->boundsWidth); + cam->y = clampAxis(targetY - (float)cam->viewHeight * 0.5f, (float)cam->viewHeight, cam->boundsHeight); +} + +void kon_camera2Follow(kon_camera2_t *cam, float targetX, float targetY, float dt) +{ + float wantX = clampAxis(targetX - (float)cam->viewWidth * 0.5f, (float)cam->viewWidth, cam->boundsWidth); + float wantY = clampAxis(targetY - (float)cam->viewHeight * 0.5f, (float)cam->viewHeight, cam->boundsHeight); + float ease = 1.0f; + + if (cam->stiffness > 0.0f && dt > 0.0f) ease = 1.0f - expf(-cam->stiffness * dt); + cam->x += (wantX - cam->x) * ease; + cam->y += (wantY - cam->y) * ease; +} + +int kon_camera2X(const kon_camera2_t *cam) +{ + return (int)floorf(cam->x + 0.5f); +} + +int kon_camera2Y(const kon_camera2_t *cam) +{ + return (int)floorf(cam->y + 0.5f); +} + +/*** animation ***/ + +void kon_animPlay(kon_anim_t *anim, int first, int count, float fps, int loop) +{ + if (anim->first == first && anim->count == count && anim->fps == fps && anim->loop == loop) return; + anim->first = first; + anim->count = count; + anim->fps = fps; + anim->loop = loop; + anim->time = 0.0f; +} + +void kon_animStep(kon_anim_t *anim, float dt) +{ + if (anim->count <= 0 || !(dt > 0.0f)) return; + anim->time += dt; + if (anim->loop) { + float length = (float)anim->count / anim->fps; + + if (anim->fps > 0.0f && anim->time >= length) anim->time = fmodf(anim->time, length); + } +} + +int kon_animFrame(const kon_anim_t *anim) +{ + int n; + + if (anim->count <= 0) return anim->first; + if (!(anim->fps > 0.0f)) return anim->first; + n = (int)(anim->time * anim->fps); + if (n >= anim->count) n = anim->loop ? n % anim->count : anim->count - 1; + return anim->first + n; +} + +int kon_animDone(const kon_anim_t *anim) +{ + if (anim->loop || anim->count <= 0) return 0; + return anim->fps > 0.0f && (int)(anim->time * anim->fps) >= anim->count; +} + +void kon_drawFrame(kon_framebuffer_t *fb, const kon_image *sheet, int frameWidth, int frameHeight, int index, int x, int y, int flip) +{ + kon_rect_t src = kon_frameRect(sheet, frameWidth, frameHeight, index); + + if (src.w <= 0) return; + if (!flip) { + kon_drawImageRegion(fb, x, y, sheet, src); + return; + } + kon_drawImageEx(fb, sheet, src, (float)x + (float)frameWidth * 0.5f, (float)y + (float)frameHeight * 0.5f, + (float)frameWidth * 0.5f, (float)frameHeight * 0.5f, -1.0f, 1.0f, 0.0f, KON_FILTER_NEAREST); +} diff --git a/kon/kon2d.h b/kon/kon2d.h new file mode 100644 index 0000000..7a58dc6 --- /dev/null +++ b/kon/kon2d.h @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: MIT */ +/* + * kon2d.h + * + * Copyright (c) 2026 KonAki + * + * 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. + * + * 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. + */ + +#ifndef KON2D_H +#define KON2D_H + +#include "konsofren.h" +#include "koncollide.h" +#include "kontile.h" + +/*** platformer ***/ + +/* a box that runs and jumps through a tilemap. the game sets moveX and jumpHeld every frame, calls kon_platformerUpdate and reads + box, velocity and onGround back. everything is in map pixels and seconds. kon_platformerInit fills in the tuning values, + change them afterwards as you like */ +typedef struct kon_platformer { + kon_box2_t box; + kon_vec2_t velocity; + int onGround; + + int moveX; /* -1 left, 0 stand, 1 right */ + int jumpHeld; + + float runSpeed; + float groundAccel, airAccel; /* how fast velocity.x closes in on the run speed, per second */ + float gravity; + float jumpSpeed; + float jumpCut; /* letting go of jump early slows the rise to this speed */ + float maxFall; + float coyoteTime; /* seconds after leaving a ledge a jump still works */ + float bufferTime; /* seconds before landing a jump press is remembered */ + + /* private */ + float coyote, buffer, leftover; + int jumpWas; +} kon_platformer_t; + +void kon_platformerInit(kon_platformer_t *p, kon_box2_t box); + +/* one step of dt seconds, dt should be small and steady. returns the KON_HIT_ flags of the move */ +int kon_platformerStep(kon_platformer_t *p, const kon_tilemap_t *map, const uint8_t *solid, int solidCount, float dt); + +/* takes the frame time and runs steps of 1/120 of a second, so the jump feels the same at any frame rate */ +void kon_platformerUpdate(kon_platformer_t *p, const kon_tilemap_t *map, const uint8_t *solid, int solidCount, double dt); + +/*** camera ***/ + +/* x, y is the map pixel at the top left of the view. bounds is the size of the map in pixels, the view never leaves it + (and a map smaller than the view is centred) */ +typedef struct kon_camera2 { + float x, y; + int viewWidth, viewHeight; + float boundsWidth, boundsHeight; + float stiffness; /* how quickly it catches up, around 6 to 12 feels right, 0 or less follows exactly */ +} kon_camera2_t; + +void kon_camera2Init(kon_camera2_t *cam, int viewWidth, int viewHeight, float boundsWidth, float boundsHeight); + +/* puts the target in the middle of the view at once */ +void kon_camera2Snap(kon_camera2_t *cam, float targetX, float targetY); + +/* eases towards having the target in the middle */ +void kon_camera2Follow(kon_camera2_t *cam, float targetX, float targetY, float dt); + +/* whole pixels, what to pass to kon_drawTilemap so nothing shimmers */ +int kon_camera2X(const kon_camera2_t *cam); +int kon_camera2Y(const kon_camera2_t *cam); + +/*** animation ***/ + +/* plays count frames starting at frame first of a sheet at fps frames a second */ +typedef struct kon_anim { + int first, count; + float fps; + int loop; + float time; +} kon_anim_t; + +/* starts over, unless it is already playing the same thing */ +void kon_animPlay(kon_anim_t *anim, int first, int count, float fps, int loop); +void kon_animStep(kon_anim_t *anim, float dt); + +/* the frame to draw, the last one when a non looping animation has finished */ +int kon_animFrame(const kon_anim_t *anim); +int kon_animDone(const kon_anim_t *anim); + +/* draws frame index of a sheet of frameWidth by frameHeight frames with its top left at x, y */ +void kon_drawFrame(kon_framebuffer_t *fb, const kon_image *sheet, int frameWidth, int frameHeight, int index, int x, int y, int flip); + +#endif |
