From 78a5253dcf6499a34fd91e0cd28c922553613b29 Mon Sep 17 00:00:00 2001 From: Roderic Bos Date: Sat, 17 Feb 2024 20:43:44 +0100 Subject: [PATCH 1/7] setup, use offsetCamera --- examples/2d_camera_platformer.c | 321 ++++++++++++++++++++++++++++++++ index.html | 1 + nob.c | 10 + raylib.js | 54 +++++- 4 files changed, 381 insertions(+), 5 deletions(-) create mode 100644 examples/2d_camera_platformer.c diff --git a/examples/2d_camera_platformer.c b/examples/2d_camera_platformer.c new file mode 100644 index 0000000..c82dc6b --- /dev/null +++ b/examples/2d_camera_platformer.c @@ -0,0 +1,321 @@ +/******************************************************************************************* +* +* raylib [core] example - 2D Camera platformer +* +* Example originally created with raylib 2.5, last time updated with raylib 3.0 +* +* Example contributed by arvyy (@arvyy) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2019-2024 arvyy (@arvyy) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" + +void raylib_js_set_entry(void (*entry)(void)); + +#define G 400 +#define PLAYER_JUMP_SPD 350.0f +#define PLAYER_HOR_SPD 200.0f + +typedef struct Player { + Vector2 position; + float speed; + bool canJump; +} Player; + +typedef struct EnvItem { + Rectangle rect; + int blocking; + Color color; +} EnvItem; + +//---------------------------------------------------------------------------------- +// Module functions declaration +//---------------------------------------------------------------------------------- +void UpdatePlayer(Player *player, EnvItem *envItems, int envItemsLength, float delta); +void UpdateCameraCenter(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height); +void UpdateCameraCenterInsideMap(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height); +void UpdateCameraCenterSmoothFollow(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height); +void UpdateCameraEvenOutOnLanding(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height); +void UpdateCameraPlayerBoundsPush(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height); + + +//moved to globals for now +// used to be at the start of main when gameframe was inlined + + const int screenWidth = 800; + const int screenHeight = 450; + Player player = { 0 }; + EnvItem envItems[] = { + {{ 0, 0, 1000, 400 }, 0, LIGHTGRAY }, + {{ 0, 400, 1000, 200 }, 1, GRAY }, + {{ 300, 200, 400, 10 }, 1, GRAY }, + {{ 250, 300, 100, 10 }, 1, GRAY }, + {{ 650, 300, 100, 10 }, 1, GRAY } + }; + + int envItemsLength = sizeof(envItems)/sizeof(envItems[0]); + Camera2D camera = { 0 }; + + // Store pointers to the multiple update camera functions + void (*cameraUpdaters[])(Camera2D*, Player*, EnvItem*, int, float, int, int) = { + UpdateCameraCenter, + UpdateCameraCenterInsideMap, + UpdateCameraCenterSmoothFollow, + UpdateCameraEvenOutOnLanding, + UpdateCameraPlayerBoundsPush + }; + + int cameraOption = 0; + int cameraUpdatersLength = sizeof(cameraUpdaters)/sizeof(cameraUpdaters[0]); + + char *cameraDescriptions[] = { + "Follow player center", + "Follow player center, but clamp to map edges", + "Follow player center; smoothed", + "Follow player center horizontally; update player center vertically after landing", + "Player push camera on getting too close to screen edge" + }; + +void GameFrame() { + + // Update + //---------------------------------------------------------------------------------- + float deltaTime = GetFrameTime(); + + UpdatePlayer(&player, envItems, envItemsLength, deltaTime); + + camera.zoom += ((float)GetMouseWheelMove()*0.05f); + + if (camera.zoom > 3.0f) camera.zoom = 3.0f; + else if (camera.zoom < 0.25f) camera.zoom = 0.25f; + + if (IsKeyPressed(KEY_R)) + { + camera.zoom = 1.0f; + player.position = (Vector2){ 400, 280 }; + } + + if (IsKeyPressed(KEY_C)) cameraOption = (cameraOption + 1)%cameraUpdatersLength; + + // Call update camera function by its pointer + cameraUpdaters[cameraOption](&camera, &player, envItems, envItemsLength, deltaTime, screenWidth, screenHeight); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(LIGHTGRAY); + + BeginMode2D(camera); + + for (int i = 0; i < envItemsLength; i++) DrawRectangleRec(envItems[i].rect, envItems[i].color); + + Rectangle playerRect = { player.position.x - 20, player.position.y - 40, 40, 40 }; + DrawRectangleRec(playerRect, RED); + + DrawCircle(player.position.x, player.position.y, 5, GOLD); + + EndMode2D(); + + DrawText("Controls:", 20, 20, 10, BLACK); + DrawText("- Right/Left to move", 40, 40, 10, DARKGRAY); + DrawText("- Space to jump", 40, 60, 10, DARKGRAY); + DrawText("- Mouse Wheel to Zoom in-out, R to reset zoom", 40, 80, 10, DARKGRAY); + DrawText("- C to change camera mode", 40, 100, 10, DARKGRAY); + DrawText("Current camera mode:", 20, 120, 10, BLACK); + DrawText(cameraDescriptions[cameraOption], 40, 140, 10, DARKGRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- +} + + + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + + + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera"); + + player.position = (Vector2){ 400, 280 }; + player.speed = 0; + player.canJump = false; + + camera.target = player.position; + camera.offset = (Vector2){ screenWidth/2.0f, screenHeight/2.0f }; + camera.rotation = 0.0f; + camera.zoom = 1.0f; + + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + +#ifdef PLATFORM_WEB + raylib_js_set_entry(GameFrame); +#else + // Main game loop + while (!WindowShouldClose()) + { + GameFrame(); + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- +#endif + + return 0; +} + +void UpdatePlayer(Player *player, EnvItem *envItems, int envItemsLength, float delta) +{ + if (IsKeyDown(KEY_LEFT)) player->position.x -= PLAYER_HOR_SPD*delta; + if (IsKeyDown(KEY_RIGHT)) player->position.x += PLAYER_HOR_SPD*delta; + if (IsKeyDown(KEY_SPACE) && player->canJump) + { + player->speed = -PLAYER_JUMP_SPD; + player->canJump = false; + } + + bool hitObstacle = false; + for (int i = 0; i < envItemsLength; i++) + { + EnvItem *ei = envItems + i; + Vector2 *p = &(player->position); + if (ei->blocking && + ei->rect.x <= p->x && + ei->rect.x + ei->rect.width >= p->x && + ei->rect.y >= p->y && + ei->rect.y <= p->y + player->speed*delta) + { + hitObstacle = true; + player->speed = 0.0f; + p->y = ei->rect.y; + break; + } + } + + if (!hitObstacle) + { + player->position.y += player->speed*delta; + player->speed += G*delta; + player->canJump = false; + } + else player->canJump = true; +} + +void UpdateCameraCenter(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height) +{ + camera->offset = (Vector2){ width/2.0f, height/2.0f }; + camera->target = player->position; +} + +void UpdateCameraCenterInsideMap(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height) +{ + camera->target = player->position; + camera->offset = (Vector2){ width/2.0f, height/2.0f }; + float minX = 1000, minY = 1000, maxX = -1000, maxY = -1000; + + for (int i = 0; i < envItemsLength; i++) + { + EnvItem *ei = envItems + i; + minX = fminf(ei->rect.x, minX); + maxX = fmaxf(ei->rect.x + ei->rect.width, maxX); + minY = fminf(ei->rect.y, minY); + maxY = fmaxf(ei->rect.y + ei->rect.height, maxY); + } + + Vector2 max = GetWorldToScreen2D((Vector2){ maxX, maxY }, *camera); + Vector2 min = GetWorldToScreen2D((Vector2){ minX, minY }, *camera); + + if (max.x < width) camera->offset.x = width - (max.x - width/2); + if (max.y < height) camera->offset.y = height - (max.y - height/2); + if (min.x > 0) camera->offset.x = width/2 - min.x; + if (min.y > 0) camera->offset.y = height/2 - min.y; +} + +void UpdateCameraCenterSmoothFollow(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height) +{ + static float minSpeed = 30; + static float minEffectLength = 10; + static float fractionSpeed = 0.8f; + + camera->offset = (Vector2){ width/2.0f, height/2.0f }; + Vector2 diff = Vector2Subtract(player->position, camera->target); + float length = Vector2Length(diff); + + if (length > minEffectLength) + { + float speed = fmaxf(fractionSpeed*length, minSpeed); + camera->target = Vector2Add(camera->target, Vector2Scale(diff, speed*delta/length)); + } +} + +void UpdateCameraEvenOutOnLanding(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height) +{ + static float evenOutSpeed = 700; + static int eveningOut = false; + static float evenOutTarget; + + camera->offset = (Vector2){ width/2.0f, height/2.0f }; + camera->target.x = player->position.x; + + if (eveningOut) + { + if (evenOutTarget > camera->target.y) + { + camera->target.y += evenOutSpeed*delta; + + if (camera->target.y > evenOutTarget) + { + camera->target.y = evenOutTarget; + eveningOut = 0; + } + } + else + { + camera->target.y -= evenOutSpeed*delta; + + if (camera->target.y < evenOutTarget) + { + camera->target.y = evenOutTarget; + eveningOut = 0; + } + } + } + else + { + if (player->canJump && (player->speed == 0) && (player->position.y != camera->target.y)) + { + eveningOut = 1; + evenOutTarget = player->position.y; + } + } +} + +void UpdateCameraPlayerBoundsPush(Camera2D *camera, Player *player, EnvItem *envItems, int envItemsLength, float delta, int width, int height) +{ + static Vector2 bbox = { 0.2f, 0.2f }; + + Vector2 bboxWorldMin = GetScreenToWorld2D((Vector2){ (1 - bbox.x)*0.5f*width, (1 - bbox.y)*0.5f*height }, *camera); + Vector2 bboxWorldMax = GetScreenToWorld2D((Vector2){ (1 + bbox.x)*0.5f*width, (1 + bbox.y)*0.5f*height }, *camera); + camera->offset = (Vector2){ (1 - bbox.x)*0.5f * width, (1 - bbox.y)*0.5f*height }; + + if (player->position.x < bboxWorldMin.x) camera->target.x = player->position.x; + if (player->position.y < bboxWorldMin.y) camera->target.y = player->position.y; + if (player->position.x > bboxWorldMax.x) camera->target.x = bboxWorldMin.x + (player->position.x - bboxWorldMax.x); + if (player->position.y > bboxWorldMax.y) camera->target.y = bboxWorldMin.y + (player->position.y - bboxWorldMax.y); +} \ No newline at end of file diff --git a/index.html b/index.html index 960a2cd..8013807 100644 --- a/index.html +++ b/index.html @@ -72,6 +72,7 @@ "shapes": ["shapes_colors_palette"], "text": ["text_writing_anim"], "textures": ["textures_logo_raylib"], + "camera": ["2d_camera_platformer"], } const defaultWasm = Object.values(wasmPaths)[0][0]; diff --git a/nob.c b/nob.c index 15ef36d..2b75572 100644 --- a/nob.c +++ b/nob.c @@ -48,6 +48,11 @@ Example examples[] = { .bin_path = "./build/text_writing_anim", .wasm_path = "./wasm/text_writing_anim.wasm", }, + { + .src_path = "./examples/2d_camera_platformer.c", + .bin_path = "./build/2d_camera_platformer", + .wasm_path = "./wasm/2d_camera_platformer.wasm", + }, }; bool build_native(void) @@ -57,7 +62,12 @@ bool build_native(void) cmd.count = 0; nob_cmd_append(&cmd, "clang", "-I./include/"); nob_cmd_append(&cmd, "-o", examples[i].bin_path, examples[i].src_path); +#ifdef _WIN32 + //TODO check this Works on my Machine TM + nob_cmd_append(&cmd, "-L./lib/", "-lraylib", "-lopengl32", "-luser32", "-lmsvcrt", "-lgdi32", "-lshell32", "-lwinmm"); +#else nob_cmd_append(&cmd, "-L./lib/", "-lraylib", "-lm"); +#endif if (!nob_cmd_run_sync(cmd)) return 1; } } diff --git a/raylib.js b/raylib.js index a6a0154..817881e 100644 --- a/raylib.js +++ b/raylib.js @@ -44,6 +44,16 @@ class RaylibJs { this.currentMousePosition = {x: 0, y: 0}; this.images = []; this.quit = false; + + this.camera2DOffset = undefined; + } + + applyCameraOffset(x, y) { + if (this.camera2DOffset) { + x += this.camera2DOffset.x; + y += this.camera2DOffset.y; + } + return [x, y]; } constructor() { @@ -146,8 +156,10 @@ class RaylibJs { DrawCircleV(center_ptr, radius, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; - const [x, y] = new Float32Array(buffer, center_ptr, 2); + let [x, y] = new Float32Array(buffer, center_ptr, 2); + [x, y] = this.applyCameraOffset(x, y); const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4); + const color = color_hex_unpacked(r, g, b, a); this.ctx.beginPath(); this.ctx.arc(x, y, radius, 0, 2*Math.PI, false); @@ -164,7 +176,8 @@ class RaylibJs { DrawText(text_ptr, posX, posY, fontSize, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; const text = cstr_by_ptr(buffer, text_ptr); - const color = getColorFromMemory(buffer, color_ptr); + const color = getColorFromMemory(buffer, color_ptr); + [posX, posY] = this.applyCameraOffset(posX, posY); fontSize *= this.#FONT_SCALE_MAGIC; this.ctx.fillStyle = color; // TODO: since the default font is part of Raylib the css that defines it should be located in raylib.js and not in index.html @@ -180,6 +193,7 @@ class RaylibJs { DrawRectangle(posX, posY, width, height, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; const color = getColorFromMemory(buffer, color_ptr); + [posX, posY] = this.applyCameraOffset(posX, posY); this.ctx.fillStyle = color; this.ctx.fillRect(posX, posY, width, height); } @@ -243,7 +257,8 @@ class RaylibJs { DrawRectangleRec(rec_ptr, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; - const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4); + let [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4); + [x, y] = this.applyCameraOffset(x, y); const color = getColorFromMemory(buffer, color_ptr); this.ctx.fillStyle = color; this.ctx.fillRect(x, y, w, h); @@ -251,7 +266,9 @@ class RaylibJs { DrawRectangleLinesEx(rec_ptr, lineThick, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; - const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4); + let [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4); + [x, y] = this.applyCameraOffset(x, y); + const color = getColorFromMemory(buffer, color_ptr); this.ctx.strokeStyle = color; this.ctx.lineWidth = lineThick; @@ -337,12 +354,39 @@ class RaylibJs { DrawTextEx(font, text_ptr, position_ptr, fontSize, spacing, tint_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; const text = cstr_by_ptr(buffer, text_ptr); - const [posX, posY] = new Float32Array(buffer, position_ptr, 2); + let [posX, posY] = new Float32Array(buffer, position_ptr, 2); const tint = getColorFromMemory(buffer, tint_ptr); + [posX, posY] = this.applyCameraOffset(posX,posY); this.ctx.fillStyle = tint; this.ctx.font = fontSize+"px myfont"; this.ctx.fillText(text, posX, posY + fontSize); } + + // Newly added + BeginMode2D(camera_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const [offsetX, offsetY, targetX, targetY, rotation, zoom] = new Float32Array(buffer, camera_ptr, 6); + //console.log('BeginMode2D', offsetX, offsetY, targetX, targetY, rotation, zoom); + if (rotation !== 0) throw Error("Rotation not yet supported"); + if (zoom !== 1) throw Error("Zoom not yet supported"); + + this.camera2DOffset = { x: offsetX - targetX, y: offsetY - targetY }; + + + } + EndMode2D() {this.camera2DOffset = undefined;} + DrawCircle(posX, posY, radius, color_ptr) + { + const buffer = this.wasm.instance.exports.memory.buffer; + const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4); + const color = color_hex_unpacked(r, g, b, a); + [posX, posY] = this.applyCameraOffset(posX,posY); + this.ctx.beginPath(); + this.ctx.arc(posX, posY, radius, 0, 2*Math.PI, false); + this.ctx.fillStyle = color; + this.ctx.fill(); + + } raylib_js_set_entry(entry) { this.entryFunction = this.wasm.instance.exports.__indirect_function_table.get(entry); From 0c4b6b75af444040d2897c67ea4ec260a8bdd4f3 Mon Sep 17 00:00:00 2001 From: Roderic Bos Date: Sat, 17 Feb 2024 21:31:50 +0100 Subject: [PATCH 2/7] remove stdlib dependency and add raymath implementation --- examples/2d_camera_platformer.c | 21 ++++++++++++++++----- raylib.js | 17 +++++++++-------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/examples/2d_camera_platformer.c b/examples/2d_camera_platformer.c index c82dc6b..651253f 100644 --- a/examples/2d_camera_platformer.c +++ b/examples/2d_camera_platformer.c @@ -14,6 +14,7 @@ ********************************************************************************************/ #include "raylib.h" +#define RAYMATH_IMPLEMENTATION #include "raymath.h" void raylib_js_set_entry(void (*entry)(void)); @@ -81,6 +82,16 @@ void UpdateCameraPlayerBoundsPush(Camera2D *camera, Player *player, EnvItem *env "Follow player center horizontally; update player center vertically after landing", "Player push camera on getting too close to screen edge" }; + +//custom MATH funcitons +float myfminf(float a, float b) { + if (b < a ) return b; + return a; +} +float myfmaxf(float a, float b) { + if (b > a ) return b; + return a; +} void GameFrame() { @@ -232,10 +243,10 @@ void UpdateCameraCenterInsideMap(Camera2D *camera, Player *player, EnvItem *envI for (int i = 0; i < envItemsLength; i++) { EnvItem *ei = envItems + i; - minX = fminf(ei->rect.x, minX); - maxX = fmaxf(ei->rect.x + ei->rect.width, maxX); - minY = fminf(ei->rect.y, minY); - maxY = fmaxf(ei->rect.y + ei->rect.height, maxY); + minX = myfminf(ei->rect.x, minX); + maxX = myfmaxf(ei->rect.x + ei->rect.width, maxX); + minY = myfminf(ei->rect.y, minY); + maxY = myfmaxf(ei->rect.y + ei->rect.height, maxY); } Vector2 max = GetWorldToScreen2D((Vector2){ maxX, maxY }, *camera); @@ -259,7 +270,7 @@ void UpdateCameraCenterSmoothFollow(Camera2D *camera, Player *player, EnvItem *e if (length > minEffectLength) { - float speed = fmaxf(fractionSpeed*length, minSpeed); + float speed = myfmaxf(fractionSpeed*length, minSpeed); camera->target = Vector2Add(camera->target, Vector2Scale(diff, speed*delta/length)); } } diff --git a/raylib.js b/raylib.js index 817881e..85b8072 100644 --- a/raylib.js +++ b/raylib.js @@ -45,13 +45,13 @@ class RaylibJs { this.images = []; this.quit = false; - this.camera2DOffset = undefined; + this.cameraOffset2D = undefined; } applyCameraOffset(x, y) { - if (this.camera2DOffset) { - x += this.camera2DOffset.x; - y += this.camera2DOffset.y; + if (this.cameraOffset2D) { + x += this.cameraOffset2D.x; + y += this.cameraOffset2D.y; } return [x, y]; } @@ -370,11 +370,9 @@ class RaylibJs { if (rotation !== 0) throw Error("Rotation not yet supported"); if (zoom !== 1) throw Error("Zoom not yet supported"); - this.camera2DOffset = { x: offsetX - targetX, y: offsetY - targetY }; - - + this.cameraOffset2D = { x: offsetX - targetX, y: offsetY - targetY }; } - EndMode2D() {this.camera2DOffset = undefined;} + EndMode2D() {this.cameraOffset2D = undefined;} DrawCircle(posX, posY, radius, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; @@ -387,6 +385,9 @@ class RaylibJs { this.ctx.fill(); } + GetWorldToScreen2D() {} + GetScreenToWorld2D() {} + //End newly added raylib_js_set_entry(entry) { this.entryFunction = this.wasm.instance.exports.__indirect_function_table.get(entry); From 00aa97c748abead48e618794df14a6a1f9924e49 Mon Sep 17 00:00:00 2001 From: Roderic Bos Date: Sat, 17 Feb 2024 23:56:58 +0100 Subject: [PATCH 3/7] basic math stuff --- raylib.js | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 7 deletions(-) diff --git a/raylib.js b/raylib.js index 85b8072..1f181d4 100644 --- a/raylib.js +++ b/raylib.js @@ -356,7 +356,7 @@ class RaylibJs { const text = cstr_by_ptr(buffer, text_ptr); let [posX, posY] = new Float32Array(buffer, position_ptr, 2); const tint = getColorFromMemory(buffer, tint_ptr); - [posX, posY] = this.applyCameraOffset(posX,posY); + [posX, posY] = this.applyCameraOffset(posX, posY); this.ctx.fillStyle = tint; this.ctx.font = fontSize+"px myfont"; this.ctx.fillText(text, posX, posY + fontSize); @@ -366,27 +366,61 @@ class RaylibJs { BeginMode2D(camera_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; const [offsetX, offsetY, targetX, targetY, rotation, zoom] = new Float32Array(buffer, camera_ptr, 6); - //console.log('BeginMode2D', offsetX, offsetY, targetX, targetY, rotation, zoom); if (rotation !== 0) throw Error("Rotation not yet supported"); if (zoom !== 1) throw Error("Zoom not yet supported"); - this.cameraOffset2D = { x: offsetX - targetX, y: offsetY - targetY }; + this.cameraOffset2D = { x: offsetX - targetX, y: offsetY - targetY }; + } + EndMode2D() { + this.cameraOffset2D = undefined; } - EndMode2D() {this.cameraOffset2D = undefined;} DrawCircle(posX, posY, radius, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4); const color = color_hex_unpacked(r, g, b, a); - [posX, posY] = this.applyCameraOffset(posX,posY); + [posX, posY] = this.applyCameraOffset(posX, posY); this.ctx.beginPath(); this.ctx.arc(posX, posY, radius, 0, 2*Math.PI, false); this.ctx.fillStyle = color; this.ctx.fill(); } - GetWorldToScreen2D() {} - GetScreenToWorld2D() {} + GetWorldToScreen2D(result_ptr, position_ptr, camera_ptr) {//COPY PASTE TO BELOW + const buffer = this.wasm.instance.exports.memory.buffer; + let [posX, posY] = new Float32Array(buffer, position_ptr, 2); + const [offsetX, offsetY, targetX, targetY, rotation, zoom] = new Float32Array(buffer, camera_ptr, 6); + + const matOrigin = matrixTranslate(-targetX, -targetY, 0.0); + const matRotation = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix + const matScale = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix + const matTranslation = matrixTranslate(offsetX, offsetY, 0.0); + + const matCamera = matrixMultiply(matrixMultiply(matOrigin, matrixMultiply(matScale, matRotation)), matTranslation); + + [posX, posY] = vector3Transform([posX, posY, 0.0], matCamera); + + //return + new Float32Array(buffer, result_ptr, 2).set([posX, posY]); + } + GetScreenToWorld2D(result_ptr, position_ptr, camera_ptr) { //COPY PASTE FROM ABOVE + const buffer = this.wasm.instance.exports.memory.buffer; + let [posX, posY] = new Float32Array(buffer, position_ptr, 2); + const [offsetX, offsetY, targetX, targetY, rotation, zoom] = new Float32Array(buffer, camera_ptr, 6); + + const matOrigin = matrixTranslate(-targetX, -targetY, 0.0); + const matRotation = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix + const matScale = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix + const matTranslation = matrixTranslate(offsetX, offsetY, 0.0); + + const matCamera = matrixMultiply(matrixMultiply(matOrigin, matrixMultiply(matScale, matRotation)), matTranslation); + const invertedCamera = matrixInvert(matCamera); + + [posX, posY] = vector3Transform([posX, posY, 0.0], invertedCamera); + + //return + new Float32Array(buffer, result_ptr, 2).set([posX, posY]); + } //End newly added raylib_js_set_entry(entry) { @@ -554,3 +588,91 @@ function getColorFromMemory(buffer, color_ptr) { const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4); return color_hex_unpacked(r, g, b, a); } + +//matrix functions implementation taken from raylib sourcecode +function matrixTranslate(x, y, z) +{ + return [1.0, 0.0, 0.0, x, + 0.0, 1.0, 0.0, y, + 0.0, 0.0, 1.0, z, + 0.0, 0.0, 0.0, 1.0 + ] +} +function matrixMultiply(left, right) { + const mat = []; + mat[0] = left[0]*right[0] + left[1]*right[4] + left[2]*right[8] + left[3]*right[12]; + mat[1] = left[0]*right[1] + left[1]*right[5] + left[2]*right[9] + left[3]*right[13]; + mat[2] = left[0]*right[2] + left[1]*right[6] + left[2]*right[10] + left[3]*right[14]; + mat[3] = left[0]*right[3] + left[1]*right[7] + left[2]*right[11] + left[3]*right[15]; + mat[4] = left[4]*right[0] + left[5]*right[4] + left[6]*right[8] + left[7]*right[12]; + mat[5] = left[4]*right[1] + left[5]*right[5] + left[6]*right[9] + left[7]*right[13]; + mat[6] = left[4]*right[2] + left[5]*right[6] + left[6]*right[10] + left[7]*right[14]; + mat[7] = left[4]*right[3] + left[5]*right[7] + left[6]*right[11] + left[7]*right[15]; + mat[8] = left[8]*right[0] + left[9]*right[4] + left[10]*right[8] + left[11]*right[12]; + mat[9] = left[8]*right[1] + left[9]*right[5] + left[10]*right[9] + left[11]*right[13]; + mat[10] = left[8]*right[2] + left[9]*right[6] + left[10]*right[10] + left[11]*right[14]; + mat[11] = left[8]*right[3] + left[9]*right[7] + left[10]*right[11] + left[11]*right[15]; + mat[12] = left[12]*right[0] + left[13]*right[4] + left[14]*right[8] + left[15]*right[12]; + mat[13] = left[12]*right[1] + left[13]*right[5] + left[14]*right[9] + left[15]*right[13]; + mat[14] = left[12]*right[2] + left[13]*right[6] + left[14]*right[10] + left[15]*right[14]; + mat[15] = left[12]*right[3] + left[13]*right[7] + left[14]*right[11] + left[15]*right[15]; + return mat; +} + +function matrixInvert(mat) { + const result = []; + + // Cache the matrix values (speed optimization) + const a00 = mat[0], a01 = mat[1], a02 = mat[2], a03 = mat[3]; + const a10 = mat[4], a11 = mat[5], a12 = mat[6], a13 = mat[7]; + const a20 = mat[8], a21 = mat[9], a22 = mat[10], a23 = mat[11]; + const a30 = mat[12], a31 = mat[13], a32 = mat[14], a33 = mat[15]; + + const b00 = a00*a11 - a01*a10; + const b01 = a00*a12 - a02*a10; + const b02 = a00*a13 - a03*a10; + const b03 = a01*a12 - a02*a11; + const b04 = a01*a13 - a03*a11; + const b05 = a02*a13 - a03*a12; + const b06 = a20*a31 - a21*a30; + const b07 = a20*a32 - a22*a30; + const b08 = a20*a33 - a23*a30; + const b09 = a21*a32 - a22*a31; + const b10 = a21*a33 - a23*a31; + const b11 = a22*a33 - a23*a32; + + // Calculate the invert determinant (inlined to avoid double-caching) + const invDet = 1.0/(b00*b11 - b01*b10 + b02*b09 + b03*b08 - b04*b07 + b05*b06); + + result[0] = (a11*b11 - a12*b10 + a13*b09)*invDet; + result[1] = (-a01*b11 + a02*b10 - a03*b09)*invDet; + result[2] = (a31*b05 - a32*b04 + a33*b03)*invDet; + result[3] = (-a21*b05 + a22*b04 - a23*b03)*invDet; + result[4] = (-a10*b11 + a12*b08 - a13*b07)*invDet; + result[5] = (a00*b11 - a02*b08 + a03*b07)*invDet; + result[6] = (-a30*b05 + a32*b02 - a33*b01)*invDet; + result[7] = (a20*b05 - a22*b02 + a23*b01)*invDet; + result[8] = (a10*b10 - a11*b08 + a13*b06)*invDet; + result[9] = (-a00*b10 + a01*b08 - a03*b06)*invDet; + result[10] = (a30*b04 - a31*b02 + a33*b00)*invDet; + result[11] = (-a20*b04 + a21*b02 - a23*b00)*invDet; + result[12] = (-a10*b09 + a11*b07 - a12*b06)*invDet; + result[13] = (a00*b09 - a01*b07 + a02*b06)*invDet; + result[14] = (-a30*b03 + a31*b01 - a32*b00)*invDet; + result[15] = (a20*b03 - a21*b01 + a22*b00)*invDet; + + return result; +} + +function vector3Transform(v, mat) { + + const x = v[0]; + const y = v[1]; + const z = v[2]; + + const posX = mat[0]*x + mat[4]*y + mat[8]*z + mat[12]; + const posY = mat[1]*x + mat[5]*y + mat[9]*z + mat[13]; + const posZ = mat[2]*x + mat[6]*y + mat[10]*z + mat[14]; + + return [posX, posY, posZ]; +} \ No newline at end of file From 4a4031fd65d6c031451130904b4b0737b148648d Mon Sep 17 00:00:00 2001 From: Roderic Bos Date: Sun, 18 Feb 2024 10:02:52 +0100 Subject: [PATCH 4/7] extract getCameraMatrix2D --- raylib.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/raylib.js b/raylib.js index 1f181d4..5d1a0dd 100644 --- a/raylib.js +++ b/raylib.js @@ -391,12 +391,7 @@ class RaylibJs { let [posX, posY] = new Float32Array(buffer, position_ptr, 2); const [offsetX, offsetY, targetX, targetY, rotation, zoom] = new Float32Array(buffer, camera_ptr, 6); - const matOrigin = matrixTranslate(-targetX, -targetY, 0.0); - const matRotation = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix - const matScale = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix - const matTranslation = matrixTranslate(offsetX, offsetY, 0.0); - - const matCamera = matrixMultiply(matrixMultiply(matOrigin, matrixMultiply(matScale, matRotation)), matTranslation); + const matCamera = getCameraMatrix2D(offsetX, offsetY, targetX, targetY, rotation, zoom); [posX, posY] = vector3Transform([posX, posY, 0.0], matCamera); @@ -408,12 +403,7 @@ class RaylibJs { let [posX, posY] = new Float32Array(buffer, position_ptr, 2); const [offsetX, offsetY, targetX, targetY, rotation, zoom] = new Float32Array(buffer, camera_ptr, 6); - const matOrigin = matrixTranslate(-targetX, -targetY, 0.0); - const matRotation = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix - const matScale = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix - const matTranslation = matrixTranslate(offsetX, offsetY, 0.0); - - const matCamera = matrixMultiply(matrixMultiply(matOrigin, matrixMultiply(matScale, matRotation)), matTranslation); + const matCamera = getCameraMatrix2D(offsetX, offsetY, targetX, targetY, rotation, zoom); const invertedCamera = matrixInvert(matCamera); [posX, posY] = vector3Transform([posX, posY, 0.0], invertedCamera); @@ -590,14 +580,26 @@ function getColorFromMemory(buffer, color_ptr) { } //matrix functions implementation taken from raylib sourcecode + +function getCameraMatrix2D(offsetX, offsetY, targetX, targetY, rotation, zoom) +{ + const matOrigin = matrixTranslate(-targetX, -targetY, 0.0); + const matRotation = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix + const matScale = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix + const matTranslation = matrixTranslate(offsetX, offsetY, 0.0); + + const matCamera = matrixMultiply(matrixMultiply(matOrigin, matrixMultiply(matScale, matRotation)), matTranslation); + return matCamera; +} + function matrixTranslate(x, y, z) { return [1.0, 0.0, 0.0, x, 0.0, 1.0, 0.0, y, 0.0, 0.0, 1.0, z, - 0.0, 0.0, 0.0, 1.0 - ] + 0.0, 0.0, 0.0, 1.0] } + function matrixMultiply(left, right) { const mat = []; mat[0] = left[0]*right[0] + left[1]*right[4] + left[2]*right[8] + left[3]*right[12]; From 9e5b9118aef71892eb0e4830d2334cfb95401bf5 Mon Sep 17 00:00:00 2001 From: Roderic Bos Date: Sun, 18 Feb 2024 11:41:08 +0100 Subject: [PATCH 5/7] fix matrixTranslate bug and add matrixRotate and matrixScale for future implementation --- raylib.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/raylib.js b/raylib.js index 5d1a0dd..84b120c 100644 --- a/raylib.js +++ b/raylib.js @@ -45,13 +45,14 @@ class RaylibJs { this.images = []; this.quit = false; - this.cameraOffset2D = undefined; + this.camera2D = undefined; } applyCameraOffset(x, y) { - if (this.cameraOffset2D) { - x += this.cameraOffset2D.x; - y += this.cameraOffset2D.y; + if (this.camera2D) { + const [offsetX, offsetY, targetX, targetY, rotation, zoom] = this.camera2D; + x += (offsetX - targetX); + y += (offsetY - targetY); } return [x, y]; } @@ -369,11 +370,13 @@ class RaylibJs { if (rotation !== 0) throw Error("Rotation not yet supported"); if (zoom !== 1) throw Error("Zoom not yet supported"); - this.cameraOffset2D = { x: offsetX - targetX, y: offsetY - targetY }; + this.camera2D = [offsetX, offsetY, targetX, targetY, rotation, zoom]; } + EndMode2D() { - this.cameraOffset2D = undefined; + this.camera2D = undefined; } + DrawCircle(posX, posY, radius, color_ptr) { const buffer = this.wasm.instance.exports.memory.buffer; @@ -386,6 +389,7 @@ class RaylibJs { this.ctx.fill(); } + GetWorldToScreen2D(result_ptr, position_ptr, camera_ptr) {//COPY PASTE TO BELOW const buffer = this.wasm.instance.exports.memory.buffer; let [posX, posY] = new Float32Array(buffer, position_ptr, 2); @@ -584,20 +588,28 @@ function getColorFromMemory(buffer, color_ptr) { function getCameraMatrix2D(offsetX, offsetY, targetX, targetY, rotation, zoom) { const matOrigin = matrixTranslate(-targetX, -targetY, 0.0); - const matRotation = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix - const matScale = matrixTranslate(0.0, 0.0, 0.0); //TODO implement this, currently using identity matrix + const matRotation = matrixRotate(0.0, 0.0, 1.0, rotation*(Math.PI/180.0)); + const matScale = matrixScale(zoom, zoom, 1.0); const matTranslation = matrixTranslate(offsetX, offsetY, 0.0); const matCamera = matrixMultiply(matrixMultiply(matOrigin, matrixMultiply(matScale, matRotation)), matTranslation); return matCamera; } +function matrixScale(x, y, z) +{ + return [ x, 0.0, 0.0, 0.0, + 0.0, y, 0.0, 0.0, + 0.0, 0.0, z, 0.0, + 0.0, 0.0, 0.0, 1.0]; +} + function matrixTranslate(x, y, z) { - return [1.0, 0.0, 0.0, x, - 0.0, 1.0, 0.0, y, - 0.0, 0.0, 1.0, z, - 0.0, 0.0, 0.0, 1.0] + return [1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + x, y, z, 1.0] } function matrixMultiply(left, right) { @@ -666,6 +678,48 @@ function matrixInvert(mat) { return result; } +function matrixRotate(x, y, z, angle) +{ + const result = []; + + const lengthSquared = x*x + y*y + z*z; + + if ((lengthSquared != 1.0) && (lengthSquared != 0.0)) + { + const ilength = 1.0/Math.sqrt(lengthSquared); + x *= ilength; + y *= ilength; + z *= ilength; + } + + const sinres = Math.sin(angle); + const cosres = Math.cos(angle); + const t = 1.0 - cosres; + + result[0] = x*x*t + cosres; + result[1] = y*x*t + z*sinres; + result[2] = z*x*t - y*sinres; + result[3] = 0.0; + + result[4] = x*y*t - z*sinres; + result[5] = y*y*t + cosres; + result[6] = z*y*t + x*sinres; + result[7] = 0.0; + + result[8] = x*z*t + y*sinres; + result[9] = y*z*t - x*sinres; + result[10] = z*z*t + cosres; + result[11] = 0.0; + + result[12] = 0.0; + result[13] = 0.0; + result[14] = 0.0; + result[15] = 1.0; + + return result; +} + + function vector3Transform(v, mat) { const x = v[0]; From e15285d51b02b569526f491b508a84f31815a7f7 Mon Sep 17 00:00:00 2001 From: Roderic Bos Date: Sun, 18 Feb 2024 14:17:42 +0100 Subject: [PATCH 6/7] Added examples\core_2d_camera.c and enabled zoom and rotate --- examples/core_2d_camera.c | 152 ++++++++++++++++++++++++++++++++++++++ index.html | 2 +- raylib.js | 55 ++++++++++++-- 3 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 examples/core_2d_camera.c diff --git a/examples/core_2d_camera.c b/examples/core_2d_camera.c new file mode 100644 index 0000000..10dc956 --- /dev/null +++ b/examples/core_2d_camera.c @@ -0,0 +1,152 @@ +/******************************************************************************************* +* +* raylib [core] example - 2D Camera system +* +* Example originally created with raylib 1.5, last time updated with raylib 3.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2016-2024 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" + +void raylib_js_set_entry(void (*entry)(void)); + +#define MAX_BUILDINGS 100 + +const int screenWidth = 800; +const int screenHeight = 450; + +Rectangle player = { 400, 280, 40, 40 }; +Rectangle buildings[MAX_BUILDINGS] = { 0 }; +Color buildColors[MAX_BUILDINGS] = { 0 }; + +Camera2D camera = { 0 }; + + +void GameFrame() +{ + // Update + //---------------------------------------------------------------------------------- + // Player movement + if (IsKeyDown(KEY_RIGHT)) player.x += 2; + else if (IsKeyDown(KEY_LEFT)) player.x -= 2; + + // Camera target follows player + camera.target = (Vector2){ player.x + 20, player.y + 20 }; + + // Camera rotation controls + if (IsKeyDown(KEY_A)) camera.rotation--; + else if (IsKeyDown(KEY_S)) camera.rotation++; + + // Limit camera rotation to 80 degrees (-40 to 40) + if (camera.rotation > 40) camera.rotation = 40; + else if (camera.rotation < -40) camera.rotation = -40; + + // Camera zoom controls + camera.zoom += ((float)GetMouseWheelMove()*0.05f); + + if (camera.zoom > 3.0f) camera.zoom = 3.0f; + else if (camera.zoom < 0.1f) camera.zoom = 0.1f; + + // Camera reset (zoom and rotation) + if (IsKeyPressed(KEY_R)) + { + camera.zoom = 1.0f; + camera.rotation = 0.0f; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + BeginMode2D(camera); + + DrawRectangle(-6000, 320, 13000, 8000, DARKGRAY); + + for (int i = 0; i < MAX_BUILDINGS; i++) DrawRectangleRec(buildings[i], buildColors[i]); + + DrawRectangleRec(player, RED); + + DrawLine((int)camera.target.x, -screenHeight*10, (int)camera.target.x, screenHeight*10, GREEN); + DrawLine(-screenWidth*10, (int)camera.target.y, screenWidth*10, (int)camera.target.y, GREEN); + + EndMode2D(); + + DrawText("SCREEN AREA", 640, 10, 20, RED); + + DrawRectangle(0, 0, screenWidth, 5, RED); + DrawRectangle(0, 5, 5, screenHeight - 10, RED); + DrawRectangle(screenWidth - 5, 5, 5, screenHeight - 10, RED); + DrawRectangle(0, screenHeight - 5, screenWidth, 5, RED); + + DrawRectangle( 10, 10, 250, 113, Fade(SKYBLUE, 0.5f)); + DrawRectangleLines( 10, 10, 250, 113, BLUE); + + DrawText("Free 2d camera controls:", 20, 20, 10, BLACK); + DrawText("- Right/Left to move Offset", 40, 40, 10, DARKGRAY); + DrawText("- Mouse Wheel to Zoom in-out", 40, 60, 10, DARKGRAY); + DrawText("- A / S to Rotate", 40, 80, 10, DARKGRAY); + DrawText("- R to reset Zoom and Rotation", 40, 100, 10, DARKGRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- +} + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + + + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera"); + + + int spacing = 0; + + for (int i = 0; i < MAX_BUILDINGS; i++) + { + buildings[i].width = (float)GetRandomValue(50, 200); + buildings[i].height = (float)GetRandomValue(100, 800); + buildings[i].y = screenHeight - 130.0f - buildings[i].height; + buildings[i].x = -6000.0f + spacing; + + spacing += (int)buildings[i].width; + + buildColors[i] = (Color){ GetRandomValue(200, 240), GetRandomValue(200, 240), GetRandomValue(200, 250), 255 }; + } + + camera.target = (Vector2){ player.x + 20.0f, player.y + 20.0f }; + camera.offset = (Vector2){ screenWidth/2.0f, screenHeight/2.0f }; + camera.rotation = 0.0f; + camera.zoom = 1.0f; + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + +#ifdef PLATFORM_WEB + raylib_js_set_entry(GameFrame); +#else + // Main game loop + while (!WindowShouldClose()) + { + GameFrame(); + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- +#endif + + return 0; +} diff --git a/index.html b/index.html index 8013807..21ac22f 100644 --- a/index.html +++ b/index.html @@ -68,7 +68,7 @@