From c10c49e44f31ddac4b544ddf8c973774afd288c6 Mon Sep 17 00:00:00 2001 From: victorfisac Date: Sat, 11 Jun 2016 18:35:46 +0200 Subject: [PATCH 01/12] Convert physac module from static steps to fixed time steps Old physics update system used a static number of steps to calculate physics (450 for desktop and 64 for android). It was too much and it was limited by target frame time... Now physics update runs in a secondary thread using a fixed delta time value to update steps. Collisions are perfectly detected and resolved and performance has been improved so much. --- src/physac.h | 651 +++++++++++++++++++++++++-------------------------- 1 file changed, 321 insertions(+), 330 deletions(-) diff --git a/src/physac.h b/src/physac.h index 6a90dc29e..dd2b1628f 100644 --- a/src/physac.h +++ b/src/physac.h @@ -146,7 +146,7 @@ typedef struct PhysicBodyData { // Module Functions Declaration //---------------------------------------------------------------------------------- PHYSACDEF void InitPhysics(Vector2 gravity); // Initializes pointers array (just pointers, fixed size) -PHYSACDEF void UpdatePhysics(); // Update physic objects, calculating physic behaviours and collisions detection +PHYSACDEF void UpdatePhysics(double deltaTime); // Update physic objects, calculating physic behaviours and collisions detection PHYSACDEF void ClosePhysics(); // Unitialize all physic objects and empty the objects pool PHYSACDEF PhysicBody CreatePhysicBody(Vector2 position, float rotation, Vector2 scale); // Create a new physic body dinamically, initialize it and add to pool @@ -182,7 +182,7 @@ PHYSACDEF Rectangle TransformToRectangle(Transform transform); // Defines and Macros //---------------------------------------------------------------------------------- #define MAX_PHYSIC_BODIES 256 // Maximum available physic bodies slots in bodies pool -#define PHYSICS_STEPS 64 // Physics update steps per frame for improved collision-detection +#define PHYSICS_TIMESTEP 0.016666 // Physics fixed time step (1/fps) #define PHYSICS_ACCURACY 0.0001f // Velocity subtract operations round filter (friction) #define PHYSICS_ERRORPERCENT 0.001f // Collision resolve position fix @@ -218,376 +218,367 @@ PHYSACDEF void InitPhysics(Vector2 gravity) } // Update physic objects, calculating physic behaviours and collisions detection -PHYSACDEF void UpdatePhysics() +PHYSACDEF void UpdatePhysics(double deltaTime) { - // Reset all physic objects is grounded state - for (int i = 0; i < physicBodiesCount; i++) physicBodies[i]->rigidbody.isGrounded = false; - - for (int steps = 0; steps < PHYSICS_STEPS; steps++) + for (int i = 0; i < physicBodiesCount; i++) { - for (int i = 0; i < physicBodiesCount; i++) + if (physicBodies[i]->enabled) { - if (physicBodies[i]->enabled) + // Update physic behaviour + if (physicBodies[i]->rigidbody.enabled) { - // Update physic behaviour - if (physicBodies[i]->rigidbody.enabled) + // Apply friction to acceleration in X axis + if (physicBodies[i]->rigidbody.acceleration.x > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.x -= physicBodies[i]->rigidbody.friction*deltaTime; + else if (physicBodies[i]->rigidbody.acceleration.x < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.x += physicBodies[i]->rigidbody.friction*deltaTime; + else physicBodies[i]->rigidbody.acceleration.x = 0.0f; + + // Apply friction to acceleration in Y axis + if (physicBodies[i]->rigidbody.acceleration.y > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.y -= physicBodies[i]->rigidbody.friction*deltaTime; + else if (physicBodies[i]->rigidbody.acceleration.y < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.y += physicBodies[i]->rigidbody.friction*deltaTime; + else physicBodies[i]->rigidbody.acceleration.y = 0.0f; + + // Apply friction to velocity in X axis + if (physicBodies[i]->rigidbody.velocity.x > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.x -= physicBodies[i]->rigidbody.friction*deltaTime; + else if (physicBodies[i]->rigidbody.velocity.x < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.x += physicBodies[i]->rigidbody.friction*deltaTime; + else physicBodies[i]->rigidbody.velocity.x = 0.0f; + + // Apply friction to velocity in Y axis + if (physicBodies[i]->rigidbody.velocity.y > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.y -= physicBodies[i]->rigidbody.friction*deltaTime; + else if (physicBodies[i]->rigidbody.velocity.y < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.y += physicBodies[i]->rigidbody.friction*deltaTime; + else physicBodies[i]->rigidbody.velocity.y = 0.0f; + + // Apply gravity to velocity + if (physicBodies[i]->rigidbody.applyGravity) { - // Apply friction to acceleration in X axis - if (physicBodies[i]->rigidbody.acceleration.x > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.x -= physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else if (physicBodies[i]->rigidbody.acceleration.x < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.x += physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else physicBodies[i]->rigidbody.acceleration.x = 0.0f; - - // Apply friction to acceleration in Y axis - if (physicBodies[i]->rigidbody.acceleration.y > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.y -= physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else if (physicBodies[i]->rigidbody.acceleration.y < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.acceleration.y += physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else physicBodies[i]->rigidbody.acceleration.y = 0.0f; - - // Apply friction to velocity in X axis - if (physicBodies[i]->rigidbody.velocity.x > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.x -= physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else if (physicBodies[i]->rigidbody.velocity.x < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.x += physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else physicBodies[i]->rigidbody.velocity.x = 0.0f; - - // Apply friction to velocity in Y axis - if (physicBodies[i]->rigidbody.velocity.y > PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.y -= physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else if (physicBodies[i]->rigidbody.velocity.y < PHYSICS_ACCURACY) physicBodies[i]->rigidbody.velocity.y += physicBodies[i]->rigidbody.friction/PHYSICS_STEPS; - else physicBodies[i]->rigidbody.velocity.y = 0.0f; - - // Apply gravity to velocity - if (physicBodies[i]->rigidbody.applyGravity) - { - physicBodies[i]->rigidbody.velocity.x += gravityForce.x/PHYSICS_STEPS; - physicBodies[i]->rigidbody.velocity.y += gravityForce.y/PHYSICS_STEPS; - } - - // Apply acceleration to velocity - physicBodies[i]->rigidbody.velocity.x += physicBodies[i]->rigidbody.acceleration.x/PHYSICS_STEPS; - physicBodies[i]->rigidbody.velocity.y += physicBodies[i]->rigidbody.acceleration.y/PHYSICS_STEPS; - - // Apply velocity to position - physicBodies[i]->transform.position.x += physicBodies[i]->rigidbody.velocity.x/PHYSICS_STEPS; - physicBodies[i]->transform.position.y -= physicBodies[i]->rigidbody.velocity.y/PHYSICS_STEPS; + physicBodies[i]->rigidbody.velocity.x += gravityForce.x*deltaTime; + physicBodies[i]->rigidbody.velocity.y += gravityForce.y*deltaTime; } - // Update collision detection - if (physicBodies[i]->collider.enabled) + // Apply acceleration to velocity + physicBodies[i]->rigidbody.velocity.x += physicBodies[i]->rigidbody.acceleration.x*deltaTime; + physicBodies[i]->rigidbody.velocity.y += physicBodies[i]->rigidbody.acceleration.y*deltaTime; + + // Apply velocity to position + physicBodies[i]->transform.position.x += physicBodies[i]->rigidbody.velocity.x*deltaTime; + physicBodies[i]->transform.position.y -= physicBodies[i]->rigidbody.velocity.y*deltaTime; + } + + // Update collision detection + if (physicBodies[i]->collider.enabled) + { + // Update collider bounds + physicBodies[i]->collider.bounds = TransformToRectangle(physicBodies[i]->transform); + + // Check collision with other colliders + for (int k = 0; k < physicBodiesCount; k++) { - // Update collider bounds - physicBodies[i]->collider.bounds = TransformToRectangle(physicBodies[i]->transform); - - // Check collision with other colliders - for (int k = 0; k < physicBodiesCount; k++) + if (physicBodies[k]->collider.enabled && i != k) { - if (physicBodies[k]->collider.enabled && i != k) + // 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 (physicBodies[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 contact normal, direction and penetration depth - Vector2 contactNormal = { 0.0f, 0.0f }; - Vector2 direction = { 0.0f, 0.0f }; - float penetrationDepth = 0.0f; - - switch (physicBodies[i]->collider.type) + case COLLIDER_RECTANGLE: { - case COLLIDER_RECTANGLE: + switch (physicBodies[k]->collider.type) { - switch (physicBodies[k]->collider.type) + case COLLIDER_RECTANGLE: { - case COLLIDER_RECTANGLE: + // Check if colliders are overlapped + if (CheckCollisionRecs(physicBodies[i]->collider.bounds, physicBodies[k]->collider.bounds)) { - // Check if colliders are overlapped - if (CheckCollisionRecs(physicBodies[i]->collider.bounds, physicBodies[k]->collider.bounds)) - { - // Calculate direction vector from i to k - direction.x = (physicBodies[k]->transform.position.x + physicBodies[k]->transform.scale.x/2) - (physicBodies[i]->transform.position.x + physicBodies[i]->transform.scale.x/2); - direction.y = (physicBodies[k]->transform.position.y + physicBodies[k]->transform.scale.y/2) - (physicBodies[i]->transform.position.y + physicBodies[i]->transform.scale.y/2); - - // Define overlapping and penetration attributes - Vector2 overlap; + // Calculate direction vector from i to k + direction.x = (physicBodies[k]->transform.position.x + physicBodies[k]->transform.scale.x/2) - (physicBodies[i]->transform.position.x + physicBodies[i]->transform.scale.x/2); + direction.y = (physicBodies[k]->transform.position.y + physicBodies[k]->transform.scale.y/2) - (physicBodies[i]->transform.position.y + physicBodies[i]->transform.scale.y/2); + + // Define overlapping and penetration attributes + Vector2 overlap; - // Calculate overlap on X axis - overlap.x = (physicBodies[i]->transform.scale.x + physicBodies[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 = (physicBodies[i]->transform.scale.y + physicBodies[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(physicBodies[k]->transform.position, physicBodies[k]->collider.radius, physicBodies[i]->collider.bounds)) + // Calculate overlap on X axis + overlap.x = (physicBodies[i]->transform.scale.x + physicBodies[k]->transform.scale.x)/2 - abs(direction.x); + + // SAT test on X axis + if (overlap.x > 0.0f) { - // Calculate direction vector between circles - direction.x = physicBodies[k]->transform.position.x - physicBodies[i]->transform.position.x + physicBodies[i]->transform.scale.x/2; - direction.y = physicBodies[k]->transform.position.y - physicBodies[i]->transform.position.y + physicBodies[i]->transform.scale.y/2; + // Calculate overlap on Y axis + overlap.y = (physicBodies[i]->transform.scale.y + physicBodies[k]->transform.scale.y)/2 - abs(direction.y); - // Calculate closest point on rectangle to circle - Vector2 closestPoint = { 0.0f, 0.0f }; - if (direction.x > 0.0f) closestPoint.x = physicBodies[i]->collider.bounds.x + physicBodies[i]->collider.bounds.width; - else closestPoint.x = physicBodies[i]->collider.bounds.x; - - if (direction.y > 0.0f) closestPoint.y = physicBodies[i]->collider.bounds.y + physicBodies[i]->collider.bounds.height; - else closestPoint.y = physicBodies[i]->collider.bounds.y; - - // Check if the closest point is inside the circle - if (CheckCollisionPointCircle(closestPoint, physicBodies[k]->transform.position, physicBodies[k]->collider.radius)) + // SAT test on Y axis + if (overlap.y > 0.0f) { - // Recalculate direction based on closest point position - direction.x = physicBodies[k]->transform.position.x - closestPoint.x; - direction.y = physicBodies[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 = physicBodies[k]->collider.radius - distance; - } - else - { - if (abs(direction.y) < abs(direction.x)) + // Find out which axis is axis of least penetration + if (overlap.y > overlap.x) { - // Calculate final contact normal - if (direction.y > 0.0f) - { - contactNormal = (Vector2){ 0.0f, -1.0f }; - penetrationDepth = fabs(physicBodies[i]->collider.bounds.y - physicBodies[k]->transform.position.y - physicBodies[k]->collider.radius); - } - else - { - contactNormal = (Vector2){ 0.0f, 1.0f }; - penetrationDepth = fabs(physicBodies[i]->collider.bounds.y - physicBodies[k]->transform.position.y + physicBodies[k]->collider.radius); - } + // 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 { - // Calculate final contact normal - if (direction.x > 0.0f) - { - contactNormal = (Vector2){ 1.0f, 0.0f }; - penetrationDepth = fabs(physicBodies[k]->transform.position.x + physicBodies[k]->collider.radius - physicBodies[i]->collider.bounds.x); - } - else - { - contactNormal = (Vector2){ -1.0f, 0.0f }; - penetrationDepth = fabs(physicBodies[i]->collider.bounds.x + physicBodies[i]->collider.bounds.width - physicBodies[k]->transform.position.x - physicBodies[k]->collider.radius); - } + // 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; - } - } break; - case COLLIDER_CIRCLE: - { - switch (physicBodies[k]->collider.type) + } + } break; + case COLLIDER_CIRCLE: { - case COLLIDER_RECTANGLE: + if (CheckCollisionCircleRec(physicBodies[k]->transform.position, physicBodies[k]->collider.radius, physicBodies[i]->collider.bounds)) { - if (CheckCollisionCircleRec(physicBodies[i]->transform.position, physicBodies[i]->collider.radius, physicBodies[k]->collider.bounds)) + // Calculate direction vector between circles + direction.x = physicBodies[k]->transform.position.x - physicBodies[i]->transform.position.x + physicBodies[i]->transform.scale.x/2; + direction.y = physicBodies[k]->transform.position.y - physicBodies[i]->transform.position.y + physicBodies[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 = physicBodies[i]->collider.bounds.x + physicBodies[i]->collider.bounds.width; + else closestPoint.x = physicBodies[i]->collider.bounds.x; + + if (direction.y > 0.0f) closestPoint.y = physicBodies[i]->collider.bounds.y + physicBodies[i]->collider.bounds.height; + else closestPoint.y = physicBodies[i]->collider.bounds.y; + + // Check if the closest point is inside the circle + if (CheckCollisionPointCircle(closestPoint, physicBodies[k]->transform.position, physicBodies[k]->collider.radius)) { - // Calculate direction vector between circles - direction.x = physicBodies[k]->transform.position.x + physicBodies[i]->transform.scale.x/2 - physicBodies[i]->transform.position.x; - direction.y = physicBodies[k]->transform.position.y + physicBodies[i]->transform.scale.y/2 - physicBodies[i]->transform.position.y; - - // Calculate closest point on rectangle to circle - Vector2 closestPoint = { 0.0f, 0.0f }; - if (direction.x > 0.0f) closestPoint.x = physicBodies[k]->collider.bounds.x + physicBodies[k]->collider.bounds.width; - else closestPoint.x = physicBodies[k]->collider.bounds.x; - - if (direction.y > 0.0f) closestPoint.y = physicBodies[k]->collider.bounds.y + physicBodies[k]->collider.bounds.height; - else closestPoint.y = physicBodies[k]->collider.bounds.y; - - // Check if the closest point is inside the circle - if (CheckCollisionPointCircle(closestPoint, physicBodies[i]->transform.position, physicBodies[i]->collider.radius)) - { - // Recalculate direction based on closest point position - direction.x = physicBodies[i]->transform.position.x - closestPoint.x; - direction.y = physicBodies[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 = physicBodies[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(physicBodies[k]->collider.bounds.y - physicBodies[i]->transform.position.y - physicBodies[i]->collider.radius); - } - else - { - contactNormal = (Vector2){ 0.0f, 1.0f }; - penetrationDepth = fabs(physicBodies[k]->collider.bounds.y - physicBodies[i]->transform.position.y + physicBodies[i]->collider.radius); - } - } - else - { - // Calculate final contact normal and penetration depth - if (direction.x > 0.0f) - { - contactNormal = (Vector2){ 1.0f, 0.0f }; - penetrationDepth = fabs(physicBodies[i]->transform.position.x + physicBodies[i]->collider.radius - physicBodies[k]->collider.bounds.x); - } - else - { - contactNormal = (Vector2){ -1.0f, 0.0f }; - penetrationDepth = fabs(physicBodies[k]->collider.bounds.x + physicBodies[k]->collider.bounds.width - physicBodies[i]->transform.position.x - physicBodies[i]->collider.radius); - } - } - } - } - } break; - case COLLIDER_CIRCLE: - { - // Check if colliders are overlapped - if (CheckCollisionCircles(physicBodies[i]->transform.position, physicBodies[i]->collider.radius, physicBodies[k]->transform.position, physicBodies[k]->collider.radius)) - { - // Calculate direction vector between circles - direction.x = physicBodies[k]->transform.position.x - physicBodies[i]->transform.position.x; - direction.y = physicBodies[k]->transform.position.y - physicBodies[i]->transform.position.y; - - // Calculate distance between circles + // Recalculate direction based on closest point position + direction.x = physicBodies[k]->transform.position.x - closestPoint.x; + direction.y = physicBodies[k]->transform.position.y - closestPoint.y; 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 + // Calculate final contact normal + contactNormal.x = direction.x/distance; + contactNormal.y = -direction.y/distance; + + // Calculate penetration depth + penetrationDepth = physicBodies[k]->collider.radius - distance; } - } break; - default: break; - } - } break; - default: break; - } - - // Update rigidbody grounded state - if (physicBodies[i]->rigidbody.enabled) + 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(physicBodies[i]->collider.bounds.y - physicBodies[k]->transform.position.y - physicBodies[k]->collider.radius); + } + else + { + contactNormal = (Vector2){ 0.0f, 1.0f }; + penetrationDepth = fabs(physicBodies[i]->collider.bounds.y - physicBodies[k]->transform.position.y + physicBodies[k]->collider.radius); + } + } + else + { + // Calculate final contact normal + if (direction.x > 0.0f) + { + contactNormal = (Vector2){ 1.0f, 0.0f }; + penetrationDepth = fabs(physicBodies[k]->transform.position.x + physicBodies[k]->collider.radius - physicBodies[i]->collider.bounds.x); + } + else + { + contactNormal = (Vector2){ -1.0f, 0.0f }; + penetrationDepth = fabs(physicBodies[i]->collider.bounds.x + physicBodies[i]->collider.bounds.width - physicBodies[k]->transform.position.x - physicBodies[k]->collider.radius); + } + } + } + } + } break; + } + } break; + case COLLIDER_CIRCLE: { - if (contactNormal.y < 0.0f) physicBodies[i]->rigidbody.isGrounded = true; - } - - // 2. Calculate collision impulse - // ------------------------------------------------------------------------------------------------------------------------------------- - - // Calculate relative velocity - Vector2 relVelocity = { 0.0f, 0.0f }; - relVelocity.x = physicBodies[k]->rigidbody.velocity.x - physicBodies[i]->rigidbody.velocity.x; - relVelocity.y = physicBodies[k]->rigidbody.velocity.y - physicBodies[i]->rigidbody.velocity.y; - - // Calculate relative velocity in terms of the normal direction - float velAlongNormal = Vector2DotProduct(relVelocity, contactNormal); + switch (physicBodies[k]->collider.type) + { + case COLLIDER_RECTANGLE: + { + if (CheckCollisionCircleRec(physicBodies[i]->transform.position, physicBodies[i]->collider.radius, physicBodies[k]->collider.bounds)) + { + // Calculate direction vector between circles + direction.x = physicBodies[k]->transform.position.x + physicBodies[i]->transform.scale.x/2 - physicBodies[i]->transform.position.x; + direction.y = physicBodies[k]->transform.position.y + physicBodies[i]->transform.scale.y/2 - physicBodies[i]->transform.position.y; + + // Calculate closest point on rectangle to circle + Vector2 closestPoint = { 0.0f, 0.0f }; + if (direction.x > 0.0f) closestPoint.x = physicBodies[k]->collider.bounds.x + physicBodies[k]->collider.bounds.width; + else closestPoint.x = physicBodies[k]->collider.bounds.x; + + if (direction.y > 0.0f) closestPoint.y = physicBodies[k]->collider.bounds.y + physicBodies[k]->collider.bounds.height; + else closestPoint.y = physicBodies[k]->collider.bounds.y; + + // Check if the closest point is inside the circle + if (CheckCollisionPointCircle(closestPoint, physicBodies[i]->transform.position, physicBodies[i]->collider.radius)) + { + // Recalculate direction based on closest point position + direction.x = physicBodies[i]->transform.position.x - closestPoint.x; + direction.y = physicBodies[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 = physicBodies[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(physicBodies[k]->collider.bounds.y - physicBodies[i]->transform.position.y - physicBodies[i]->collider.radius); + } + else + { + contactNormal = (Vector2){ 0.0f, 1.0f }; + penetrationDepth = fabs(physicBodies[k]->collider.bounds.y - physicBodies[i]->transform.position.y + physicBodies[i]->collider.radius); + } + } + else + { + // Calculate final contact normal and penetration depth + if (direction.x > 0.0f) + { + contactNormal = (Vector2){ 1.0f, 0.0f }; + penetrationDepth = fabs(physicBodies[i]->transform.position.x + physicBodies[i]->collider.radius - physicBodies[k]->collider.bounds.x); + } + else + { + contactNormal = (Vector2){ -1.0f, 0.0f }; + penetrationDepth = fabs(physicBodies[k]->collider.bounds.x + physicBodies[k]->collider.bounds.width - physicBodies[i]->transform.position.x - physicBodies[i]->collider.radius); + } + } + } + } + } break; + case COLLIDER_CIRCLE: + { + // Check if colliders are overlapped + if (CheckCollisionCircles(physicBodies[i]->transform.position, physicBodies[i]->collider.radius, physicBodies[k]->transform.position, physicBodies[k]->collider.radius)) + { + // Calculate direction vector between circles + direction.x = physicBodies[k]->transform.position.x - physicBodies[i]->transform.position.x; + direction.y = physicBodies[k]->transform.position.y - physicBodies[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; + } - // Dot not resolve if velocities are separating - if (velAlongNormal <= 0.0f) + // Update rigidbody grounded state + if (physicBodies[i]->rigidbody.enabled) physicBodies[i]->rigidbody.isGrounded = (contactNormal.y < 0.0f); + + // 2. Calculate collision impulse + // ------------------------------------------------------------------------------------------------------------------------------------- + + // Calculate relative velocity + Vector2 relVelocity = { 0.0f, 0.0f }; + relVelocity.x = physicBodies[k]->rigidbody.velocity.x - physicBodies[i]->rigidbody.velocity.x; + relVelocity.y = physicBodies[k]->rigidbody.velocity.y - physicBodies[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(physicBodies[i]->rigidbody.bounciness, physicBodies[k]->rigidbody.bounciness); + + // Calculate impulse scalar value + float j = -(1.0f + e)*velAlongNormal; + j /= 1.0f/physicBodies[i]->rigidbody.mass + 1.0f/physicBodies[k]->rigidbody.mass; + + // Calculate final impulse vector + Vector2 impulse = { j*contactNormal.x, j*contactNormal.y }; + + // Calculate collision mass ration + float massSum = physicBodies[i]->rigidbody.mass + physicBodies[k]->rigidbody.mass; + float ratio = 0.0f; + + // Apply impulse to current rigidbodies velocities if they are enabled + if (physicBodies[i]->rigidbody.enabled) { - // Calculate minimum bounciness value from both objects - float e = fminf(physicBodies[i]->rigidbody.bounciness, physicBodies[k]->rigidbody.bounciness); + // Calculate inverted mass ration + ratio = physicBodies[i]->rigidbody.mass/massSum; - // Calculate impulse scalar value - float j = -(1.0f + e)*velAlongNormal; - j /= 1.0f/physicBodies[i]->rigidbody.mass + 1.0f/physicBodies[k]->rigidbody.mass; + // Apply impulse direction to velocity + physicBodies[i]->rigidbody.velocity.x -= impulse.x*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); + physicBodies[i]->rigidbody.velocity.y -= impulse.y*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); + } + + if (physicBodies[k]->rigidbody.enabled) + { + // Calculate inverted mass ration + ratio = physicBodies[k]->rigidbody.mass/massSum; - // Calculate final impulse vector - Vector2 impulse = { j*contactNormal.x, j*contactNormal.y }; + // Apply impulse direction to velocity + physicBodies[k]->rigidbody.velocity.x += impulse.x*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); + physicBodies[k]->rigidbody.velocity.y += impulse.y*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); + } + + // 3. Correct colliders overlaping (transform position) + // --------------------------------------------------------------------------------------------------------------------------------- + + // Calculate transform position penetration correction + Vector2 posCorrection; + posCorrection.x = penetrationDepth/((1.0f/physicBodies[i]->rigidbody.mass) + (1.0f/physicBodies[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.x; + posCorrection.y = penetrationDepth/((1.0f/physicBodies[i]->rigidbody.mass) + (1.0f/physicBodies[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.y; + + // Fix transform positions + if (physicBodies[i]->rigidbody.enabled) + { + // Fix physic objects transform position + physicBodies[i]->transform.position.x -= 1.0f/physicBodies[i]->rigidbody.mass*posCorrection.x; + physicBodies[i]->transform.position.y += 1.0f/physicBodies[i]->rigidbody.mass*posCorrection.y; - // Calculate collision mass ration - float massSum = physicBodies[i]->rigidbody.mass + physicBodies[k]->rigidbody.mass; - float ratio = 0.0f; + // Update collider bounds + physicBodies[i]->collider.bounds = TransformToRectangle(physicBodies[i]->transform); - // Apply impulse to current rigidbodies velocities if they are enabled - if (physicBodies[i]->rigidbody.enabled) + if (physicBodies[k]->rigidbody.enabled) { - // Calculate inverted mass ration - ratio = physicBodies[i]->rigidbody.mass/massSum; - - // Apply impulse direction to velocity - physicBodies[i]->rigidbody.velocity.x -= impulse.x*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); - physicBodies[i]->rigidbody.velocity.y -= impulse.y*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); - } - - if (physicBodies[k]->rigidbody.enabled) - { - // Calculate inverted mass ration - ratio = physicBodies[k]->rigidbody.mass/massSum; - - // Apply impulse direction to velocity - physicBodies[k]->rigidbody.velocity.x += impulse.x*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); - physicBodies[k]->rigidbody.velocity.y += impulse.y*ratio*(1.0f+physicBodies[i]->rigidbody.bounciness); - } - - // 3. Correct colliders overlaping (transform position) - // --------------------------------------------------------------------------------------------------------------------------------- - - // Calculate transform position penetration correction - Vector2 posCorrection; - posCorrection.x = penetrationDepth/((1.0f/physicBodies[i]->rigidbody.mass) + (1.0f/physicBodies[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.x; - posCorrection.y = penetrationDepth/((1.0f/physicBodies[i]->rigidbody.mass) + (1.0f/physicBodies[k]->rigidbody.mass))*PHYSICS_ERRORPERCENT*contactNormal.y; - - // Fix transform positions - if (physicBodies[i]->rigidbody.enabled) - { // Fix physic objects transform position - physicBodies[i]->transform.position.x -= 1.0f/physicBodies[i]->rigidbody.mass*posCorrection.x; - physicBodies[i]->transform.position.y += 1.0f/physicBodies[i]->rigidbody.mass*posCorrection.y; + physicBodies[k]->transform.position.x += 1.0f/physicBodies[k]->rigidbody.mass*posCorrection.x; + physicBodies[k]->transform.position.y -= 1.0f/physicBodies[k]->rigidbody.mass*posCorrection.y; // Update collider bounds - physicBodies[i]->collider.bounds = TransformToRectangle(physicBodies[i]->transform); - - if (physicBodies[k]->rigidbody.enabled) - { - // Fix physic objects transform position - physicBodies[k]->transform.position.x += 1.0f/physicBodies[k]->rigidbody.mass*posCorrection.x; - physicBodies[k]->transform.position.y -= 1.0f/physicBodies[k]->rigidbody.mass*posCorrection.y; - - // Update collider bounds - physicBodies[k]->collider.bounds = TransformToRectangle(physicBodies[k]->transform); - } + physicBodies[k]->collider.bounds = TransformToRectangle(physicBodies[k]->transform); } } } From 4c43a407888d516b38191b5df76e373dae6ec58e Mon Sep 17 00:00:00 2001 From: victorfisac Date: Sat, 11 Jun 2016 19:11:30 +0200 Subject: [PATCH 02/12] Update physac examples with fixed timestep method --- examples/physics_basic_rigidbody.c | 32 ++++++++++++++++++++++++++++-- examples/physics_forces.c | 30 +++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/examples/physics_basic_rigidbody.c b/examples/physics_basic_rigidbody.c index 8870c55b3..24570426f 100644 --- a/examples/physics_basic_rigidbody.c +++ b/examples/physics_basic_rigidbody.c @@ -13,10 +13,13 @@ #define PHYSAC_IMPLEMENTATION #include "physac.h" +#include #define MOVE_VELOCITY 5 #define JUMP_VELOCITY 30 +void* PhysicsThread(void *arg); + int main() { // Initialization @@ -53,6 +56,10 @@ int main() // Create pplatform physic object PhysicBody platform = CreatePhysicBody((Vector2){ screenWidth/2, screenHeight*0.7f }, 0.0f, (Vector2){ screenWidth*0.25f, 20 }); + // Create physics thread + pthread_t tid; + pthread_create(&tid, NULL, &PhysicsThread, NULL); + SetTargetFPS(60); //-------------------------------------------------------------------------------------- @@ -61,10 +68,9 @@ int main() { // Update //---------------------------------------------------------------------------------- - UpdatePhysics(); // Update all created physic objects // Check rectangle movement inputs - if (IsKeyDown('W') && rectangle->rigidbody.isGrounded) rectangle->rigidbody.velocity.y = JUMP_VELOCITY; + if (IsKeyPressed('W')) 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; @@ -117,10 +123,32 @@ int main() // De-Initialization //-------------------------------------------------------------------------------------- + pthread_cancel(tid); // Destroy physics thread + ClosePhysics(); // Unitialize physics (including all loaded objects) CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; +} + +void* PhysicsThread(void *arg) +{ + // Initialize time variables + double currentTime = GetTime(); + double previousTime = currentTime; + + // Physics update loop + while (!WindowShouldClose()) + { + currentTime = GetTime(); + double deltaTime = (double)(currentTime - previousTime); + previousTime = currentTime; + + // Delta time value needs to be inverse multiplied by physics time step value (1/target fps) + UpdatePhysics(deltaTime/PHYSICS_TIMESTEP); + } + + return NULL; } \ No newline at end of file diff --git a/examples/physics_forces.c b/examples/physics_forces.c index 3e90a21db..397c23318 100644 --- a/examples/physics_forces.c +++ b/examples/physics_forces.c @@ -13,12 +13,15 @@ #define PHYSAC_IMPLEMENTATION #include "physac.h" +#include #define FORCE_AMOUNT 5.0f #define FORCE_RADIUS 150 #define LINE_LENGTH 75 #define TRIANGLE_LENGTH 12 +void* PhysicsThread(void *arg); + int main() { // Initialization @@ -61,6 +64,10 @@ int main() PhysicBody topWall = CreatePhysicBody((Vector2){ screenWidth/2, -25 }, 0.0f, (Vector2){ screenWidth, 50 }); PhysicBody bottomWall = CreatePhysicBody((Vector2){ screenWidth/2, screenHeight + 25 }, 0.0f, (Vector2){ screenWidth, 50 }); + // Create physics thread + pthread_t tid; + pthread_create(&tid, NULL, &PhysicsThread, NULL); + SetTargetFPS(60); //-------------------------------------------------------------------------------------- @@ -69,7 +76,6 @@ int main() { // Update //---------------------------------------------------------------------------------- - UpdatePhysics(); // Update all created physic objects // Update mouse position value mousePosition = GetMousePosition(); @@ -174,10 +180,32 @@ int main() // De-Initialization //-------------------------------------------------------------------------------------- + pthread_cancel(tid); // Destroy physics thread + ClosePhysics(); // Unitialize physics module CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; +} + +void* PhysicsThread(void *arg) +{ + // Initialize time variables + double currentTime = GetTime(); + double previousTime = currentTime; + + // Physics update loop + while (!WindowShouldClose()) + { + currentTime = GetTime(); + double deltaTime = (double)(currentTime - previousTime); + previousTime = currentTime; + + // Delta time value needs to be inverse multiplied by physics time step value (1/target fps) + UpdatePhysics(deltaTime/PHYSICS_TIMESTEP); + } + + return NULL; } \ No newline at end of file From 7999bbafa8c5333b69edb7881f64986f3e3e3d45 Mon Sep 17 00:00:00 2001 From: victorfisac Date: Sat, 11 Jun 2016 19:14:25 +0200 Subject: [PATCH 03/12] Make GetTime() public to be used externally --- src/core.c | 3 +-- src/raylib.h | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core.c b/src/core.c index 0c1e0454c..00b2e82fe 100644 --- a/src/core.c +++ b/src/core.c @@ -290,7 +290,6 @@ static void InitDisplay(int width, int height); // Initialize display de static void InitGraphics(void); // Initialize OpenGL graphics static void SetupFramebufferSize(int displayWidth, int displayHeight); static void InitTimer(void); // Initialize timer -static double GetTime(void); // Returns time since InitTimer() was run static bool GetKeyStatus(int key); // Returns if a key has been pressed static bool GetMouseButtonStatus(int button); // Returns if a mouse button has been pressed static void PollInputEvents(void); // Register user events @@ -2039,7 +2038,7 @@ static void InitTimer(void) } // Get current time measure (in seconds) since InitTimer() -static double GetTime(void) +double GetTime(void) { #if defined(PLATFORM_DESKTOP) || defined(PLATFORM_WEB) return glfwGetTime(); diff --git a/src/raylib.h b/src/raylib.h index bfcb9bf56..bfed533bf 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -582,6 +582,7 @@ Matrix GetCameraMatrix(Camera camera); // Returns camera tr void SetTargetFPS(int fps); // Set target FPS (maximum) float GetFPS(void); // Returns current FPS float GetFrameTime(void); // Returns time in seconds for one frame +double GetTime(void); // Returns time since InitTimer() was run internally Color GetColor(int hexValue); // Returns a Color struct from hexadecimal value int GetHexValue(Color color); // Returns hexadecimal value for a Color From 16609d6702f60ae714741888837a80756628400c Mon Sep 17 00:00:00 2001 From: victorfisac Date: Sun, 12 Jun 2016 22:04:51 +0200 Subject: [PATCH 04/12] Revert "Make GetTime() public to be used externally" This reverts commit 7999bbafa8c5333b69edb7881f64986f3e3e3d45. --- src/core.c | 3 ++- src/raylib.h | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core.c b/src/core.c index 899f01624..122453e35 100644 --- a/src/core.c +++ b/src/core.c @@ -290,6 +290,7 @@ static void InitDisplay(int width, int height); // Initialize display de static void InitGraphics(void); // Initialize OpenGL graphics static void SetupFramebufferSize(int displayWidth, int displayHeight); static void InitTimer(void); // Initialize timer +static double GetTime(void); // Returns time since InitTimer() was run static bool GetKeyStatus(int key); // Returns if a key has been pressed static bool GetMouseButtonStatus(int button); // Returns if a mouse button has been pressed static void PollInputEvents(void); // Register user events @@ -2030,7 +2031,7 @@ static void InitTimer(void) } // Get current time measure (in seconds) since InitTimer() -double GetTime(void) +static double GetTime(void) { #if defined(PLATFORM_DESKTOP) || defined(PLATFORM_WEB) return glfwGetTime(); diff --git a/src/raylib.h b/src/raylib.h index bfed533bf..bfcb9bf56 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -582,7 +582,6 @@ Matrix GetCameraMatrix(Camera camera); // Returns camera tr void SetTargetFPS(int fps); // Set target FPS (maximum) float GetFPS(void); // Returns current FPS float GetFrameTime(void); // Returns time in seconds for one frame -double GetTime(void); // Returns time since InitTimer() was run internally Color GetColor(int hexValue); // Returns a Color struct from hexadecimal value int GetHexValue(Color color); // Returns hexadecimal value for a Color From 5625c11e9982838498722c33d832289f3e79fa6e Mon Sep 17 00:00:00 2001 From: victorfisac Date: Sun, 12 Jun 2016 22:07:06 +0200 Subject: [PATCH 05/12] Added internal hi-resolution timer to physac... ... and now physac thread creation is done in InitPhysics() and it is destroyed in ClosePhysics(). User just need to call these functions to use physac module. --- src/physac.h | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/physac.h b/src/physac.h index dd2b1628f..ea8801c3a 100644 --- a/src/physac.h +++ b/src/physac.h @@ -177,6 +177,18 @@ PHYSACDEF Rectangle TransformToRectangle(Transform transform); #endif #include // Required for: cos(), sin(), abs(), fminf() +#include // Required for typedef unsigned long long int uint64_t, used by hi-res timer +#include // Required for: pthread_create() +#include "utils.h" // Required for: TraceLog() + +#if defined(PLATFORM_DESKTOP) + // Functions required to query time on Windows + int __stdcall QueryPerformanceCounter(unsigned long long int *lpPerformanceCount); + int __stdcall QueryPerformanceFrequency(unsigned long long int *lpFrequency); +#elif defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) + #include // Required for: timespec + #include // Required for: clock_gettime() +#endif //---------------------------------------------------------------------------------- // Defines and Macros @@ -195,6 +207,9 @@ PHYSACDEF Rectangle TransformToRectangle(Transform transform); //---------------------------------------------------------------------------------- // Global Variables Definition //---------------------------------------------------------------------------------- +static bool physicsThreadEnabled = false; // Physics calculations thread exit control +static uint64_t baseTime; // Base time measure for hi-res timer +static double currentTime, previousTime; // Used to track timmings static PhysicBody physicBodies[MAX_PHYSIC_BODIES]; // Physic bodies pool static int physicBodiesCount; // Counts current enabled physic bodies static Vector2 gravityForce; // Gravity force @@ -202,6 +217,9 @@ static Vector2 gravityForce; // Gravity f //---------------------------------------------------------------------------------- // Module specific Functions Declaration //---------------------------------------------------------------------------------- +static void* PhysicsThread(void *arg); // Physics calculations thread function +static void InitTimer(void); // Initialize hi-resolution timer +static double GetCurrentTime(void); // Time measure returned are microseconds 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 @@ -215,6 +233,10 @@ PHYSACDEF void InitPhysics(Vector2 gravity) // Initialize physics variables physicBodiesCount = 0; gravityForce = gravity; + + // Create physics thread + pthread_t tid; + pthread_create(&tid, NULL, &PhysicsThread, NULL); } // Update physic objects, calculating physic behaviours and collisions detection @@ -592,6 +614,9 @@ PHYSACDEF void UpdatePhysics(double deltaTime) // Unitialize all physic objects and empty the objects pool PHYSACDEF void ClosePhysics() { + // Exit physics thread loop + physicsThreadEnabled = false; + // Free all dynamic memory allocations for (int i = 0; i < physicBodiesCount; i++) PHYSAC_FREE(physicBodies[i]); @@ -710,6 +735,65 @@ PHYSACDEF Rectangle TransformToRectangle(Transform transform) //---------------------------------------------------------------------------------- // Module specific Functions Definition //---------------------------------------------------------------------------------- +// Physics calculations thread function +static void* PhysicsThread(void *arg) +{ + // Initialize thread loop state + physicsThreadEnabled = true; + + // Initialize hi-resolution timer + InitTimer(); + + // Physics update loop + while (physicsThreadEnabled) + { + currentTime = GetCurrentTime(); + double deltaTime = (double)(currentTime - previousTime); + previousTime = currentTime; + + // Delta time value needs to be inverse multiplied by physics time step value (1/target fps) + UpdatePhysics(deltaTime/PHYSICS_TIMESTEP); + } + + return NULL; +} + +// Initialize hi-resolution timer +static void InitTimer(void) +{ +#if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) + struct timespec now; + + if (clock_gettime(CLOCK_MONOTONIC, &now) == 0) // Success + { + baseTime = (uint64_t)now.tv_sec*1000000000LLU + (uint64_t)now.tv_nsec; + } + else TraceLog(WARNING, "No hi-resolution timer available"); +#endif + + previousTime = GetCurrentTime(); // Get time as double +} + +// Time measure returned are microseconds +static double GetCurrentTime(void) +{ +#if defined(PLATFORM_DESKTOP) + unsigned long long int clockFrequency, currentTime; + + QueryPerformanceFrequency(&clockFrequency); + QueryPerformanceCounter(¤tTime); + + return (double)(currentTime/clockFrequency); +#endif + +#if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint64_t time = (uint64_t)ts.tv_sec*1000000000LLU + (uint64_t)ts.tv_nsec; + + return (double)(time - baseTime)*1e-9; +#endif +} // Returns the dot product of two Vector2 static float Vector2DotProduct(Vector2 v1, Vector2 v2) From 6a2bbae5216e66e5581d697998efe135ad826c50 Mon Sep 17 00:00:00 2001 From: victorfisac Date: Sun, 12 Jun 2016 22:07:36 +0200 Subject: [PATCH 06/12] Updated physics examples with new module changes --- examples/physics_basic_rigidbody.c | 32 +---------------------------- examples/physics_forces.c | 33 +----------------------------- 2 files changed, 2 insertions(+), 63 deletions(-) diff --git a/examples/physics_basic_rigidbody.c b/examples/physics_basic_rigidbody.c index 24570426f..811ab9828 100644 --- a/examples/physics_basic_rigidbody.c +++ b/examples/physics_basic_rigidbody.c @@ -13,12 +13,10 @@ #define PHYSAC_IMPLEMENTATION #include "physac.h" -#include #define MOVE_VELOCITY 5 #define JUMP_VELOCITY 30 -void* PhysicsThread(void *arg); int main() { @@ -28,7 +26,6 @@ int main() int screenHeight = 450; InitWindow(screenWidth, screenHeight, "raylib [physac] example - basic rigidbody"); - InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module // Debug variables @@ -56,10 +53,6 @@ int main() // Create pplatform physic object PhysicBody platform = CreatePhysicBody((Vector2){ screenWidth/2, screenHeight*0.7f }, 0.0f, (Vector2){ screenWidth*0.25f, 20 }); - // Create physics thread - pthread_t tid; - pthread_create(&tid, NULL, &PhysicsThread, NULL); - SetTargetFPS(60); //-------------------------------------------------------------------------------------- @@ -122,33 +115,10 @@ int main() } // De-Initialization - //-------------------------------------------------------------------------------------- - pthread_cancel(tid); // Destroy physics thread - + //-------------------------------------------------------------------------------------- ClosePhysics(); // Unitialize physics (including all loaded objects) - CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; -} - -void* PhysicsThread(void *arg) -{ - // Initialize time variables - double currentTime = GetTime(); - double previousTime = currentTime; - - // Physics update loop - while (!WindowShouldClose()) - { - currentTime = GetTime(); - double deltaTime = (double)(currentTime - previousTime); - previousTime = currentTime; - - // Delta time value needs to be inverse multiplied by physics time step value (1/target fps) - UpdatePhysics(deltaTime/PHYSICS_TIMESTEP); - } - - return NULL; } \ No newline at end of file diff --git a/examples/physics_forces.c b/examples/physics_forces.c index 397c23318..285667539 100644 --- a/examples/physics_forces.c +++ b/examples/physics_forces.c @@ -13,15 +13,12 @@ #define PHYSAC_IMPLEMENTATION #include "physac.h" -#include #define FORCE_AMOUNT 5.0f #define FORCE_RADIUS 150 #define LINE_LENGTH 75 #define TRIANGLE_LENGTH 12 -void* PhysicsThread(void *arg); - int main() { // Initialization @@ -30,7 +27,6 @@ int main() int screenHeight = 450; InitWindow(screenWidth, screenHeight, "raylib [physac] example - forces"); - InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module // Global variables @@ -64,10 +60,6 @@ int main() PhysicBody topWall = CreatePhysicBody((Vector2){ screenWidth/2, -25 }, 0.0f, (Vector2){ screenWidth, 50 }); PhysicBody bottomWall = CreatePhysicBody((Vector2){ screenWidth/2, screenHeight + 25 }, 0.0f, (Vector2){ screenWidth, 50 }); - // Create physics thread - pthread_t tid; - pthread_create(&tid, NULL, &PhysicsThread, NULL); - SetTargetFPS(60); //-------------------------------------------------------------------------------------- @@ -179,33 +171,10 @@ int main() } // De-Initialization - //-------------------------------------------------------------------------------------- - pthread_cancel(tid); // Destroy physics thread - + //-------------------------------------------------------------------------------------- ClosePhysics(); // Unitialize physics module - CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; -} - -void* PhysicsThread(void *arg) -{ - // Initialize time variables - double currentTime = GetTime(); - double previousTime = currentTime; - - // Physics update loop - while (!WindowShouldClose()) - { - currentTime = GetTime(); - double deltaTime = (double)(currentTime - previousTime); - previousTime = currentTime; - - // Delta time value needs to be inverse multiplied by physics time step value (1/target fps) - UpdatePhysics(deltaTime/PHYSICS_TIMESTEP); - } - - return NULL; } \ No newline at end of file From 54537e8f0b57df2f3f15d8e46309672f46e4775a Mon Sep 17 00:00:00 2001 From: victorfisac Date: Tue, 14 Jun 2016 20:23:46 +0200 Subject: [PATCH 07/12] Fixed bug in delta time calculation... and added PHYSAC_NO_THREADS define. Improved physac example drawing frames per second in screen. --- examples/physics_basic_rigidbody.c | 2 ++ examples/physics_forces.c | 4 +++- src/physac.h | 26 ++++++++++++++++---------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/physics_basic_rigidbody.c b/examples/physics_basic_rigidbody.c index 811ab9828..5223f46a4 100644 --- a/examples/physics_basic_rigidbody.c +++ b/examples/physics_basic_rigidbody.c @@ -110,6 +110,8 @@ int main() // 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); + DrawFPS(10, 10); + EndDrawing(); //---------------------------------------------------------------------------------- } diff --git a/examples/physics_forces.c b/examples/physics_forces.c index 285667539..875105529 100644 --- a/examples/physics_forces.c +++ b/examples/physics_forces.c @@ -164,7 +164,9 @@ int main() // 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); + DrawText("Use R to reset objects position", screenWidth/2 - MeasureText("Use R to reset objects position", 20)/2, screenHeight*0.875f, 20, GRAY); + + DrawFPS(10, 10); EndDrawing(); //---------------------------------------------------------------------------------- diff --git a/src/physac.h b/src/physac.h index ea8801c3a..5ce3970e0 100644 --- a/src/physac.h +++ b/src/physac.h @@ -178,8 +178,10 @@ PHYSACDEF Rectangle TransformToRectangle(Transform transform); #include // Required for: cos(), sin(), abs(), fminf() #include // Required for typedef unsigned long long int uint64_t, used by hi-res timer -#include // Required for: pthread_create() -#include "utils.h" // Required for: TraceLog() + +#ifndef PHYSAC_NO_THREADS + #include // Required for: pthread_create() +#endif #if defined(PLATFORM_DESKTOP) // Functions required to query time on Windows @@ -234,9 +236,11 @@ PHYSACDEF void InitPhysics(Vector2 gravity) physicBodiesCount = 0; gravityForce = gravity; - // Create physics thread - pthread_t tid; - pthread_create(&tid, NULL, &PhysicsThread, NULL); + #ifndef PHYSAC_NO_THREADS // NOTE: if defined, user will need to create a thread for PhysicsThread function manually + // Create physics thread + pthread_t tid; + pthread_create(&tid, NULL, &PhysicsThread, NULL); + #endif } // Update physic objects, calculating physic behaviours and collisions detection @@ -768,7 +772,6 @@ static void InitTimer(void) { baseTime = (uint64_t)now.tv_sec*1000000000LLU + (uint64_t)now.tv_nsec; } - else TraceLog(WARNING, "No hi-resolution timer available"); #endif previousTime = GetCurrentTime(); // Get time as double @@ -777,22 +780,25 @@ static void InitTimer(void) // Time measure returned are microseconds static double GetCurrentTime(void) { + double time; + #if defined(PLATFORM_DESKTOP) unsigned long long int clockFrequency, currentTime; QueryPerformanceFrequency(&clockFrequency); QueryPerformanceCounter(¤tTime); - - return (double)(currentTime/clockFrequency); + #endif #if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); - uint64_t time = (uint64_t)ts.tv_sec*1000000000LLU + (uint64_t)ts.tv_nsec; + uint64_t temp = (uint64_t)ts.tv_sec*1000000000LLU + (uint64_t)ts.tv_nsec; - return (double)(time - baseTime)*1e-9; + time = (double)(temp - baseTime)*1e-9; #endif + + return time; } // Returns the dot product of two Vector2 From 5a1cbb2842f6801f7a86086e87f4821fd0b09229 Mon Sep 17 00:00:00 2001 From: victorfisac Date: Tue, 14 Jun 2016 20:25:08 +0200 Subject: [PATCH 08/12] Fix current time value --- src/physac.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/physac.h b/src/physac.h index 5ce3970e0..4f9b736f3 100644 --- a/src/physac.h +++ b/src/physac.h @@ -788,6 +788,7 @@ static double GetCurrentTime(void) QueryPerformanceFrequency(&clockFrequency); QueryPerformanceCounter(¤tTime); + time = (double)((double)currentTime/(double)clockFrequency); #endif #if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) From 1a8fbe5cf0b982cf74434f1ba4654fced71a0450 Mon Sep 17 00:00:00 2001 From: victorfisac Date: Tue, 14 Jun 2016 20:31:48 +0200 Subject: [PATCH 09/12] Add pthread external library to source... and add instructions in physac examples to run it successful. --- .gitignore | 3 ++- examples/physics_basic_rigidbody.c | 4 ++++ examples/physics_forces.c | 5 +++++ src/external/pthread/pthreadGC2.dll | Bin 0 -> 119888 bytes 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/external/pthread/pthreadGC2.dll diff --git a/.gitignore b/.gitignore index b221a37b9..cf9cdfe14 100644 --- a/.gitignore +++ b/.gitignore @@ -72,4 +72,5 @@ src/libraylib.bc # external libraries DLLs !src/external/glfw3/lib/win32/glfw3.dll !src/external/openal_soft/lib/win32/OpenAL32.dll -!src/external/OculusSDK/LibOVR/LibOVRRT32_1.dll \ No newline at end of file +!src/external/OculusSDK/LibOVR/LibOVRRT32_1.dll +!src/external/pthread/pthreadGC2.dll \ No newline at end of file diff --git a/examples/physics_basic_rigidbody.c b/examples/physics_basic_rigidbody.c index 5223f46a4..b85f75435 100644 --- a/examples/physics_basic_rigidbody.c +++ b/examples/physics_basic_rigidbody.c @@ -5,6 +5,10 @@ * 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) * +* +* Compile example using: +* cmd /c IF NOT EXIST pthreadGC2.dll copy C:\raylib\raylib\src\external\pthread\pthreadGC2.dll $(CURRENT_DIRECTORY) /Y +* * Copyright (c) 2016 Victor Fisac and Ramon Santamaria (@raysan5) * ********************************************************************************************/ diff --git a/examples/physics_forces.c b/examples/physics_forces.c index 875105529..7de854839 100644 --- a/examples/physics_forces.c +++ b/examples/physics_forces.c @@ -5,6 +5,11 @@ * 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) * +* NOTE: This example requires raylib module [rlgl] +* +* Compile example using: +* cmd /c IF NOT EXIST pthreadGC2.dll copy C:\raylib\raylib\src\external\pthread\pthreadGC2.dll $(CURRENT_DIRECTORY) /Y +* * Copyright (c) 2016 Victor Fisac and Ramon Santamaria (@raysan5) * ********************************************************************************************/ diff --git a/src/external/pthread/pthreadGC2.dll b/src/external/pthread/pthreadGC2.dll new file mode 100644 index 0000000000000000000000000000000000000000..67b9289dfd9f6f1ad12c1d137f4e57ad1a54271c GIT binary patch literal 119888 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 D*YY=? literal 0 HcmV?d00001 From 4e84ded7ef3b165081e08a83d95bf54387a413ca Mon Sep 17 00:00:00 2001 From: victorfisac Date: Tue, 14 Jun 2016 20:38:49 +0200 Subject: [PATCH 10/12] Fixed spacing and set UpdatePhysics() function as static... and remove static from PhysicsThread(). --- examples/physics_basic_rigidbody.c | 8 +- examples/physics_forces.c | 2 +- src/physac.h | 418 ++++++++++++++--------------- 3 files changed, 213 insertions(+), 215 deletions(-) diff --git a/examples/physics_basic_rigidbody.c b/examples/physics_basic_rigidbody.c index b85f75435..084bfb0ee 100644 --- a/examples/physics_basic_rigidbody.c +++ b/examples/physics_basic_rigidbody.c @@ -21,7 +21,6 @@ #define MOVE_VELOCITY 5 #define JUMP_VELOCITY 30 - int main() { // Initialization @@ -30,7 +29,7 @@ int main() int screenHeight = 450; InitWindow(screenWidth, screenHeight, "raylib [physac] example - basic rigidbody"); - InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module + // InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module // Debug variables bool isDebug = false; @@ -64,8 +63,7 @@ int main() while (!WindowShouldClose()) // Detect window close button or ESC key { // Update - //---------------------------------------------------------------------------------- - + //---------------------------------------------------------------------------------- // Check rectangle movement inputs if (IsKeyPressed('W')) rectangle->rigidbody.velocity.y = JUMP_VELOCITY; if (IsKeyDown('A')) rectangle->rigidbody.velocity.x = -MOVE_VELOCITY; @@ -121,7 +119,7 @@ int main() } // De-Initialization - //-------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------- ClosePhysics(); // Unitialize physics (including all loaded objects) CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/examples/physics_forces.c b/examples/physics_forces.c index 7de854839..efe8e2407 100644 --- a/examples/physics_forces.c +++ b/examples/physics_forces.c @@ -178,7 +178,7 @@ int main() } // De-Initialization - //-------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------- ClosePhysics(); // Unitialize physics module CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/src/physac.h b/src/physac.h index 4f9b736f3..b8cc8f156 100644 --- a/src/physac.h +++ b/src/physac.h @@ -146,7 +146,7 @@ typedef struct PhysicBodyData { // Module Functions Declaration //---------------------------------------------------------------------------------- PHYSACDEF void InitPhysics(Vector2 gravity); // Initializes pointers array (just pointers, fixed size) -PHYSACDEF void UpdatePhysics(double deltaTime); // Update physic objects, calculating physic behaviours and collisions detection +PHYSACDEF void* PhysicsThread(void *arg); // Physics calculations thread function PHYSACDEF void ClosePhysics(); // Unitialize all physic objects and empty the objects pool PHYSACDEF PhysicBody CreatePhysicBody(Vector2 position, float rotation, Vector2 scale); // Create a new physic body dinamically, initialize it and add to pool @@ -219,7 +219,7 @@ static Vector2 gravityForce; // Gravity f //---------------------------------------------------------------------------------- // Module specific Functions Declaration //---------------------------------------------------------------------------------- -static void* PhysicsThread(void *arg); // Physics calculations thread function +static void UpdatePhysics(double deltaTime); // Update physic objects, calculating physic behaviours and collisions detection static void InitTimer(void); // Initialize hi-resolution timer static double GetCurrentTime(void); // Time measure returned are microseconds static float Vector2DotProduct(Vector2 v1, Vector2 v2); // Returns the dot product of two Vector2 @@ -243,8 +243,214 @@ PHYSACDEF void InitPhysics(Vector2 gravity) #endif } +// Unitialize all physic objects and empty the objects pool +PHYSACDEF void ClosePhysics() +{ + // Exit physics thread loop + physicsThreadEnabled = false; + + // Free all dynamic memory allocations + for (int i = 0; i < physicBodiesCount; i++) PHYSAC_FREE(physicBodies[i]); + + // Reset enabled physic objects count + physicBodiesCount = 0; +} + +// Create a new physic body dinamically, initialize it and add to pool +PHYSACDEF PhysicBody CreatePhysicBody(Vector2 position, float rotation, Vector2 scale) +{ + // Allocate dynamic memory + PhysicBody obj = (PhysicBody)PHYSAC_MALLOC(sizeof(PhysicBodyData)); + + // Initialize physic body values with generic values + obj->id = physicBodiesCount; + obj->enabled = true; + + obj->transform = (Transform){ (Vector2){ position.x - scale.x/2, position.y - scale.y/2 }, rotation, scale }; + + obj->rigidbody.enabled = false; + obj->rigidbody.mass = 1.0f; + obj->rigidbody.acceleration = (Vector2){ 0.0f, 0.0f }; + obj->rigidbody.velocity = (Vector2){ 0.0f, 0.0f }; + obj->rigidbody.applyGravity = false; + obj->rigidbody.isGrounded = false; + obj->rigidbody.friction = 0.0f; + obj->rigidbody.bounciness = 0.0f; + + obj->collider.enabled = true; + obj->collider.type = COLLIDER_RECTANGLE; + obj->collider.bounds = TransformToRectangle(obj->transform); + obj->collider.radius = 0.0f; + + // Add new physic body to the pointers array + physicBodies[physicBodiesCount] = obj; + + // Increase enabled physic bodies count + physicBodiesCount++; + + return obj; +} + +// Destroy a specific physic body and take it out of the list +PHYSACDEF void DestroyPhysicBody(PhysicBody pbody) +{ + // Free dynamic memory allocation + PHYSAC_FREE(physicBodies[pbody->id]); + + // Remove *obj from the pointers array + for (int i = pbody->id; i < physicBodiesCount; i++) + { + // Resort all the following pointers of the array + if ((i + 1) < physicBodiesCount) + { + physicBodies[i] = physicBodies[i + 1]; + physicBodies[i]->id = physicBodies[i + 1]->id; + } + else PHYSAC_FREE(physicBodies[i]); + } + + // Decrease enabled physic bodies count + physicBodiesCount--; +} + +// Apply directional force to a physic body +PHYSACDEF void ApplyForce(PhysicBody pbody, Vector2 force) +{ + if (pbody->rigidbody.enabled) + { + pbody->rigidbody.velocity.x += force.x/pbody->rigidbody.mass; + pbody->rigidbody.velocity.y += force.y/pbody->rigidbody.mass; + } +} + +// Apply radial force to all physic objects in range +PHYSACDEF void ApplyForceAtPosition(Vector2 position, float force, float radius) +{ + for (int i = 0; i < physicBodiesCount; i++) + { + if (physicBodies[i]->rigidbody.enabled) + { + // Calculate direction and distance between force and physic body position + Vector2 distance = (Vector2){ physicBodies[i]->transform.position.x - position.x, physicBodies[i]->transform.position.y - position.y }; + + if (physicBodies[i]->collider.type == COLLIDER_RECTANGLE) + { + distance.x += physicBodies[i]->transform.scale.x/2; + distance.y += physicBodies[i]->transform.scale.y/2; + } + + float distanceLength = Vector2Length(distance); + + // Check if physic body is in force range + if (distanceLength <= radius) + { + // Normalize force direction + distance.x /= distanceLength; + distance.y /= -distanceLength; + + // Calculate final force + Vector2 finalForce = { distance.x*force, distance.y*force }; + + // Apply force to the physic body + ApplyForce(physicBodies[i], finalForce); + } + } + } +} + +// Convert Transform data type to Rectangle (position and scale) +PHYSACDEF Rectangle TransformToRectangle(Transform transform) +{ + return (Rectangle){transform.position.x, transform.position.y, transform.scale.x, transform.scale.y}; +} + +// Physics calculations thread function +PHYSACDEF void* PhysicsThread(void *arg) +{ + // Initialize thread loop state + physicsThreadEnabled = true; + + // Initialize hi-resolution timer + InitTimer(); + + // Physics update loop + while (physicsThreadEnabled) + { + currentTime = GetCurrentTime(); + double deltaTime = (double)(currentTime - previousTime); + previousTime = currentTime; + + // Delta time value needs to be inverse multiplied by physics time step value (1/target fps) + UpdatePhysics(deltaTime/PHYSICS_TIMESTEP); + } + + return NULL; +} + +//---------------------------------------------------------------------------------- +// Module specific Functions Definition +//---------------------------------------------------------------------------------- +// Initialize hi-resolution timer +static void InitTimer(void) +{ +#if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) + struct timespec now; + + if (clock_gettime(CLOCK_MONOTONIC, &now) == 0) // Success + { + baseTime = (uint64_t)now.tv_sec*1000000000LLU + (uint64_t)now.tv_nsec; + } +#endif + + previousTime = GetCurrentTime(); // Get time as double +} + +// Time measure returned are microseconds +static double GetCurrentTime(void) +{ + double time; + +#if defined(PLATFORM_DESKTOP) + unsigned long long int clockFrequency, currentTime; + + QueryPerformanceFrequency(&clockFrequency); + QueryPerformanceCounter(¤tTime); + + time = (double)((double)currentTime/(double)clockFrequency); +#endif + +#if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint64_t temp = (uint64_t)ts.tv_sec*1000000000LLU + (uint64_t)ts.tv_nsec; + + time = (double)(temp - baseTime)*1e-9; +#endif + + return time; +} + +// Returns the dot product of two Vector2 +static float Vector2DotProduct(Vector2 v1, Vector2 v2) +{ + float result; + + result = v1.x*v2.x + v1.y*v2.y; + + return result; +} + +static float Vector2Length(Vector2 v) +{ + float result; + + result = sqrt(v.x*v.x + v.y*v.y); + + return result; +} + // Update physic objects, calculating physic behaviours and collisions detection -PHYSACDEF void UpdatePhysics(double deltaTime) +static void UpdatePhysics(double deltaTime) { for (int i = 0; i < physicBodiesCount; i++) { @@ -615,210 +821,4 @@ PHYSACDEF void UpdatePhysics(double deltaTime) } } -// Unitialize all physic objects and empty the objects pool -PHYSACDEF void ClosePhysics() -{ - // Exit physics thread loop - physicsThreadEnabled = false; - - // Free all dynamic memory allocations - for (int i = 0; i < physicBodiesCount; i++) PHYSAC_FREE(physicBodies[i]); - - // Reset enabled physic objects count - physicBodiesCount = 0; -} - -// Create a new physic body dinamically, initialize it and add to pool -PHYSACDEF PhysicBody CreatePhysicBody(Vector2 position, float rotation, Vector2 scale) -{ - // Allocate dynamic memory - PhysicBody obj = (PhysicBody)PHYSAC_MALLOC(sizeof(PhysicBodyData)); - - // Initialize physic body values with generic values - obj->id = physicBodiesCount; - obj->enabled = true; - - obj->transform = (Transform){ (Vector2){ position.x - scale.x/2, position.y - scale.y/2 }, rotation, scale }; - - obj->rigidbody.enabled = false; - obj->rigidbody.mass = 1.0f; - obj->rigidbody.acceleration = (Vector2){ 0.0f, 0.0f }; - obj->rigidbody.velocity = (Vector2){ 0.0f, 0.0f }; - obj->rigidbody.applyGravity = false; - obj->rigidbody.isGrounded = false; - obj->rigidbody.friction = 0.0f; - obj->rigidbody.bounciness = 0.0f; - - obj->collider.enabled = true; - obj->collider.type = COLLIDER_RECTANGLE; - obj->collider.bounds = TransformToRectangle(obj->transform); - obj->collider.radius = 0.0f; - - // Add new physic body to the pointers array - physicBodies[physicBodiesCount] = obj; - - // Increase enabled physic bodies count - physicBodiesCount++; - - return obj; -} - -// Destroy a specific physic body and take it out of the list -PHYSACDEF void DestroyPhysicBody(PhysicBody pbody) -{ - // Free dynamic memory allocation - PHYSAC_FREE(physicBodies[pbody->id]); - - // Remove *obj from the pointers array - for (int i = pbody->id; i < physicBodiesCount; i++) - { - // Resort all the following pointers of the array - if ((i + 1) < physicBodiesCount) - { - physicBodies[i] = physicBodies[i + 1]; - physicBodies[i]->id = physicBodies[i + 1]->id; - } - else PHYSAC_FREE(physicBodies[i]); - } - - // Decrease enabled physic bodies count - physicBodiesCount--; -} - -// Apply directional force to a physic body -PHYSACDEF void ApplyForce(PhysicBody pbody, Vector2 force) -{ - if (pbody->rigidbody.enabled) - { - pbody->rigidbody.velocity.x += force.x/pbody->rigidbody.mass; - pbody->rigidbody.velocity.y += force.y/pbody->rigidbody.mass; - } -} - -// Apply radial force to all physic objects in range -PHYSACDEF void ApplyForceAtPosition(Vector2 position, float force, float radius) -{ - for (int i = 0; i < physicBodiesCount; i++) - { - if (physicBodies[i]->rigidbody.enabled) - { - // Calculate direction and distance between force and physic body position - Vector2 distance = (Vector2){ physicBodies[i]->transform.position.x - position.x, physicBodies[i]->transform.position.y - position.y }; - - if (physicBodies[i]->collider.type == COLLIDER_RECTANGLE) - { - distance.x += physicBodies[i]->transform.scale.x/2; - distance.y += physicBodies[i]->transform.scale.y/2; - } - - float distanceLength = Vector2Length(distance); - - // Check if physic body is in force range - if (distanceLength <= radius) - { - // Normalize force direction - distance.x /= distanceLength; - distance.y /= -distanceLength; - - // Calculate final force - Vector2 finalForce = { distance.x*force, distance.y*force }; - - // Apply force to the physic body - ApplyForce(physicBodies[i], finalForce); - } - } - } -} - -// Convert Transform data type to Rectangle (position and scale) -PHYSACDEF Rectangle TransformToRectangle(Transform transform) -{ - return (Rectangle){transform.position.x, transform.position.y, transform.scale.x, transform.scale.y}; -} - -//---------------------------------------------------------------------------------- -// Module specific Functions Definition -//---------------------------------------------------------------------------------- -// Physics calculations thread function -static void* PhysicsThread(void *arg) -{ - // Initialize thread loop state - physicsThreadEnabled = true; - - // Initialize hi-resolution timer - InitTimer(); - - // Physics update loop - while (physicsThreadEnabled) - { - currentTime = GetCurrentTime(); - double deltaTime = (double)(currentTime - previousTime); - previousTime = currentTime; - - // Delta time value needs to be inverse multiplied by physics time step value (1/target fps) - UpdatePhysics(deltaTime/PHYSICS_TIMESTEP); - } - - return NULL; -} - -// Initialize hi-resolution timer -static void InitTimer(void) -{ -#if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) - struct timespec now; - - if (clock_gettime(CLOCK_MONOTONIC, &now) == 0) // Success - { - baseTime = (uint64_t)now.tv_sec*1000000000LLU + (uint64_t)now.tv_nsec; - } -#endif - - previousTime = GetCurrentTime(); // Get time as double -} - -// Time measure returned are microseconds -static double GetCurrentTime(void) -{ - double time; - -#if defined(PLATFORM_DESKTOP) - unsigned long long int clockFrequency, currentTime; - - QueryPerformanceFrequency(&clockFrequency); - QueryPerformanceCounter(¤tTime); - - time = (double)((double)currentTime/(double)clockFrequency); -#endif - -#if defined(PLATFORM_ANDROID) || defined(PLATFORM_RPI) - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - uint64_t temp = (uint64_t)ts.tv_sec*1000000000LLU + (uint64_t)ts.tv_nsec; - - time = (double)(temp - baseTime)*1e-9; -#endif - - return time; -} - -// Returns the dot product of two Vector2 -static float Vector2DotProduct(Vector2 v1, Vector2 v2) -{ - float result; - - result = v1.x*v2.x + v1.y*v2.y; - - return result; -} - -static float Vector2Length(Vector2 v) -{ - float result; - - result = sqrt(v.x*v.x + v.y*v.y); - - return result; -} - #endif // PHYSAC_IMPLEMENTATION \ No newline at end of file From 1879a8129e786e859cc2984e294ef9c22663f923 Mon Sep 17 00:00:00 2001 From: victorfisac Date: Tue, 14 Jun 2016 20:40:12 +0200 Subject: [PATCH 11/12] Fixed little bug in physac example --- examples/physics_basic_rigidbody.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/physics_basic_rigidbody.c b/examples/physics_basic_rigidbody.c index 084bfb0ee..75720c978 100644 --- a/examples/physics_basic_rigidbody.c +++ b/examples/physics_basic_rigidbody.c @@ -29,7 +29,7 @@ int main() int screenHeight = 450; InitWindow(screenWidth, screenHeight, "raylib [physac] example - basic rigidbody"); - // InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module + InitPhysics((Vector2){ 0.0f, -9.81f/2 }); // Initialize physics module // Debug variables bool isDebug = false; From 1b0996fb0bcf68e2a14bc6260c6f2c5366ab033f Mon Sep 17 00:00:00 2001 From: victorfisac Date: Tue, 14 Jun 2016 20:54:20 +0200 Subject: [PATCH 12/12] Updated physac header documentation --- src/physac.h | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/physac.h b/src/physac.h index b8cc8f156..dd4c41269 100644 --- a/src/physac.h +++ b/src/physac.h @@ -15,6 +15,10 @@ * The generated implementation will stay private inside implementation file and all * internal symbols and functions will only be visible inside that file. * +* #define PHYSAC_NO_THREADS +* The generated implementation won't include pthread library and user must create a secondary thread to call PhysicsThread(). +* It is so important that the thread where PhysicsThread() is called must not have v-sync or any other CPU limitation. +* * #define PHYSAC_STANDALONE * Avoid raylib.h header inclusion in this file. Data types defined on raylib are defined * internally in the library and input management and drawing functions must be provided by @@ -27,12 +31,16 @@ * * LIMITATIONS: * -* // TODO. +* - There is a limit of 256 physic objects. +* - Physics behaviour can be unexpected using bounciness or friction values out of 0.0f - 1.0f range. +* - The module is limited to 2D axis oriented physics. +* - Physics colliders must be rectangle or circle shapes (there is not a custom polygon collider type). * * VERSIONS: * -* 1.0 (09-Jun-2016) Module names review and converted to header-only. -* 0.9 (23-Mar-2016) Complete module redesign, steps-based for better physics resolution. +* 1.0 (14-Jun-2016) New module defines and fixed some delta time calculation bugs. +* 0.9 (09-Jun-2016) Module names review and converted to header-only. +* 0.8 (23-Mar-2016) Complete module redesign, steps-based for better physics resolution. * 0.3 (13-Feb-2016) Reviewed to add PhysicObjects pool. * 0.2 (03-Jan-2016) Improved physics calculations. * 0.1 (30-Dec-2015) Initial release.