/* Copyright (c) 2015 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "collision/CollisionRect2D.h" #include "collision/CollisionShared.h" #include "gamemath/Matrix4x4f.h" #include "gamemath/Vector2f.h" #include "renderer/VertexTypes.h" #include "shadercollection/ShaderCollection.h" #include "shell/Shell.h" #include "shell/ShellKeyCodes.h" #include "testharness/BouncingBallScreen.h" #include "testharness/SharedEvents.h" #include "testharness/TestHarness_globals.h" #include "utilities/IOUtilities.h" #include "utilities/printfFormats.h" #include "utilities/Ranrot.h" #include #include #include #include #include #include #define stemobject_implementation BouncingBallScreen stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_entry(activate); stemobject_vtable_entry(deactivate); stemobject_vtable_end(); #define FRAME_INTERVAL (1.0 / 60.0) #define GRAVITY 0x00080 #define ELASTICITY 0x0E000 #define BALL_AREA_X 0x0F0000 #define BALL_AREA_Y 0x0B0000 #define RADIUS_MIN 0x02000 #define RADIUS_MAX 0x10000 #define INITIAL_VELOCITY_MAX 0x08000 #define COLLISION_DURATION 0.25 BouncingBallScreen * BouncingBallScreen_create(Renderer * renderer) { stemobject_create_implementation(init, renderer) } #define CIRCLE_TESSELATIONS 32 #define COLOR_RECT COLOR4f(1.0f, 1.0f, 0.0f, 1.0f) #define COLOR_CIRCLE_LAST COLOR4f(0.0f, 0.25f, 0.5f, 1.0f) #define COLOR_CIRCLE COLOR4f(0.0f, 1.0f, 1.0f, 1.0f) static void setVertexColor(struct vertex_p2f_t2f_c4f * vertex, Color4f color) { vertex->color[0] = color.red; vertex->color[1] = color.green; vertex->color[2] = color.blue; vertex->color[3] = color.alpha; } static void writeCollisionObjectVertices(Renderable * renderable, VertexIO * vertexIO, void * context) { BouncingBallScreen * self = context; double currentTime = Shell_getCurrentTime(); for (size_t objectIndex = 0; objectIndex < self->resolver->objectIO->movingObjectCount; objectIndex++) { CollisionObject * object = self->resolver->objectIO->movingObjects[objectIndex]; switch (call_virtual(getShapeType, object)) { case COLLISION_SHAPE_RECT_2D: { CollisionRect2D * rect = (CollisionRect2D *) object; struct vertex_p2f_t2f_c4f vertices[8], vertex = {.texCoords = {0.0f, 0.0f}}; uint32_t indexes[8]; setVertexColor(&vertex, COLOR_RECT); vertex.position[0] = xtof(rect->position.x); vertex.position[1] = xtof(rect->position.y); vertices[0] = vertex; vertex.position[0] = xtof(rect->position.x + rect->size.x); vertices[1] = vertex; vertex.position[1] = xtof(rect->position.y + rect->size.y); vertices[2] = vertex; vertex.position[0] = xtof(rect->position.x); vertices[3] = vertex; for (unsigned int vertexIndex = 0; vertexIndex < 4; vertexIndex++) { indexes[vertexIndex * 2 + 0] = vertexIndex; indexes[vertexIndex * 2 + 1] = (vertexIndex + 1) % 4; } VertexIO_writeIndexedVertices(vertexIO, sizeof(vertices) / sizeof(vertices[0]), vertices, sizeof(indexes) / sizeof(indexes[0]), indexes); break; } case COLLISION_SHAPE_CIRCLE: { CollisionCircle * circle = (CollisionCircle *) object; struct bouncingBall * ball = circle->owner, * lastBall = NULL; struct vertex_p2f_t2f_c4f vertices[CIRCLE_TESSELATIONS * 2], vertex = {.texCoords = {0.0f, 0.0f}}; uint32_t indexes[CIRCLE_TESSELATIONS * 4 + 4]; unsigned int ballIndex; for (ballIndex = 0; ballIndex < BOUNCING_BALL_COUNT - 1; ballIndex++) { if (&self->balls[ballIndex].circle == circle) { break; } } lastBall = &self->lastBalls[ballIndex]; setVertexColor(&vertex, COLOR_CIRCLE_LAST); for (unsigned int tesselationIndex = 0; tesselationIndex < CIRCLE_TESSELATIONS; tesselationIndex++) { vertex.position[0] = xtof(lastBall->circle.lastPosition.x) + xtof(lastBall->circle.radius) * cosf(tesselationIndex * M_PI * 2 / CIRCLE_TESSELATIONS); vertex.position[1] = xtof(lastBall->circle.lastPosition.y) + xtof(lastBall->circle.radius) * sinf(tesselationIndex * M_PI * 2 / CIRCLE_TESSELATIONS); vertices[tesselationIndex] = vertex; } if (currentTime - ball->lastCollisionTime < COLLISION_DURATION) { vertex.color[0] = 1.0f - (currentTime - ball->lastCollisionTime) / COLLISION_DURATION; vertex.color[1] = (currentTime - ball->lastCollisionTime) / COLLISION_DURATION; vertex.color[2] = (currentTime - ball->lastCollisionTime) / COLLISION_DURATION; } else { setVertexColor(&vertex, COLOR_CIRCLE); } for (unsigned int tesselationIndex = 0; tesselationIndex < CIRCLE_TESSELATIONS; tesselationIndex++) { vertex.position[0] = xtof(circle->position.x) + xtof(circle->radius) * cosf(tesselationIndex * M_PI * 2 / CIRCLE_TESSELATIONS); vertex.position[1] = xtof(circle->position.y) + xtof(circle->radius) * sinf(tesselationIndex * M_PI * 2 / CIRCLE_TESSELATIONS); vertices[CIRCLE_TESSELATIONS + tesselationIndex] = vertex; } for (unsigned int tesselationIndex = 0; tesselationIndex < CIRCLE_TESSELATIONS; tesselationIndex++) { indexes[tesselationIndex * 2 + 0] = tesselationIndex; indexes[tesselationIndex * 2 + 1] = (tesselationIndex + 1) % CIRCLE_TESSELATIONS; } float angle = atan2f(circle->position.y - circle->lastPosition.y, circle->position.x - circle->lastPosition.x); int bestEdgeIndex = roundf((angle + M_PI / 2) * CIRCLE_TESSELATIONS / (M_PI * 2)); bestEdgeIndex = (bestEdgeIndex % CIRCLE_TESSELATIONS + CIRCLE_TESSELATIONS) % CIRCLE_TESSELATIONS; indexes[CIRCLE_TESSELATIONS * 2 + 0] = bestEdgeIndex; indexes[CIRCLE_TESSELATIONS * 2 + 1] = bestEdgeIndex + CIRCLE_TESSELATIONS; bestEdgeIndex += CIRCLE_TESSELATIONS / 2; bestEdgeIndex %= CIRCLE_TESSELATIONS; indexes[CIRCLE_TESSELATIONS * 2 + 2] = bestEdgeIndex; indexes[CIRCLE_TESSELATIONS * 2 + 3] = bestEdgeIndex + CIRCLE_TESSELATIONS; for (unsigned int tesselationIndex = 0; tesselationIndex < CIRCLE_TESSELATIONS; tesselationIndex++) { indexes[CIRCLE_TESSELATIONS * 2 + 4 + tesselationIndex * 2] = CIRCLE_TESSELATIONS + tesselationIndex; indexes[CIRCLE_TESSELATIONS * 2 + 5 + tesselationIndex * 2] = CIRCLE_TESSELATIONS + (tesselationIndex + 1) % CIRCLE_TESSELATIONS; } VertexIO_writeIndexedVertices(vertexIO, sizeof(vertices) / sizeof(vertices[0]), vertices, sizeof(indexes) / sizeof(indexes[0]), indexes); if (ball->lastCollisionCircle != NULL) { struct vertex_p2f_t2f_c4f vertices[2], vertex = {.texCoords = {0.0f, 0.0f}}; setVertexColor(&vertex, COLOR4f(1.0f - (currentTime - ball->lastCollisionTime) / COLLISION_DURATION, 0.0f, 0.0f, 1.0f)); vertex.position[0] = xtof(ball->circle.position.x); vertex.position[1] = xtof(ball->circle.position.y); vertices[0] = vertex; vertex.position[0] = xtof(ball->lastCollisionCircle->position.x); vertex.position[1] = xtof(ball->lastCollisionCircle->position.y); vertices[1] = vertex; uint32_t indexes[2] = {0, 1}; VertexIO_writeIndexedVertices(vertexIO, sizeof(vertices) / sizeof(vertices[0]), vertices, sizeof(indexes) / sizeof(indexes[0]), indexes); } break; } } } } static void writeTextVertices(Renderable * renderable, VertexIO * vertexIO, void * context) { BouncingBallScreen * self = context; if (self->drawLabels) { size_t objectIndex; char labelString[3]; char infoString[64]; for (objectIndex = 0; objectIndex < self->resolver->objectIO->movingObjectCount; objectIndex++) { CollisionObject * object = self->resolver->objectIO->movingObjects[objectIndex]; if (call_virtual(getShapeType, object) == COLLISION_SHAPE_CIRCLE) { CollisionCircle * circle = (CollisionCircle *) object; snprintf_safe(labelString, sizeof(labelString), SIZE_T_FORMAT, objectIndex); writeStringVertices(g_font, labelString, TEXT_STRLEN, 0.5f, VECTOR2f(xtof(circle->position.x), xtof(circle->position.y)), VECTOR2f(0.5f, 0.375f), false, COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO); } } snprintf_safe(infoString, sizeof(infoString), "Frame %u (%gx speed) (%u collisions)", self->frameCount, self->speedMultiplier, self->lastCollisionCount); writeStringVertices(g_font, infoString, TEXT_STRLEN, 0.5f, VECTOR2f(0.0f, -11.5f), VECTOR2f(0.5f, 0.5f), false, COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO); } } bool BouncingBallScreen_init(BouncingBallScreen * self, Renderer * renderer) { call_super(init, self); self->renderer = renderer; self->renderLayer = RenderLayer_create(RENDER_LAYER_SORT_NONE, NULL, NULL); self->shaderConfigurationLine = ShaderConfiguration2DTexture_create(ShaderCollection_get2DTextureShader()); call_virtual(setTexture, self->shaderConfigurationLine, 0, g_blankTexture, false); self->shaderConfigurationText = ShaderConfiguration2DTexture_create(ShaderCollection_get2DTextureShader()); call_virtual(setTexture, self->shaderConfigurationText, 0, g_fontTexture, false); self->renderPipelineConfiguration = default2DRenderPipelineConfiguration(RENDER_BLEND_ALPHA); self->lineRenderable = Renderable_createWithCallback(PRIMITIVE_LINES, &self->renderPipelineConfiguration, self->shaderConfigurationLine, writeCollisionObjectVertices, NULL, self); self->textRenderable = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &self->renderPipelineConfiguration, self->shaderConfigurationText, writeTextVertices, NULL, self); self->intersectionManager = IntersectionManager_createWithStandardHandlers(); self->backgrounded = false; self->paused = false; self->drawLabels = false; self->speedMultiplier = 1; self->runFraction = 0.0f; RenderLayer_addRenderable(self->renderLayer, self->lineRenderable, 0, RECT4i_EMPTY); RenderLayer_addRenderable(self->renderLayer, self->textRenderable, 0, RECT4i_EMPTY); return true; } void BouncingBallScreen_dispose(BouncingBallScreen * self) { if (self->runLoop != NULL) { FixedIntervalRunLoop_dispose(self->runLoop); } Renderable_dispose(self->lineRenderable); Renderable_dispose(self->textRenderable); ShaderConfiguration2DTexture_dispose(self->shaderConfigurationLine); ShaderConfiguration2DTexture_dispose(self->shaderConfigurationText); IntersectionManager_dispose(self->intersectionManager); RenderLayer_dispose(self->renderLayer); call_super(dispose, self); } static void stepSimulation(BouncingBallScreen * self) { unsigned int ballIndex; double currentTime = Shell_getCurrentTime(); for (ballIndex = 0; ballIndex < BOUNCING_BALL_COUNT; ballIndex++) { if (currentTime - self->balls[ballIndex].lastCollisionTime > COLLISION_DURATION) { self->balls[ballIndex].lastCollisionCircle = NULL; } if (self->balls[ballIndex].circle.position.x < -BALL_AREA_X || self->balls[ballIndex].circle.position.x > BALL_AREA_X || self->balls[ballIndex].circle.position.y < -BALL_AREA_Y || self->balls[ballIndex].circle.position.y > BALL_AREA_Y) { self->balls[ballIndex].velocity = VECTOR2x_ZERO; } else { self->balls[ballIndex].velocity.y -= GRAVITY; } CollisionCircle_updatePosition(&self->balls[ballIndex].circle, Vector2x_add(self->balls[ballIndex].circle.position, self->balls[ballIndex].velocity)); } memcpy(self->lastBalls, self->balls, sizeof(self->balls)); self->lastCollisionCount = 0; CollisionResolver_resolveAll(self->resolver); self->frameCount++; } static void run(void * context) { BouncingBallScreen * self = context; self->runFraction += self->speedMultiplier; while (self->runFraction > 1.0f) { stepSimulation(self); self->runFraction -= 1.0f; } } void ballCollision(CollisionRecord collision, fixed16_16 timesliceSize, fixed16_16 subframeTime) { CollisionCircle * circle = (CollisionCircle *) collision.object1; struct bouncingBall * ball = (struct bouncingBall *) circle->owner; BouncingBallScreen * self = (BouncingBallScreen *) ball->owner; Vector2x reflectedVelocity; ball->lastCollisionTime = Shell_getCurrentTime(); if (call_virtual(getShapeType, collision.object2) == COLLISION_SHAPE_CIRCLE) { ball->lastCollisionCircle = (CollisionCircle *) collision.object2; } else { ball->lastCollisionCircle = NULL; } reflectedVelocity = Vector2x_reflect(ball->velocity, VECTOR2x(collision.normal.x, collision.normal.y)); ball->velocity = Vector2x_multiplyScalar(reflectedVelocity, ELASTICITY); circle->position = Vector2x_add(circle->lastPosition, Vector2x_multiplyScalar(ball->velocity, timesliceSize)); self->lastCollisionCount++; } static bool draw(Atom eventID, void * eventData, void * context) { BouncingBallScreen * self = context; double * activeDrawDelta = eventData; if (!self->paused && !self->backgrounded) { FixedIntervalRunLoop_advance(self->runLoop, *activeDrawDelta); } Renderer_clear(self->renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f)); Renderer_drawLayer(self->renderer, self->renderLayer, 0.0, 0.0); if (!self->paused && !self->backgrounded) { Shell_redisplay(); } return true; } static bool keyDown(Atom eventID, void * eventData, void * context) { BouncingBallScreen * self = context; struct keyEvent * event = eventData; switch (event->keyCode) { case KEY_CODE_P: self->paused = !self->paused; if (!self->paused) { Shell_redisplay(); } break; case KEY_CODE_OPEN_BRACKET: if (!self->paused && !event->isRepeat) { self->speedMultiplier /= 2; } break; case KEY_CODE_CLOSE_BRACKET: if (self->paused) { stepSimulation(self); Shell_redisplay(); } else if (!event->isRepeat) { self->speedMultiplier *= 2; } break; case KEY_CODE_TAB: self->drawLabels = !self->drawLabels; Shell_redisplay(); break; } return true; } static bool backgrounded(Atom eventID, void * eventData, void * context) { BouncingBallScreen * self = context; self->backgrounded = true; return true; } static bool foregrounded(Atom eventID, void * eventData, void * context) { BouncingBallScreen * self = context; self->backgrounded = false; if (!self->paused) { Shell_redisplay(); } return true; } static bool resized(Atom eventID, void * eventData, void * context) { BouncingBallScreen * self = context; Matrix4x4f matrix = Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, -12.0f * g_viewRatio, 12.0f * g_viewRatio, -12.0f, 12.0f, -1.0f, 1.0f); call_virtual(setProjectionMatrix, self->shaderConfigurationLine, matrix); call_virtual(setProjectionMatrix, self->shaderConfigurationText, matrix); Shell_redisplay(); return true; } void BouncingBallScreen_activate(BouncingBallScreen * self, Screen * lastScreen, const char * transitionName) { unsigned int ballIndex; int randomSeed; self->resolver = CollisionResolver_create(self->intersectionManager, false, NULL, NULL); CollisionResolver_addObject(self->resolver, (CollisionObject *) CollisionRect2D_create(NULL, NULL, NULL, VECTOR2x(BALL_AREA_X, BALL_AREA_Y), VECTOR2x(BALL_AREA_X * -2, BALL_AREA_Y * -2), 0x00000)); if (g_fixedRandomSeed) { randomSeed = g_randomSeed; } else { randomSeed = time(NULL); } sdrand(randomSeed); for (ballIndex = 0; ballIndex < BOUNCING_BALL_COUNT; ballIndex++) { self->balls[ballIndex].owner = self; self->balls[ballIndex].velocity.x = irand() % INITIAL_VELOCITY_MAX; self->balls[ballIndex].velocity.y = irand() % INITIAL_VELOCITY_MAX; self->balls[ballIndex].lastCollisionTime = 0.0; self->balls[ballIndex].lastCollisionCircle = NULL; stemobject_assign_vtable(self->balls[ballIndex].circle, CollisionCircle); CollisionCircle_init(&self->balls[ballIndex].circle, &self->balls[ballIndex], ballCollision, NULL, VECTOR2x(irand() % (BALL_AREA_X - RADIUS_MAX), irand() % (BALL_AREA_Y - RADIUS_MAX)), RADIUS_MIN + uirand() % (RADIUS_MAX - RADIUS_MIN)); CollisionCircle_updatePosition(&self->balls[ballIndex].circle, Vector2x_add(self->balls[ballIndex].circle.position, self->balls[ballIndex].velocity)); CollisionResolver_addObject(self->resolver, (CollisionObject *) &self->balls[ballIndex].circle); } memcpy(self->lastBalls, self->balls, sizeof(self->balls)); CollisionResolver_resolveAll(self->resolver); self->runLoop = FixedIntervalRunLoop_create(FRAME_INTERVAL, FRAME_INTERVAL * 0.375f, run, self); self->frameCount = 0; EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_KEY_DOWN), keyDown, self); EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_BACKGROUNDED), backgrounded, self); EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_FOREGROUNDED), foregrounded, self); EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_RESIZED), resized, self); EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_DRAW), draw, self); Matrix4x4f matrix = Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, -12.0f * g_viewRatio, 12.0f * g_viewRatio, -12.0f, 12.0f, -1.0f, 1.0f); call_virtual(setProjectionMatrix, self->shaderConfigurationLine, matrix); call_virtual(setProjectionMatrix, self->shaderConfigurationText, matrix); Shell_redisplay(); } void BouncingBallScreen_deactivate(BouncingBallScreen * self, Screen * nextScreen, const char * transitionName) { call_virtual(dispose, self->resolver->objectIO->movingObjects[0]); CollisionResolver_dispose(self->resolver); FixedIntervalRunLoop_dispose(self->runLoop); self->runLoop = NULL; EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_KEY_DOWN), keyDown, self); EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_BACKGROUNDED), backgrounded, self); EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_FOREGROUNDED), foregrounded, self); EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_RESIZED), resized, self); EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_DRAW), draw, self); }