As I mentioned in the comments I would go for vector math as its simplifies things and get rid of the goniometrics...
I represent wall as a line (2 endpoints) and object as set of points. As I test only object point versus wall collision then the walls should form convex polygon (because concave tooth might hit object edge instead of vertex first).
I modified these examples:
Into this C++/VCL bouncing example:
//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#include "GLSL_math.h"
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main;
//---------------------------------------------------------------------------
vec2 acc = vec2(0,+9.81); // global acceleration
float bouncyness = 0.95; // global bouncyness effectivity
float friction = 0.001; // air friction k*vel^2 / sec
float scale = 10.0; // time warp
//---------------------------------------------------------------------------
class wall
{
public:
vec2 p0,p1; // end points
vec2 t,n; // tangent, normal
float d; // signed |max| displacement of object in normal direction (used in update)
wall() { p0=vec2(0,0); p1=p0; t=p0; n=p0; }
wall(wall& a) { *this=a; }
~wall() {}
wall* operator = (const wall *a) { *this=*a; return this; }
//wall* operator = (const wall &a) { ...copy... return this; }
void set(const vec2 &q0,const vec2 &q1)
{
p0=q0;
p1=q1;
t=normalize(p1-p0);
n.x=-t.y; n.y=+t.x;
}
void draw(TCanvas *can)
{
can->MoveTo(p0.x,p0.y);
can->LineTo(p1.x,p1.y);
}
};
//---------------------------------------------------------------------------
const int obj_pnts=5; // points of object
class obj
{
public:
vec2 p[obj_pnts]; // vertexes relative to p0
vec2 pos; // center position
vec2 vel; // center velocity
obj() { set(vec2(0,0),0.0,0.0); vel=vec2(0,0); }
obj(obj& a) { *this=a; }
~obj() {}
obj* operator = (const obj *a) { *this=*a; return this; }
//obj* operator = (const obj &a) { ...copy... return this; }
void set(const vec2 &q,float a,float r) // center, rotation, radius
{
pos=q;
int i;
float da=2.0*M_PI/float(obj_pnts);
for (i=0;i<obj_pnts;i++){ p[i]=vec2(r*cos(a),r*sin(a)); a+=da; }
}
void draw(TCanvas *can)
{
int i;
vec2 q;
q=pos+p[obj_pnts-1]; can->MoveTo(q.x,q.y);
for (i=0;i<obj_pnts;i++){ q=pos+p[i]; can->LineTo(q.x,q.y); }
}
};
//---------------------------------------------------------------------------
const int walls=4; // number of walls
wall w[walls]; // walls (should form a convex polygon as only vertexes of object are tested)
obj o; // bouncing object
//---------------------------------------------------------------------------
void update(double dt)
{
int i,j,e;
float d,dd;
vec2 p0,p1,q0,q1,pos0,vel0;
pos0=o.pos; // store last position for latter
vel0=o.vel; // store last velocity for latter
// Newton/d'Lambert simulation on objects center
o.vel+= acc*dt;
o.vel-=o.vel*length(o.vel)*friction*dt; // k*vel^2 air friction
o.pos+=o.vel*dt;
// collision test init
for (j=0;j<walls;j++) w[j].d=0.0; // reset wall penetration
// collision test
for (e=0,i=0;i<obj_pnts;i++) // test all points
{
p0= pos0+o.p[i]; // position before update
p1=o.pos+o.p[i]; // position after update
for (j=0;j<walls;j++) // test all walls
{
q0=p0-w[j].p0; // relative position to wall start point before update
q1=p1-w[j].p0; // relative position to wall start point after update
// convert to wall local coordinates
q0=vec2(dot(q0,w[j].t),dot(q0,w[j].n));
q1=vec2(dot(q1,w[j].t),dot(q1,w[j].n));
// collision?
if (q0.y*q1.y<=0.0)
{
e=1; // flag that colliosn exist
d=fabs(q1.y); // perpendicular penetration of wall
if (fabs(w[j].d)<d) w[j].d=q1.y; // store max penetration for each wall
}
}
}
// if collison found update pos and vel of object
if (e)
{
// compute displacement direction as avg of wall normals (to handle multiple walls colision)
for (p0=vec2(0,0),j=0;j<walls;j++)
{
if (w[j].d>=0.0) p0+=w[j].n;
else p0-=w[j].n;
}
p0=normalize(-p0); // normal (outward)
p1=vec2(-p0.y,+p0.x); // tangent
// compute displacement so it matches penetration with new normal p0
for (d=0.0,j=0;j<walls;j++)
{
dd=fabs(w[j].d)/dot(p0,w[j].n);
if (d<dd) d=dd;
}
o.pos+=p0*(d+0.1); // displace slightly more to avoid collision in next iteration
// use displacement direction as normal to surface
p0=normalize(-p0); // normal
p1=vec2(-p0.y,+p0.x); // tangent
// convert vel to wall relative
o.vel=vec2(dot(o.vel,p1),dot(o.vel,p0));
// convert back and reflect normal direction
o.vel=(o.vel.x*p1)-(o.vel.y*bouncyness*p0);
// full stop if slow speed to avoid accuracy problems
if (length(vel0)<0.1*length(acc)*scale)
{
o.vel=vec2(0,0);
o.pos=(o.pos+pos0)*0.5;
}
}
}
//---------------------------------------------------------------------------
void TMain::draw()
{
if (!_redraw) return;
// clear buffer
bmp->Canvas->Brush->Color=clBlack;
bmp->Canvas->FillRect(TRect(0,0,xs,ys));
// walls
bmp->Canvas->Pen->Color=clWhite;
for (int i=0;i<walls;i++) w[i].draw(bmp->Canvas);
// objects
o.draw(bmp->Canvas);
// render backbuffer
Main->Canvas->Draw(0,0,bmp);
_redraw=false;
}
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
{
bmp=new Graphics::TBitmap;
bmp->HandleType=bmDIB;
bmp->PixelFormat=pf32bit;
pyx=NULL;
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender)
{
if (pyx) delete[] pyx;
delete bmp;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender)
{
xs=ClientWidth; xs2=xs>>1;
ys=ClientHeight; ys2=ys>>1;
bmp->Width=xs;
bmp->Height=ys;
if (pyx) delete[] pyx;
pyx=new int*[ys];
for (int y=0;y<ys;y++) pyx[y]=(int*) bmp->ScanLine[y];
_redraw=true;
// walls
vec2 p0,p1,p2,p3;
p0=vec2(0.1*xs,0.5*ys);
p1=vec2(0.3*xs,0.1*ys);
p2=vec2(0.9*xs,0.5*ys);
p3=vec2(0.7*xs,0.9*ys);
w[0].set(p0,p1);
w[1].set(p1,p2);
w[2].set(p2,p3);
w[3].set(p3,p0);
// objects
o.set(vec2(0.5*xs,0.5*ys),30.0*M_PI/180.0,0.05*xs);
update(0.0);
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormPaint(TObject *Sender)
{
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender)
{
update(0.04*scale);
_redraw=true;
draw();
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y)
{
// set velocity towards mouse with magnitude based on x resolution of window
o.vel=normalize(vec2(X,Y)-o.pos)*0.2*xs;
}
//---------------------------------------------------------------------------
This is a single form App with single timer in it firing every 40ms calling functions update
and draw
. On resize of app I configure the walls and object start positon and orientation. On click (mouse down) event I set the object speed so it flies towards mouse...
Now the only important stuff in here is the update(dt)
function which simulate the physics (updates velocity and position of object), then it test if any point of object collide with any wall and remember for each wall how much in which side a penetration occurs (remember the biggest one).
After all this I simply compute avg normal of all collided walls in outwards direction ("go above ground") and recompute displacement so it matches the original one with actual wall normal. This complication I added because when you hit more walls at once the dispalcement on per wall basis in some cases leads to moving the object below ground...
And at last once the speed is close to zero I force the speed to zero and displace a bit to avoid accuracy problems which would lead to falling of object partially below ground.
In case you got your walls defined with strict winding rule you could use normal direction instead of these tweaks however in such case the walls would be just single sided...
Here preview of few clicks on mouse (note my global acceleration is set with gravity downwards):

vector math used is with GLSL like syntax you can use any vector math you have at your disposal like GLM or rewrite the math into x,y
pairs yourself. I use only basic operations like vector size, vector normalize to unit size, dot product, multiply vector by constant and +/- vectors ...
The only goniometrics used is just to generate the object points (you can change number of points with the constant obj_pnts
from line,triangle,square,... to whatever circle like surface ...
All the collision testing and reflection of speed is based on normal and tangent vectors along with dot product operation. You know if you have unit vector n
then dot(a,n)
will give you perpendicular projection of a
vector into n
which can be used as penetration of wall and also to convert between local and global coordinates of wall and world without any heavy math...
[Edit1] I tweaked the code a bit to enhance the robustness
I changed the handling of stuck/slow speed problems by detecting state when more than one wall is hit at once and ignore the tangent speed in such case so the object bounce always straight upwards (relative to obstacle) with enough speed to avoid stuck and accuracy problems in next iteration. This allows to get rid of the slow speed stuck state (causing the slow sliding). Also I changed the timewarp behavior (so it does not affect constants)
Here updated code:
//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#include "GLSL_math.h"
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main;
//---------------------------------------------------------------------------
vec2 acc = vec2(0,+9.81); // global acceleration
float bouncyness = 0.80; // global bouncyness effectivity
float friction = 0.01; // air friction k*vel^2 / sec
int twarp = 10; // time warp
//---------------------------------------------------------------------------
class wall
{
public:
vec2 p0,p1; // end points
vec2 t,n; // tangent, normal
float d; // signed |max| displacement of object in normal direction (used in update)
wall() { p0=vec2(0,0); p1=p0; t=p0; n=p0; }
wall(wall& a) { *this=a; }
~wall() {}
wall* operator = (const wall *a) { *this=*a; return this; }
//wall* operator = (const wall &a) { ...copy... return this; }
void set(const vec2 &q0,const vec2 &q1)
{
p0=q0;
p1=q1;
t=normalize(p1-p0);
n.x=-t.y; n.y=+t.x;
}
void draw(TCanvas *can)
{
can->MoveTo(p0.x,p0.y);
can->LineTo(p1.x,p1.y);
}
};
//---------------------------------------------------------------------------
const int obj_pnts=7; // points of object
class obj
{
public:
vec2 p[obj_pnts]; // vertexes relative to p0
vec2 pos; // center position
vec2 vel; // center velocity
obj() { set(vec2(0,0),0.0,0.0); vel=vec2(0,0); }
obj(obj& a) { *this=a; }
~obj() {}
obj* operator = (const obj *a) { *this=*a; return this; }
//obj* operator = (const obj &a) { ...copy... return this; }
void set(const vec2 &q,float a,float r) // center, rotation, radius
{
pos=q;
int i;
float da=2.0*M_PI/float(obj_pnts);
for (i=0;i<obj_pnts;i++){ p[i]=vec2(r*cos(a),r*sin(a)); a+=da; }
}
void draw(TCanvas *can)
{
int i;
vec2 q;
q=pos+p[obj_pnts-1]; can->MoveTo(q.x,q.y);
for (i=0;i<obj_pnts;i++){ q=pos+p[i]; can->LineTo(q.x,q.y); }
}
};
//---------------------------------------------------------------------------
const int walls=4; // number of walls
wall w[walls]; // walls (should form a convex polygon as only vertexes of object are tested)
obj o; // bouncing object
//---------------------------------------------------------------------------
void update(double dt)
{
int i,j,e;
float d,dd;
vec2 p0,p1,q0,q1,pos0,vel0;
pos0=o.pos; // store last position for latter
vel0=o.vel; // store last velocity for latter
// Newton/d'Lambert simulation on objects center
o.vel+= acc*dt;
o.vel-=o.vel*length(o.vel)*friction*dt; // k*vel^2 air friction
o.pos+=o.vel*dt;
// collision test init
for (j=0;j<walls;j++) w[j].d=0.0; // reset wall penetration
// collision test
for (e=0,i=0;i<obj_pnts;i++) // test all points
{
p0= pos0+o.p[i]; // position before update
p1=o.pos+o.p[i]; // position after update
for (j=0;j<walls;j++) // test all walls
{
q0=p0-w[j].p0; // relative position to wall start point before update
q1=p1-w[j].p0; // relative position to wall start point after update
// convert to wall local coordinates
q0=vec2(dot(q0,w[j].t),dot(q0,w[j].n));
q1=vec2(dot(q1,w[j].t),dot(q1,w[j].n));
// collision?
if (q0.y*q1.y<=0.0)
{
e=1; // flag that colliosn exist
d=fabs(q1.y); // perpendicular penetration of wall
if (fabs(w[j].d)<d) w[j].d=q1.y; // store max penetration for each wall
}
}
}
// if collison found update pos and vel of object
if (e)
{
// compute displacement direction as avg of wall normals (to handle multiple walls colision)
// and e is number of hit walls
for (e=0,p0=vec2(0,0),j=0;j<walls;j++)
{
if (fabs(w[j].d)>1e-6) e++;
if (w[j].d>=0.0) p0+=w[j].n;
else p0-=w[j].n;
}
p0=normalize(-p0); // normal (outward)
p1=vec2(-p0.y,+p0.x); // tangent
// compute displacement so it matches penetration with new normal p0
for (d=0.0,j=0;j<walls;j++)
{
dd=fabs(w[j].d)/dot(p0,w[j].n);
if (d<dd) d=dd;
}
o.pos+=p0*(d+0.5); // displace slightly more to avoid collision in next iteration
// use displacement direction as normal to surface
p0=normalize(-p0); // normal
p1=vec2(-p0.y,+p0.x); // tangent
// convert vel to wall relative
o.vel=vec2(dot(o.vel,p1),dot(o.vel,p0));
// convert back and reflect normal direction
if (e>1) // if hit more walls at once
{
o.vel.x=0.0; // disipate tangential speed
d=0.5/dt; // enforce speed upwards
if (o.vel.y<d) o.vel.y=d;
}
o.vel=(o.vel.x*p1)-(o.vel.y*bouncyness*p0);
}
}
//---------------------------------------------------------------------------
void TMain::draw()
{
if (!_redraw) return;
// clear buffer
bmp->Canvas->Brush->Color=clBlack;
bmp->Canvas->FillRect(TRect(0,0,xs,ys));
// walls
bmp->Canvas->Pen->Color=clWhite;
for (int i=0;i<walls;i++) w[i].draw(bmp->Canvas);
// objects
bmp->Canvas->Pen->Color=clAqua;
o.draw(bmp->Canvas);
// render backbuffer
Main->Canvas->Draw(0,0,bmp);
_redraw=false;
}
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
{
bmp=new Graphics::TBitmap;
bmp->HandleType=bmDIB;
bmp->PixelFormat=pf32bit;
pyx=NULL;
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender)
{
if (pyx) delete[] pyx;
delete bmp;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender)
{
xs=ClientWidth; xs2=xs>>1;
ys=ClientHeight; ys2=ys>>1;
bmp->Width=xs;
bmp->Height=ys;
if (pyx) delete[] pyx;
pyx=new int*[ys];
for (int y=0;y<ys;y++) pyx[y]=(int*) bmp->ScanLine[y];
_redraw=true;
// walls
vec2 p0,p1,p2,p3;
p0=vec2(0.1*xs,0.5*ys);
p1=vec2(0.3*xs,0.1*ys);
p2=vec2(0.9*xs,0.5*ys);
p3=vec2(0.7*xs,0.9*ys);
w[0].set(p0,p1);
w[1].set(p1,p2);
w[2].set(p2,p3);
w[3].set(p3,p0);
// objects
o.set(vec2(0.5*xs,0.5*ys),30.0*M_PI/180.0,0.05*xs);
update(0.04);
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormPaint(TObject *Sender)
{
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender)
{
for (int i=0;i<twarp;i++) update(0.04);
_redraw=true;
draw();
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y)
{
// set velocity towards mouse with magnitude based on x resolution of window
o.vel=normalize(vec2(X,Y)-o.pos)*0.2*xs;
}
//---------------------------------------------------------------------------
And preview:
