Oh alright.
THE BASICS
The general idea is that your walls consist of line segments, rather than tiles. You can still have tiles to draw your graphics... but the map that the ingame objects interact with is a bunch of line segments.
Likewise, objects are defined by the lines of their bounding boxes (assuming you want objects to be rectangular, which for this example I'm going to assume is the case).
When you move, you no longer have to check tiles to see if you overlap them. Instead, you project lines from each of the object's corners in the direction they're moving. If any of those lines intersect with a wall, you know there is a collision, and you only allow movement up to the point of intersection.
This is really hard to illustrate in ASCII graphics... but I'm going to try
Fig 1:
O is where the object is currently
? is where the object is moving to (position + velocity)
=,  are walls
1 2 3 4 5 6 7 8 9 10

OOO 
OOO 
OOO 



 ???
 ???
 ???
==========
 
What you'd do is project a line from each corner of the 'O' in the direction of the movement vector
Fig 2:
. = the projected line
X = where the projected line intersects with a wall
1 2 3 4 5 6 7 8 9 10

OOO 
OOO 
OOO 
.. 
.. 
..
X. ???
 ..??
 ?..
==========
 
Since there's an intersection here, you know the object is hitting a wall. So to "eject" it, you simply scale the movement vector back so it doesn't exceed the intersection point. In this example I'm only doing the lowerright corner... but in practice you'd have to do all 4 corners (or 3 if you want to get sneaky  or even down to 2 if you're only moving along 1 axis ... but I digress)
In the end... stopping movement at the intersection point has pretty much perfect results:
Fig 3:
~ = the previous object position
O = current object position
? = attempted object position
1 2 3 4 5 6 7 8 9 10

~~~ 
~~~ 
~~~ 
.. OOO
.OOO
OOO
X. ???
 ..??
 ?..
==========
 
Reasons why this is better than tilebased collision:
#1) There are usually going to be fewer nearby "lines" to check against than there are going to be tiles. So it can perform better
#2) Tunnelling is impossible. Walls can be paper thin, objects can be any size, and you can move as fast as you like  you will never tunnel through a wall.
#3) Slopes are a nonissue. Angled lines or straight lines don't matter... intersection code for all of them is the same.
#4) You get "perfect" diagonal movement... whereas with the tile approach you have to sort of "zigzag" across the map because you can only move 1 axis at a time.
THE "STABBING" PROBLEM
While the above is pretty simple and covers most cases, it does not cover
all cases. Only projecting from the corners of the object means that you'll only collide with a wall when your corner hits one. It doesn't handle when your SIDE hits one. I call this getting "stabbed" by a wall. Example:
1 2 3 4 5

OOOO ????
OOOO ?===========
OOOO ???
OOOO ?===========
OOOO ????
 
Clearly we're moving into a wall here. However our collision detection won't catch it because none of the object's corners will intersect a wall line. The wall is "stabbing" the object between its corners.
To solve this, we need to project a line from the
corners of the walls in the opposite direction of the movement vector... and check collision with the lines of the object. Doing that here:
1 2 3 4 5

OOOO ????
OOOO ?===========
OOOO ???
O..X...===========
OOOO ????
 
In this example, projecting a line from the bottomleft wall corner
towards the object, it shows us that the projected line intersects with the rightside wall of the object. So again... all you have to do is scale the movement vector back so it does not exceed this collision point:
1 2 3 4 5

~~~OOOO???
~~~OOOO===========
~~~OOOO??
~..OOOO=========== (I dont know if this diagram is any clearer....
~~~OOOO??? it's kind of messy.)
 
THE MATH
The key to making this approach work is being able to get the intersection point (if any) of two line segments. And quick. This might seem easy... but if you actually try to start coding it you might realize it's a tougher problem than you thought. Especially if you want it to be precise.
But when you know the trick to it, it's pretty simple.
Let's say we have 4 points forming 2 lines. Points A and B form line AB, and points C and D form line CD. We want to find where these two lines intersect. We'll also say that the lines are inclusive of A/C but not of B/D.
First, let's take an interesting twist on how to represent points on a line. Let's scale it back so that we can deal with a normalized (scaled to 1.0) value. We can do this by saying any point 'P' on line AB can be represented by the following formula:
With this formula... 'k' becomes a normalized value:
When k=0.0, P=A
When k=1.0, P=B
when k=0.5, P = halfway between A and B
if k is <0 or >=1, then P does not lie on line AB.. but is to either side of it.
We can do the same with line CD:
Here, 'm' is our normalized value. Again,
0 <= m < 1
must be true for Q to fall on line segment CD
Now for the interesting bit. At the point of intersection, both lines share the exact same point (the intersection point). So at that point... P=Q.
This also means that at the intersection point...
1 2

P = Q
((B  A) * k) + A = ((D  C) * m) + C
 
A, B, C, and D are all known values here. So the only unknowns are k and m. If we solve this equation for k and m, we can find the collision point through some simple calculations!
I'll spare you the work of actually solving these. The actual formulas are below:
1 2 3 4 5 6 7 8

to simplify, I am using shorthand:
DCy = (D.y  C.y)
BAx = (B.x  A.x)
etc
k = (DCy*CAx  DCx*CAy) / (DCy*BAx  DCx*BAy)
m = (ABy*CAx  ABx*CAy) / (DCy*BAx  DCx*BAy)
 
Notice:
 The denominators for both are identical
 They seem to follow a pattern: (Fy * Gx  Fx * Gy)
Let's be cute and put that pattern in a function. Let's call it "perpDot" (the name has significance, but nevermind that for now):

double perpDot(const Vector& a, const Vector& b) { return (a.y*b.x)  (a.x*b.y); }
 
The formulas then become simplified further:
1 2

k = perpDot( DC, CA ) / perpDot( DC, BA )
m = perpDot( BA, CA ) / perpDot( DC, BA )
 
And with that, it becomes simple to find both k and m... and therefore simple to find the intersection point of two line segments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

bool linesIntersect( const Vector& A, const Vector& B
const Vector& C, const Vector& D )
{
Vector BA = BA;
Vector DC = DC;
// 'f' is the denominator in the above formula
double f = perpDot(DC,BA);
if(f == 0) // can't divide by zero. This also means (by definition of perpDot which I might
return false; // explain later) the lines are parallel and therefore have no intersection point
Vector CA = CA;
double k = perpDot(DC,CA) / f;
if(k < 0) return false; // remember, 0 <= k < 1 must be true for P
if(k >= 1) return false; // to fall within the line segment
double m = perpDot(BA,CA) / f;
if(m < 0) return false; // same with m
if(m >= 1) return false;
return true;
}
 