diff --git a/examples/physics_basic_rigidbody.c b/examples/physics_basic_rigidbody.c index c604dd14b..f0edba723 100644 --- a/examples/physics_basic_rigidbody.c +++ b/examples/physics_basic_rigidbody.c @@ -12,7 +12,7 @@ #include "raylib.h" #define MOVE_VELOCITY 5 -#define JUMP_VELOCITY 35 +#define JUMP_VELOCITY 30 int main() { @@ -22,42 +22,34 @@ int main() int screenHeight = 450; InitWindow(screenWidth, screenHeight, "raylib [physac] example - basic rigidbody"); - InitPhysics(); // Initialize physics module + InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module SetTargetFPS(60); // Debug variables bool isDebug = false; - // Player physic object - PhysicObject *player = CreatePhysicObject((Vector2){ screenWidth*0.25f, screenHeight/2 }, 0.0f, (Vector2){ 50, 50 }); - player->rigidbody.enabled = true; // Enable physic object rigidbody behaviour - player->rigidbody.applyGravity = true; - player->rigidbody.friction = 0.3f; - player->collider.enabled = true; // Enable physic object collisions detection + // Create rectangle physic object + PhysicObject *rectangle = CreatePhysicObject((Vector2){ screenWidth*0.25f, screenHeight/2 }, 0.0f, (Vector2){ 75, 50 }); + rectangle->rigidbody.enabled = true; // Enable physic object rigidbody behaviour + rectangle->rigidbody.applyGravity = true; + rectangle->rigidbody.friction = 0.1f; + rectangle->rigidbody.bounciness = 6.0f; - // Player physic object - PhysicObject *player2 = CreatePhysicObject((Vector2){ screenWidth*0.75f, screenHeight/2 }, 0.0f, (Vector2){ 50, 50 }); - player2->rigidbody.enabled = true; - player2->rigidbody.applyGravity = true; - player2->rigidbody.friction = 0.1f; - player2->collider.enabled = true; + // Create square physic object + PhysicObject *square = CreatePhysicObject((Vector2){ screenWidth*0.75f, screenHeight/2 }, 0.0f, (Vector2){ 50, 50 }); + square->rigidbody.enabled = true; // Enable physic object rigidbody behaviour + square->rigidbody.applyGravity = true; + square->rigidbody.friction = 0.1f; - // Floor physic object + // Create walls physic objects PhysicObject *floor = CreatePhysicObject((Vector2){ screenWidth/2, screenHeight*0.95f }, 0.0f, (Vector2){ screenWidth*0.9f, 100 }); - floor->collider.enabled = true; // Enable just physic object collisions detection - - // Left wall physic object PhysicObject *leftWall = CreatePhysicObject((Vector2){ 0.0f, screenHeight/2 }, 0.0f, (Vector2){ screenWidth*0.1f, screenHeight }); - leftWall->collider.enabled = true; - - // Right wall physic object PhysicObject *rightWall = CreatePhysicObject((Vector2){ screenWidth, screenHeight/2 }, 0.0f, (Vector2){ screenWidth*0.1f, screenHeight }); - rightWall->collider.enabled = true; + PhysicObject *roof = CreatePhysicObject((Vector2){ screenWidth/2, screenHeight*0.05f }, 0.0f, (Vector2){ screenWidth*0.9f, 100 }); - // Platform physic objectdd + // Create pplatform physic object PhysicObject *platform = CreatePhysicObject((Vector2){ screenWidth/2, screenHeight*0.7f }, 0.0f, (Vector2){ screenWidth*0.25f, 20 }); - platform->collider.enabled = true; //-------------------------------------------------------------------------------------- @@ -68,20 +60,18 @@ int main() //---------------------------------------------------------------------------------- UpdatePhysics(); // Update all created physic objects - // Check debug switch input - if (IsKeyPressed('P')) isDebug = !isDebug; - - // Check player movement inputs - if (IsKeyDown('W') && player->rigidbody.isGrounded) player->rigidbody.velocity.y = JUMP_VELOCITY; - - if (IsKeyDown('A')) player->rigidbody.velocity.x = -MOVE_VELOCITY; - else if (IsKeyDown('D')) player->rigidbody.velocity.x = MOVE_VELOCITY; + // Check rectangle movement inputs + if (IsKeyDown('W') && rectangle->rigidbody.isGrounded) rectangle->rigidbody.velocity.y = JUMP_VELOCITY; + if (IsKeyDown('A')) rectangle->rigidbody.velocity.x = -MOVE_VELOCITY; + else if (IsKeyDown('D')) rectangle->rigidbody.velocity.x = MOVE_VELOCITY; // Check player 2 movement inputs - if (IsKeyDown(KEY_UP) && player2->rigidbody.isGrounded) player2->rigidbody.velocity.y = JUMP_VELOCITY; + if (IsKeyDown(KEY_UP) && square->rigidbody.isGrounded) square->rigidbody.velocity.y = JUMP_VELOCITY; + if (IsKeyDown(KEY_LEFT)) square->rigidbody.velocity.x = -MOVE_VELOCITY; + else if (IsKeyDown(KEY_RIGHT)) square->rigidbody.velocity.x = MOVE_VELOCITY; - if (IsKeyDown(KEY_LEFT)) player2->rigidbody.velocity.x = -MOVE_VELOCITY; - else if (IsKeyDown(KEY_RIGHT)) player2->rigidbody.velocity.x = MOVE_VELOCITY; + // Check debug switch input + if (IsKeyPressed('P')) isDebug = !isDebug; //---------------------------------------------------------------------------------- // Draw @@ -89,29 +79,31 @@ int main() BeginDrawing(); ClearBackground(RAYWHITE); + + // Convert transform values to rectangle data type variable + DrawRectangleRec(TransformToRectangle(floor->transform), DARKGRAY); + DrawRectangleRec(TransformToRectangle(leftWall->transform), DARKGRAY); + DrawRectangleRec(TransformToRectangle(rightWall->transform), DARKGRAY); + DrawRectangleRec(TransformToRectangle(roof->transform), DARKGRAY); + + DrawRectangleRec(TransformToRectangle(platform->transform), DARKGRAY); + + DrawRectangleRec(TransformToRectangle(rectangle->transform), RED); + DrawRectangleRec(TransformToRectangle(square->transform), BLUE); if (isDebug) { DrawRectangleLines(floor->collider.bounds.x, floor->collider.bounds.y, floor->collider.bounds.width, floor->collider.bounds.height, GREEN); DrawRectangleLines(leftWall->collider.bounds.x, leftWall->collider.bounds.y, leftWall->collider.bounds.width, leftWall->collider.bounds.height, GREEN); DrawRectangleLines(rightWall->collider.bounds.x, rightWall->collider.bounds.y, rightWall->collider.bounds.width, rightWall->collider.bounds.height, GREEN); + DrawRectangleLines(roof->collider.bounds.x, roof->collider.bounds.y, roof->collider.bounds.width, roof->collider.bounds.height, GREEN); DrawRectangleLines(platform->collider.bounds.x, platform->collider.bounds.y, platform->collider.bounds.width, platform->collider.bounds.height, GREEN); - DrawRectangleLines(player->collider.bounds.x, player->collider.bounds.y, player->collider.bounds.width, player->collider.bounds.height, GREEN); - DrawRectangleLines(player2->collider.bounds.x, player2->collider.bounds.y, player2->collider.bounds.width, player2->collider.bounds.height, GREEN); - } - else - { - // Convert transform values to rectangle data type variable - DrawRectangleRec(TransformToRectangle(floor->transform), DARKGRAY); - DrawRectangleRec(TransformToRectangle(leftWall->transform), DARKGRAY); - DrawRectangleRec(TransformToRectangle(rightWall->transform), DARKGRAY); - DrawRectangleRec(TransformToRectangle(platform->transform), DARKGRAY); - DrawRectangleRec(TransformToRectangle(player->transform), RED); - DrawRectangleRec(TransformToRectangle(player2->transform), BLUE); + DrawRectangleLines(rectangle->collider.bounds.x, rectangle->collider.bounds.y, rectangle->collider.bounds.width, rectangle->collider.bounds.height, GREEN); + DrawRectangleLines(square->collider.bounds.x, square->collider.bounds.y, square->collider.bounds.width, square->collider.bounds.height, GREEN); } - // Draw all physic object information in specific screen position and font size - // DrawPhysicObjectInfo(player, (Vector2){ 10.0f, 10.0f }, 10); + // Draw help message + DrawText("Use WASD to move rectangle and ARROWS to move square", screenWidth/2 - MeasureText("Use WASD to move rectangle and ARROWS to move square", 20)/2, screenHeight*0.075f, 20, LIGHTGRAY); EndDrawing(); //---------------------------------------------------------------------------------- diff --git a/examples/physics_forces.c b/examples/physics_forces.c new file mode 100644 index 000000000..2afd14ee3 --- /dev/null +++ b/examples/physics_forces.c @@ -0,0 +1,160 @@ +/******************************************************************************************* +* +* raylib [physac] example - Forces +* +* This example has been created using raylib 1.5 (www.raylib.com) +* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* +* Copyright (c) 2016 Victor Fisac and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" +#include "math.h" + +#define FORCE_AMOUNT 5.0f +#define FORCE_RADIUS 150 +#define LINE_LENGTH 100 + +int main() +{ + // Initialization + //-------------------------------------------------------------------------------------- + int screenWidth = 800; + int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [physac] example - forces"); + InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module + + SetTargetFPS(60); + + // Global variables + Vector2 mousePosition; + bool isDebug = false; + + // Create rectangle physic objects + PhysicObject *rectangles[3]; + for (int i = 0; i < 3; i++) + { + rectangles[i] = CreatePhysicObject((Vector2){ screenWidth/4*(i+1), (((i % 2) == 0) ? (screenHeight/3) : (screenHeight/1.5f)) }, 0.0f, (Vector2){ 50, 50 }); + rectangles[i]->rigidbody.enabled = true; // Enable physic object rigidbody behaviour + rectangles[i]->rigidbody.friction = 0.1f; + } + + // Create circles physic objects + PhysicObject *circles[3]; + for (int i = 0; i < 3; i++) + { + circles[i] = CreatePhysicObject((Vector2){ screenWidth/4*(i+1), (((i % 2) == 0) ? (screenHeight/1.5f) : (screenHeight/4)) }, 0.0f, (Vector2){ 0, 0 }); + circles[i]->rigidbody.enabled = true; // Enable physic object rigidbody behaviour + circles[i]->rigidbody.friction = 0.1f; + circles[i]->collider.type = COLLIDER_CIRCLE; + circles[i]->collider.radius = 25; + } + + // Create walls physic objects + PhysicObject *leftWall = CreatePhysicObject((Vector2){ -25, screenHeight/2 }, 0.0f, (Vector2){ 50, screenHeight }); + PhysicObject *rightWall = CreatePhysicObject((Vector2){ screenWidth + 25, screenHeight/2 }, 0.0f, (Vector2){ 50, screenHeight }); + PhysicObject *topWall = CreatePhysicObject((Vector2){ screenWidth/2, -25 }, 0.0f, (Vector2){ screenWidth, 50 }); + PhysicObject *bottomWall = CreatePhysicObject((Vector2){ screenWidth/2, screenHeight + 25 }, 0.0f, (Vector2){ screenWidth, 50 }); + + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + UpdatePhysics(); // Update all created physic objects + + // Update mouse position value + mousePosition = GetMousePosition(); + + // Check force input + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) ApplyForceAtPosition(mousePosition, FORCE_AMOUNT, FORCE_RADIUS); + + // Check reset input + if (IsKeyPressed('R')) + { + // Reset rectangle physic objects positions + for (int i = 0; i < 3; i++) + { + rectangles[i]->transform.position = (Vector2){ screenWidth/4*(i+1) - rectangles[i]->transform.scale.x/2, (((i % 2) == 0) ? (screenHeight/3) : (screenHeight/1.5f)) - rectangles[i]->transform.scale.y/2 }; + rectangles[i]->rigidbody.velocity =(Vector2){ 0.0f, 0.0f }; + } + + // Reset circles physic objects positions + for (int i = 0; i < 3; i++) + { + circles[i]->transform.position = (Vector2){ screenWidth/4*(i+1), (((i % 2) == 0) ? (screenHeight/1.5f) : (screenHeight/4)) }; + circles[i]->rigidbody.velocity =(Vector2){ 0.0f, 0.0f }; + } + } + + // Check debug switch input + if (IsKeyPressed('P')) isDebug = !isDebug; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + // Draw rectangles + for (int i = 0; i < 3; i++) + { + // Convert transform values to rectangle data type variable + DrawRectangleRec(TransformToRectangle(rectangles[i]->transform), RED); + if (isDebug) DrawRectangleLines(rectangles[i]->collider.bounds.x, rectangles[i]->collider.bounds.y, rectangles[i]->collider.bounds.width, rectangles[i]->collider.bounds.height, GREEN); + + // Draw force radius + DrawCircleLines(mousePosition.x, mousePosition.y, FORCE_RADIUS, BLACK); + + // Draw direction line + if (CheckCollisionPointCircle((Vector2){ rectangles[i]->transform.position.x + rectangles[i]->transform.scale.x/2, rectangles[i]->transform.position.y + rectangles[i]->transform.scale.y/2 }, mousePosition, FORCE_RADIUS)) + { + Vector2 direction = { rectangles[i]->transform.position.x + rectangles[i]->transform.scale.x/2 - mousePosition.x, rectangles[i]->transform.position.y + rectangles[i]->transform.scale.y/2 - mousePosition.y }; + float angle = atan2l(direction.y, direction.x); + + DrawLineV((Vector2){ rectangles[i]->transform.position.x + rectangles[i]->transform.scale.x/2, rectangles[i]->transform.position.y + rectangles[i]->transform.scale.y/2 }, + (Vector2){ rectangles[i]->transform.position.x + rectangles[i]->transform.scale.x/2 + (cos(angle)*LINE_LENGTH), rectangles[i]->transform.position.y + rectangles[i]->transform.scale.y/2 + (sin(angle)*LINE_LENGTH) }, BLACK); + } + } + + // Draw circles + for (int i = 0; i < 3; i++) + { + DrawCircleV(circles[i]->transform.position, circles[i]->collider.radius, BLUE); + if (isDebug) DrawCircleLines(circles[i]->transform.position.x, circles[i]->transform.position.y, circles[i]->collider.radius, GREEN); + + // Draw force radius + DrawCircleLines(mousePosition.x, mousePosition.y, FORCE_RADIUS, BLACK); + + // Draw direction line + if (CheckCollisionPointCircle((Vector2){ circles[i]->transform.position.x, circles[i]->transform.position.y }, mousePosition, FORCE_RADIUS)) + { + Vector2 direction = { circles[i]->transform.position.x - mousePosition.x, circles[i]->transform.position.y - mousePosition.y }; + float angle = atan2l(direction.y, direction.x); + + DrawLineV((Vector2){ circles[i]->transform.position.x, circles[i]->transform.position.y }, + (Vector2){ circles[i]->transform.position.x + (cos(angle)*LINE_LENGTH), circles[i]->transform.position.y + (sin(angle)*LINE_LENGTH) }, BLACK); + } + } + + // Draw help messages + DrawText("Use LEFT MOUSE BUTTON to apply a force", screenWidth/2 - MeasureText("Use LEFT MOUSE BUTTON to apply a force", 20)/2, screenHeight*0.075f, 20, LIGHTGRAY); + DrawText("Use R to reset objects position", screenWidth/2 - MeasureText("Use R to reset objects position", 20)/2, screenHeight*0.875f, 20, GRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + ClosePhysics(); // Unitialize physics module + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} \ No newline at end of file diff --git a/src/physac.c b/src/physac.c index 13247117c..718a06bb4 100644 --- a/src/physac.c +++ b/src/physac.c @@ -36,10 +36,9 @@ // Defines and Macros //---------------------------------------------------------------------------------- #define MAX_PHYSIC_OBJECTS 256 -#define PHYSICS_GRAVITY -9.81f/2 #define PHYSICS_STEPS 450 -#define PHYSICS_ACCURACY 0.0001f // Velocity subtract operations round filter (friction) -#define PHYSICS_ERRORPERCENT 0.001f // Collision resolve position fix +#define PHYSICS_ACCURACY 0.0001f // Velocity subtract operations round filter (friction) +#define PHYSICS_ERRORPERCENT 0.001f // Collision resolve position fix //---------------------------------------------------------------------------------- // Types and Structures Definition @@ -52,53 +51,70 @@ //---------------------------------------------------------------------------------- static PhysicObject *physicObjects[MAX_PHYSIC_OBJECTS]; // Physic objects pool static int physicObjectsCount; // Counts current enabled physic objects +static Vector2 gravityForce; // Gravity force //---------------------------------------------------------------------------------- // Module specific Functions Declaration //---------------------------------------------------------------------------------- static float Vector2DotProduct(Vector2 v1, Vector2 v2); // Returns the dot product of two Vector2 +static float Vector2Length(Vector2 v); // Returns the length of a Vector2 //---------------------------------------------------------------------------------- // Module Functions Definition //---------------------------------------------------------------------------------- // Initializes pointers array (just pointers, fixed size) -void InitPhysics() +void InitPhysics(Vector2 gravity) { // Initialize physics variables physicObjectsCount = 0; + gravityForce = gravity; } // Update physic objects, calculating physic behaviours and collisions detection void UpdatePhysics() { // Reset all physic objects is grounded state - for(int i = 0; i < physicObjectsCount; i++) + for (int i = 0; i < physicObjectsCount; i++) { - if(physicObjects[i]->rigidbody.enabled) physicObjects[i]->rigidbody.isGrounded = false; + if (physicObjects[i]->rigidbody.enabled) physicObjects[i]->rigidbody.isGrounded = false; } - for(int steps = 0; steps < PHYSICS_STEPS; steps++) + for (int steps = 0; steps < PHYSICS_STEPS; steps++) { - for(int i = 0; i < physicObjectsCount; i++) + for (int i = 0; i < physicObjectsCount; i++) { - if(physicObjects[i]->enabled) + if (physicObjects[i]->enabled) { // Update physic behaviour - if(physicObjects[i]->rigidbody.enabled) + if (physicObjects[i]->rigidbody.enabled) { // Apply friction to acceleration in X axis if (physicObjects[i]->rigidbody.acceleration.x > PHYSICS_ACCURACY) physicObjects[i]->rigidbody.acceleration.x -= physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; else if (physicObjects[i]->rigidbody.acceleration.x < PHYSICS_ACCURACY) physicObjects[i]->rigidbody.acceleration.x += physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; else physicObjects[i]->rigidbody.acceleration.x = 0.0f; + // Apply friction to acceleration in Y axis + if (physicObjects[i]->rigidbody.acceleration.y > PHYSICS_ACCURACY) physicObjects[i]->rigidbody.acceleration.y -= physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; + else if (physicObjects[i]->rigidbody.acceleration.y < PHYSICS_ACCURACY) physicObjects[i]->rigidbody.acceleration.y += physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; + else physicObjects[i]->rigidbody.acceleration.y = 0.0f; + // Apply friction to velocity in X axis if (physicObjects[i]->rigidbody.velocity.x > PHYSICS_ACCURACY) physicObjects[i]->rigidbody.velocity.x -= physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; else if (physicObjects[i]->rigidbody.velocity.x < PHYSICS_ACCURACY) physicObjects[i]->rigidbody.velocity.x += physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; else physicObjects[i]->rigidbody.velocity.x = 0.0f; + // Apply friction to velocity in Y axis + if (physicObjects[i]->rigidbody.velocity.y > PHYSICS_ACCURACY) physicObjects[i]->rigidbody.velocity.y -= physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; + else if (physicObjects[i]->rigidbody.velocity.y < PHYSICS_ACCURACY) physicObjects[i]->rigidbody.velocity.y += physicObjects[i]->rigidbody.friction/PHYSICS_STEPS; + else physicObjects[i]->rigidbody.velocity.y = 0.0f; + // Apply gravity to velocity - if (physicObjects[i]->rigidbody.applyGravity) physicObjects[i]->rigidbody.velocity.y += PHYSICS_GRAVITY/PHYSICS_STEPS; + if (physicObjects[i]->rigidbody.applyGravity) + { + physicObjects[i]->rigidbody.velocity.x += gravityForce.x/PHYSICS_STEPS; + physicObjects[i]->rigidbody.velocity.y += gravityForce.y/PHYSICS_STEPS; + } // Apply acceleration to velocity physicObjects[i]->rigidbody.velocity.x += physicObjects[i]->rigidbody.acceleration.x/PHYSICS_STEPS; @@ -120,142 +136,314 @@ void UpdatePhysics() { if (physicObjects[k]->collider.enabled && i != k) { - // Check if colliders are overlapped - if (CheckCollisionRecs(physicObjects[i]->collider.bounds, physicObjects[k]->collider.bounds)) + // Resolve physic collision + // NOTE: collision resolve is generic for all directions and conditions (no axis separated cases behaviours) + // and it is separated in rigidbody attributes resolve (velocity changes by impulse) and position correction (position overlap) + + // 1. Calculate collision normal + // ------------------------------------------------------------------------------------------------------------------------------------- + + // Define collision contact normal, direction and penetration depth + Vector2 contactNormal = { 0.0f, 0.0f }; + Vector2 direction = { 0.0f, 0.0f }; + float penetrationDepth = 0.0f; + + switch(physicObjects[i]->collider.type) { - // Resolve physic collision - // NOTE: collision resolve is generic for all directions and conditions (no axis separated cases behaviours) - // and it is separated in rigidbody attributes resolve (velocity changes by impulse) and position correction (position overlap) - - // 1. Calculate collision normal - // ------------------------------------------------------------------------------------------------------------------------------------- - - // Define collision ontact normal - Vector2 contactNormal = { 0.0f, 0.0f }; + case COLLIDER_RECTANGLE: + { + switch(physicObjects[k]->collider.type) + { + case COLLIDER_RECTANGLE: + { + // Check if colliders are overlapped + if (CheckCollisionRecs(physicObjects[i]->collider.bounds, physicObjects[k]->collider.bounds)) + { + // Calculate direction vector from i to k + direction.x = (physicObjects[k]->transform.position.x + physicObjects[k]->transform.scale.x/2) - (physicObjects[i]->transform.position.x + physicObjects[i]->transform.scale.x/2); + direction.y = (physicObjects[k]->transform.position.y + physicObjects[k]->transform.scale.y/2) - (physicObjects[i]->transform.position.y + physicObjects[i]->transform.scale.y/2); + + // Define overlapping and penetration attributes + Vector2 overlap; + + // Calculate overlap on X axis + overlap.x = (physicObjects[i]->transform.scale.x + physicObjects[k]->transform.scale.x)/2 - abs(direction.x); + + // SAT test on X axis + if (overlap.x > 0.0f) + { + // Calculate overlap on Y axis + overlap.y = (physicObjects[i]->transform.scale.y + physicObjects[k]->transform.scale.y)/2 - abs(direction.y); + + // SAT test on Y axis + if (overlap.y > 0.0f) + { + // Find out which axis is axis of least penetration + if (overlap.y > overlap.x) + { + // Point towards k knowing that direction points from i to k + if (direction.x < 0.0f) contactNormal = (Vector2){ -1.0f, 0.0f }; + else contactNormal = (Vector2){ 1.0f, 0.0f }; + + // Update penetration depth for position correction + penetrationDepth = overlap.x; + } + else + { + // Point towards k knowing that direction points from i to k + if (direction.y < 0.0f) contactNormal = (Vector2){ 0.0f, 1.0f }; + else contactNormal = (Vector2){ 0.0f, -1.0f }; + + // Update penetration depth for position correction + penetrationDepth = overlap.y; + } + } + } + } + } break; + case COLLIDER_CIRCLE: + { + if (CheckCollisionCircleRec(physicObjects[k]->transform.position, physicObjects[k]->collider.radius, physicObjects[i]->collider.bounds)) + { + // Calculate direction vector between circles + direction.x = physicObjects[k]->transform.position.x - physicObjects[i]->transform.position.x + physicObjects[i]->transform.scale.x/2; + direction.y = physicObjects[k]->transform.position.y - physicObjects[i]->transform.position.y + physicObjects[i]->transform.scale.y/2; + + // Calculate closest point on rectangle to circle + Vector2 closestPoint = { 0.0f, 0.0f }; + if (direction.x > 0.0f) closestPoint.x = physicObjects[i]->collider.bounds.x + physicObjects[i]->collider.bounds.width; + else closestPoint.x = physicObjects[i]->collider.bounds.x; + + if (direction.y > 0.0f) closestPoint.y = physicObjects[i]->collider.bounds.y + physicObjects[i]->collider.bounds.height; + else closestPoint.y = physicObjects[i]->collider.bounds.y; + + // Check if the closest point is inside the circle + if (CheckCollisionPointCircle(closestPoint, physicObjects[k]->transform.position, physicObjects[k]->collider.radius)) + { + // Recalculate direction based on closest point position + direction.x = physicObjects[k]->transform.position.x - closestPoint.x; + direction.y = physicObjects[k]->transform.position.y - closestPoint.y; + float distance = Vector2Length(direction); + + // Calculate final contact normal + contactNormal.x = direction.x/distance; + contactNormal.y = -direction.y/distance; + + // Calculate penetration depth + penetrationDepth = physicObjects[k]->collider.radius - distance; + } + else + { + if (abs(direction.y) < abs(direction.x)) + { + // Calculate final contact normal + if (direction.y > 0.0f) + { + contactNormal = (Vector2){ 0.0f, -1.0f }; + penetrationDepth = fabs(physicObjects[i]->collider.bounds.y - physicObjects[k]->transform.position.y - physicObjects[k]->collider.radius); + } + else + { + contactNormal = (Vector2){ 0.0f, 1.0f }; + penetrationDepth = fabs(physicObjects[i]->collider.bounds.y - physicObjects[k]->transform.position.y + physicObjects[k]->collider.radius); + } + } + else + { + // Calculate final contact normal + if (direction.x > 0.0f) + { + contactNormal = (Vector2){ 1.0f, 0.0f }; + penetrationDepth = fabs(physicObjects[k]->transform.position.x + physicObjects[k]->collider.radius - physicObjects[i]->collider.bounds.x); + } + else + { + contactNormal = (Vector2){ -1.0f, 0.0f }; + penetrationDepth = fabs(physicObjects[i]->collider.bounds.x + physicObjects[i]->collider.bounds.width - physicObjects[k]->transform.position.x - physicObjects[k]->collider.radius); + } + } + } + } + } break; + } + } break; + case COLLIDER_CIRCLE: + { + switch(physicObjects[k]->collider.type) + { + case COLLIDER_RECTANGLE: + { + if (CheckCollisionCircleRec(physicObjects[i]->transform.position, physicObjects[i]->collider.radius, physicObjects[k]->collider.bounds)) + { + // Calculate direction vector between circles + direction.x = physicObjects[k]->transform.position.x + physicObjects[i]->transform.scale.x/2 - physicObjects[i]->transform.position.x; + direction.y = physicObjects[k]->transform.position.y + physicObjects[i]->transform.scale.y/2 - physicObjects[i]->transform.position.y; + + // Calculate closest point on rectangle to circle + Vector2 closestPoint = { 0.0f, 0.0f }; + if (direction.x > 0.0f) closestPoint.x = physicObjects[k]->collider.bounds.x + physicObjects[k]->collider.bounds.width; + else closestPoint.x = physicObjects[k]->collider.bounds.x; + + if (direction.y > 0.0f) closestPoint.y = physicObjects[k]->collider.bounds.y + physicObjects[k]->collider.bounds.height; + else closestPoint.y = physicObjects[k]->collider.bounds.y; + + // Check if the closest point is inside the circle + if (CheckCollisionPointCircle(closestPoint, physicObjects[i]->transform.position, physicObjects[i]->collider.radius)) + { + // Recalculate direction based on closest point position + direction.x = physicObjects[i]->transform.position.x - closestPoint.x; + direction.y = physicObjects[i]->transform.position.y - closestPoint.y; + float distance = Vector2Length(direction); + + // Calculate final contact normal + contactNormal.x = direction.x/distance; + contactNormal.y = -direction.y/distance; + + // Calculate penetration depth + penetrationDepth = physicObjects[k]->collider.radius - distance; + } + else + { + if (abs(direction.y) < abs(direction.x)) + { + // Calculate final contact normal + if (direction.y > 0.0f) + { + contactNormal = (Vector2){ 0.0f, -1.0f }; + penetrationDepth = fabs(physicObjects[k]->collider.bounds.y - physicObjects[i]->transform.position.y - physicObjects[i]->collider.radius); + } + else + { + contactNormal = (Vector2){ 0.0f, 1.0f }; + penetrationDepth = fabs(physicObjects[k]->collider.bounds.y - physicObjects[i]->transform.position.y + physicObjects[i]->collider.radius); + } + } + else + { + // Calculate final contact normal and penetration depth + if (direction.x > 0.0f) + { + contactNormal = (Vector2){ 1.0f, 0.0f }; + penetrationDepth = fabs(physicObjects[i]->transform.position.x + physicObjects[i]->collider.radius - physicObjects[k]->collider.bounds.x); + } + else + { + contactNormal = (Vector2){ -1.0f, 0.0f }; + penetrationDepth = fabs(physicObjects[k]->collider.bounds.x + physicObjects[k]->collider.bounds.width - physicObjects[i]->transform.position.x - physicObjects[i]->collider.radius); + } + } + } + } + } break; + case COLLIDER_CIRCLE: + { + // Check if colliders are overlapped + if (CheckCollisionCircles(physicObjects[i]->transform.position, physicObjects[i]->collider.radius, physicObjects[k]->transform.position, physicObjects[k]->collider.radius)) + { + // Calculate direction vector between circles + direction.x = physicObjects[k]->transform.position.x - physicObjects[i]->transform.position.x; + direction.y = physicObjects[k]->transform.position.y - physicObjects[i]->transform.position.y; + + // Calculate distance between circles + float distance = Vector2Length(direction); + + // Check if circles are not completely overlapped + if (distance != 0.0f) + { + // Calculate contact normal direction (Y axis needs to be flipped) + contactNormal.x = direction.x/distance; + contactNormal.y = -direction.y/distance; + } + else contactNormal = (Vector2){ 1.0f, 0.0f }; // Choose random (but consistent) values + } + } break; + default: break; + } + } break; + default: break; + } + + // Update rigidbody grounded state + if (physicObjects[i]->rigidbody.enabled) + { + if (contactNormal.y < 0.0f) physicObjects[i]->rigidbody.isGrounded = true; + } + + // 2. Calculate collision impulse + // ------------------------------------------------------------------------------------------------------------------------------------- + + // Calculate relative velocity + Vector2 relVelocity = { 0.0f, 0.0f }; + relVelocity.x = physicObjects[k]->rigidbody.velocity.x - physicObjects[i]->rigidbody.velocity.x; + relVelocity.y = physicObjects[k]->rigidbody.velocity.y - physicObjects[i]->rigidbody.velocity.y; + + // Calculate relative velocity in terms of the normal direction + float velAlongNormal = Vector2DotProduct(relVelocity, contactNormal); + + // Dot not resolve if velocities are separating + if (velAlongNormal <= 0.0f) + { + // Calculate minimum bounciness value from both objects + float e = fminf(physicObjects[i]->rigidbody.bounciness, physicObjects[k]->rigidbody.bounciness); - // Calculate direction vector from i to k - Vector2 direction; - direction.x = (physicObjects[k]->transform.position.x + physicObjects[k]->transform.scale.x/2) - (physicObjects[i]->transform.position.x + physicObjects[i]->transform.scale.x/2); - direction.y = (physicObjects[k]->transform.position.y + physicObjects[k]->transform.scale.y/2) - (physicObjects[i]->transform.position.y + physicObjects[i]->transform.scale.y/2); + // Calculate impulse scalar value + float j = -(1.0f + e)*velAlongNormal; + j /= 1.0f/physicObjects[i]->rigidbody.mass + 1.0f/physicObjects[k]->rigidbody.mass; - // Define overlapping and penetration attributes - Vector2 overlap; - float penetrationDepth = 0.0f; + // Calculate final impulse vector + Vector2 impulse = { j*contactNormal.x, j*contactNormal.y }; - // Calculate overlap on X axis - overlap.x = (physicObjects[i]->transform.scale.x + physicObjects[k]->transform.scale.x)/2 - abs(direction.x); + // Calculate collision mass ration + float massSum = physicObjects[i]->rigidbody.mass + physicObjects[k]->rigidbody.mass; + float ratio = 0.0f; - // SAT test on X axis - if (overlap.x > 0.0f) + // Apply impulse to current rigidbodies velocities if they are enabled + if (physicObjects[i]->rigidbody.enabled) { - // Calculate overlap on Y axis - overlap.y = (physicObjects[i]->transform.scale.y + physicObjects[k]->transform.scale.y)/2 - abs(direction.y); + // Calculate inverted mass ration + ratio = physicObjects[i]->rigidbody.mass/massSum; - // SAT test on Y axis - if (overlap.y > 0.0f) - { - // Find out which axis is axis of least penetration - if (overlap.y > overlap.x) - { - // Point towards k knowing that direction points from i to k - if (direction.x < 0.0f) contactNormal = (Vector2){ -1.0f, 0.0f }; - else contactNormal = (Vector2){ 1.0f, 0.0f }; - - // Update penetration depth for position correction - penetrationDepth = overlap.x; - } - else - { - // Point towards k knowing that direction points from i to k - if (direction.y < 0.0f) contactNormal = (Vector2){ 0.0f, 1.0f }; - else contactNormal = (Vector2){ 0.0f, -1.0f }; - - // Update penetration depth for position correction - penetrationDepth = overlap.y; - } - } + // Apply impulse direction to velocity + physicObjects[i]->rigidbody.velocity.x -= impulse.x*ratio*(1.0f+physicObjects[i]->rigidbody.bounciness); + physicObjects[i]->rigidbody.velocity.y -= impulse.y*ratio*(1.0f+physicObjects[i]->rigidbody.bounciness); } - // Update rigidbody grounded state - if (physicObjects[i]->rigidbody.enabled) + if (physicObjects[k]->rigidbody.enabled) { - if (contactNormal.y < 0.0f) physicObjects[i]->rigidbody.isGrounded = true; + // Calculate inverted mass ration + ratio = physicObjects[k]->rigidbody.mass/massSum; + + // Apply impulse direction to velocity + physicObjects[k]->rigidbody.velocity.x += impulse.x*ratio*(1.0f+physicObjects[i]->rigidbody.bounciness); + physicObjects[k]->rigidbody.velocity.y += impulse.y*ratio*(1.0f+physicObjects[i]->rigidbody.bounciness); } - // 2. Calculate collision impulse - // ------------------------------------------------------------------------------------------------------------------------------------- + // 3. Correct colliders overlaping (transform position) + // --------------------------------------------------------------------------------------------------------------------------------- - // Calculate relative velocity - Vector2 relVelocity = { physicObjects[k]->rigidbody.velocity.x - physicObjects[i]->rigidbody.velocity.x, physicObjects[k]->rigidbody.velocity.y - physicObjects[i]->rigidbody.velocity.y }; - - // Calculate relative velocity in terms of the normal direction - float velAlongNormal = Vector2DotProduct(relVelocity, contactNormal); - - // Dot not resolve if velocities are separating - if (velAlongNormal <= 0.0f) - { - // Calculate minimum bounciness value from both objects - float e = fminf(physicObjects[i]->rigidbody.bounciness, physicObjects[k]->rigidbody.bounciness); - - // Calculate impulse scalar value - float j = -(1.0f + e) * velAlongNormal; - j /= 1.0f/physicObjects[i]->rigidbody.mass + 1.0f/physicObjects[k]->rigidbody.mass; - - // Calculate final impulse vector - Vector2 impulse = { j*contactNormal.x, j*contactNormal.y }; - - // Calculate collision mass ration - float massSum = physicObjects[i]->rigidbody.mass + physicObjects[k]->rigidbody.mass; - float ratio = 0.0f; + // Calculate transform position penetration correction + Vector2 posCorrection; + posCorrection.x = penetrationDepth/((1.0f/physicObjects[i]->rigidbody.mass) + (1.0f/physicObjects[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.x; + posCorrection.y = penetrationDepth/((1.0f/physicObjects[i]->rigidbody.mass) + (1.0f/physicObjects[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.y; + + // Fix transform positions + if (physicObjects[i]->rigidbody.enabled) + { + // Fix physic objects transform position + physicObjects[i]->transform.position.x -= 1.0f/physicObjects[i]->rigidbody.mass*posCorrection.x; + physicObjects[i]->transform.position.y += 1.0f/physicObjects[i]->rigidbody.mass*posCorrection.y; - // Apply impulse to current rigidbodies velocities if they are enabled - if (physicObjects[i]->rigidbody.enabled) - { - // Calculate inverted mass ration - ratio = physicObjects[i]->rigidbody.mass/massSum; - - // Apply impulse direction to velocity - physicObjects[i]->rigidbody.velocity.x -= impulse.x*ratio; - physicObjects[i]->rigidbody.velocity.y -= impulse.y*ratio; - } + // Update collider bounds + physicObjects[i]->collider.bounds = TransformToRectangle(physicObjects[i]->transform); - if (physicObjects[k]->rigidbody.enabled) + if (physicObjects[k]->rigidbody.enabled) { - // Calculate inverted mass ration - ratio = physicObjects[k]->rigidbody.mass/massSum; - - // Apply impulse direction to velocity - physicObjects[k]->rigidbody.velocity.x += impulse.x*ratio; - physicObjects[k]->rigidbody.velocity.y += impulse.y*ratio; - } - - // 3. Correct colliders overlaping (transform position) - // --------------------------------------------------------------------------------------------------------------------------------- - - // Calculate transform position penetration correction - Vector2 posCorrection; - posCorrection.x = penetrationDepth/((1.0f/physicObjects[i]->rigidbody.mass) + (1.0f/physicObjects[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.x; - posCorrection.y = penetrationDepth/((1.0f/physicObjects[i]->rigidbody.mass) + (1.0f/physicObjects[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.y; - - // Fix transform positions - if (physicObjects[i]->rigidbody.enabled) - { // Fix physic objects transform position - physicObjects[i]->transform.position.x -= 1.0f/physicObjects[i]->rigidbody.mass*posCorrection.x; - physicObjects[i]->transform.position.y += 1.0f/physicObjects[i]->rigidbody.mass*posCorrection.y; + physicObjects[k]->transform.position.x += 1.0f/physicObjects[k]->rigidbody.mass*posCorrection.x; + physicObjects[k]->transform.position.y -= 1.0f/physicObjects[k]->rigidbody.mass*posCorrection.y; // Update collider bounds - physicObjects[i]->collider.bounds = TransformToRectangle(physicObjects[i]->transform); - - if (physicObjects[k]->rigidbody.enabled) - { - // Fix physic objects transform position - physicObjects[k]->transform.position.x += 1.0f/physicObjects[k]->rigidbody.mass*posCorrection.x; - physicObjects[k]->transform.position.y -= 1.0f/physicObjects[k]->rigidbody.mass*posCorrection.y; - - // Update collider bounds - physicObjects[k]->collider.bounds = TransformToRectangle(physicObjects[k]->transform); - } + physicObjects[k]->collider.bounds = TransformToRectangle(physicObjects[k]->transform); } } } @@ -298,7 +486,7 @@ PhysicObject *CreatePhysicObject(Vector2 position, float rotation, Vector2 scale obj->rigidbody.friction = 0.0f; obj->rigidbody.bounciness = 0.0f; - obj->collider.enabled = false; + obj->collider.enabled = true; obj->collider.type = COLLIDER_RECTANGLE; obj->collider.bounds = TransformToRectangle(obj->transform); obj->collider.radius = 0.0f; @@ -334,6 +522,45 @@ void DestroyPhysicObject(PhysicObject *pObj) physicObjectsCount--; } +// Apply directional force to a physic object +void ApplyForce(PhysicObject *pObj, Vector2 force) +{ + if (pObj->rigidbody.enabled) + { + pObj->rigidbody.velocity.x += force.x/pObj->rigidbody.mass; + pObj->rigidbody.velocity.y += force.y/pObj->rigidbody.mass; + } +} + +// Apply radial force to all physic objects in range +void ApplyForceAtPosition(Vector2 position, float force, float radius) +{ + for(int i = 0; i < physicObjectsCount; i++) + { + // Calculate direction and distance between force and physic object pposition + Vector2 distance = (Vector2){ physicObjects[i]->transform.position.x - position.x, physicObjects[i]->transform.position.y - position.y }; + + if(physicObjects[i]->collider.type == COLLIDER_RECTANGLE) + { + distance.x += physicObjects[i]->transform.scale.x/2; + distance.y += physicObjects[i]->transform.scale.y/2; + } + + float distanceLength = Vector2Length(distance); + + // Check if physic object is in force range + if(distanceLength <= radius) + { + // Normalize force direction + distance.x /= distanceLength; + distance.y /= -distanceLength; + + // Apply force to the physic object + ApplyForce(physicObjects[i], (Vector2){ distance.x*force, distance.y*force }); + } + } +} + // Convert Transform data type to Rectangle (position and scale) Rectangle TransformToRectangle(Transform transform) { @@ -369,3 +596,12 @@ static float Vector2DotProduct(Vector2 v1, Vector2 v2) return result; } + +static float Vector2Length(Vector2 v) +{ + float result; + + result = sqrt(v.x*v.x + v.y*v.y); + + return result; +} diff --git a/src/physac.h b/src/physac.h index fc5502c46..c70dbbe21 100644 --- a/src/physac.h +++ b/src/physac.h @@ -40,7 +40,7 @@ typedef struct Vector2 { float y; } Vector2; -typedef enum { COLLIDER_CIRCLE, COLLIDER_RECTANGLE, COLLIDER_CAPSULE } ColliderType; +typedef enum { COLLIDER_CIRCLE, COLLIDER_RECTANGLE } ColliderType; typedef struct Transform { Vector2 position; @@ -56,14 +56,14 @@ typedef struct Rigidbody { bool applyGravity; bool isGrounded; float friction; // Normalized value - float bounciness; // Normalized value + float bounciness; } Rigidbody; typedef struct Collider { bool enabled; ColliderType type; - Rectangle bounds; // Used for COLLIDER_RECTANGLE and COLLIDER_CAPSULE - int radius; // Used for COLLIDER_CIRCLE and COLLIDER_CAPSULE + Rectangle bounds; // Used for COLLIDER_RECTANGLE + int radius; // Used for COLLIDER_CIRCLE } Collider; typedef struct PhysicObject { @@ -81,13 +81,16 @@ extern "C" { // Prevents name mangling of functions //---------------------------------------------------------------------------------- // Module Functions Declaration //---------------------------------------------------------------------------------- -void InitPhysics(); // Initializes pointers array (just pointers, fixed size) +void InitPhysics(Vector2 gravity); // Initializes pointers array (just pointers, fixed size) void UpdatePhysics(); // Update physic objects, calculating physic behaviours and collisions detection void ClosePhysics(); // Unitialize all physic objects and empty the objects pool PhysicObject *CreatePhysicObject(Vector2 position, float rotation, Vector2 scale); // Create a new physic object dinamically, initialize it and add to pool void DestroyPhysicObject(PhysicObject *pObj); // Destroy a specific physic object and take it out of the list +void ApplyForce(PhysicObject *pObj, Vector2 force); // Apply directional force to a physic object +void ApplyForceAtPosition(Vector2 position, float force, float radius); // Apply radial force to all physic objects in range + Rectangle TransformToRectangle(Transform transform); // Convert Transform data type to Rectangle (position and scale) void DrawPhysicObjectInfo(PhysicObject *pObj, Vector2 position, int fontSize); // Draw physic object information at screen position diff --git a/src/raylib.h b/src/raylib.h index 527d0cb9e..ddfccea7d 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -466,7 +466,7 @@ typedef struct { // Camera system modes typedef enum { CAMERA_CUSTOM = 0, CAMERA_FREE, CAMERA_ORBITAL, CAMERA_FIRST_PERSON, CAMERA_THIRD_PERSON } CameraMode; -typedef enum { COLLIDER_CIRCLE, COLLIDER_RECTANGLE, COLLIDER_CAPSULE } ColliderType; +typedef enum { COLLIDER_CIRCLE, COLLIDER_RECTANGLE } ColliderType; typedef struct Transform { Vector2 position; @@ -482,14 +482,14 @@ typedef struct Rigidbody { bool applyGravity; bool isGrounded; float friction; // Normalized value - float bounciness; // Normalized value + float bounciness; } Rigidbody; typedef struct Collider { bool enabled; ColliderType type; - Rectangle bounds; // Used for COLLIDER_RECTANGLE and COLLIDER_CAPSULE - int radius; // Used for COLLIDER_CIRCLE and COLLIDER_CAPSULE + Rectangle bounds; // Used for COLLIDER_RECTANGLE + int radius; // Used for COLLIDER_CIRCLE } Collider; typedef struct PhysicObject { @@ -810,13 +810,16 @@ void SetBlendMode(int mode); // Set blend //---------------------------------------------------------------------------------- // Physics System Functions (Module: physac) //---------------------------------------------------------------------------------- -void InitPhysics(); // Initializes pointers array (just pointers, fixed size) +void InitPhysics(Vector2 gravity); // Initializes pointers array (just pointers, fixed size) void UpdatePhysics(); // Update physic objects, calculating physic behaviours and collisions detection void ClosePhysics(); // Unitialize all physic objects and empty the objects pool PhysicObject *CreatePhysicObject(Vector2 position, float rotation, Vector2 scale); // Create a new physic object dinamically, initialize it and add to pool void DestroyPhysicObject(PhysicObject *pObj); // Destroy a specific physic object and take it out of the list +void ApplyForce(PhysicObject *pObj, Vector2 force); // Apply directional force to a physic object +void ApplyForceAtPosition(Vector2 position, float force, float radius); // Apply radial force to all physic objects in range + Rectangle TransformToRectangle(Transform transform); // Convert Transform data type to Rectangle (position and scale) void DrawPhysicObjectInfo(PhysicObject *pObj, Vector2 position, int fontSize); // Draw physic object information at screen position