Sign in to follow this  
mikenovemberoscar

Walking on a terrain in a FPS?

Recommended Posts

Hello, I am new to gamedev.net. I have previous experience in OpenGL and am currently programming a Quake style first person shooter in C. I have an OBJ model loader sorted out and the basic camera movement system. I keep running into problems keeping the camera above the terrain (The terrain is stored as an OBJ mesh, but could change). I have tried many times but none have been successful. What is the way that professional games do it? (e.g. Quake, Call of Duty, Battlefield 1942) What is the real name for this topic? I have tried googling things like 'finding height of player on terrain' but with limited results. What is the way that you think I should do it? P.S. I am working on a mac so I can't load heightmaps using
glaux.h

Share this post


Link to post
Share on other sites
I'd suppose the easiest and straightforward way would be to link the camera to a player which has a bounding box, checked for collisions each frame. In a graph that would equal the camera being a child of player, which has a bounding box or something equivalent to that which does the same trick.

Keeping the camera over the terrain is then "just" a question of doing collision detection and response between the bounds and terrain.

Share this post


Link to post
Share on other sites
You can stick an object on a terrain by using ray tracing. In short you send an line above the player right down and detect where the line intersect with the terrain.

You can do this physics yourself or use a third party libraries, such as ODE, Bullet or Newton to name a few of the most popular.

If you have an OBJ mesh you use the same vertexes/faces as opengl to build the physics object.

Share this post


Link to post
Share on other sites
Yes, I did think of both of these methods, but I have had problems with them. I used to use this code:
float getFloor(Mesh * mesh , float  x, float y, float z )
{

vec3D player;
int cellx = 0;
int celly = 0;

int cell;

float closest_dist = 2000;
int closest_point;

int indexref;
int cv = 0;


cell = (CELL_SIZE * (int) (z/CELL_SIZE)) + (int) (x/CELL_SIZE);

if (cell < 0)
cell = -cell;

printf("cell = %i\n", cell);
printf("celly = %i\n", celly);

player.x = x;
player.y = z;


while (cv <= grid_list[cell].size) // cycles through all cells
{
vec3D p1;
vec3D p2;
vec3D p3;

float p1y;
float p2y;
float p3y;

if (sizeof(grid_list[cell].arr[cv]) == 0)
{
return y;
printf("No triangles in cell...\n");

}
printf("grid_list[%i].arr[%i] = %i\n",cell,cv,indexref);
indexref = grid_list[cell].arr[cv];


p1.x = mesh->verts[indexref]; //
p1.y = mesh->verts[indexref+1]; //
p1.y = mesh->verts[indexref+2]; //

p2.x = mesh->verts[indexref+3]; //
p2.y = mesh->verts[indexref+4]; // GET FIRST TRIANGLE ACCORDING TO indexref WHICH IS ONE OF THE TRIANGLES THAT OCCUPY THIS CELL
p2y = mesh->verts[indexref+5]; //

p3.x = mesh->verts[indexref+6]; //
p3.y = mesh->verts[indexref+7]; //
p3.y = mesh->verts[indexref+8]; //

printf("Getting triangle of index %i with points (%f,%f,%f)(%f,%f,%f)(%f,%f,%f)\n",indexref,p1.x,p1.y,p1.z,p2.x,p2.y,p2.z,p3.x,p3.y,p3.z);


if (pointInTriangle(p1, p2, p3, player) == 1)
{
// player is inside this triangle
printf("Player inside triangle...\n");
return (p1.y + p2.y + p3.y)/3;

}
else
{
cv++;
}


}


printf("There was no triangle...\n");
// there was no triangle :(
return y;
// printf("\n\n --BEGINNING NEW GETFLOOR SESSION-- \n\n");

}


int pointInTriangle(vec3D A, vec3D B, vec3D C, vec3D P)
{

/*
p1
/ / /____ p2 p3
*/



vec2D point;

vec3D AB;
vec3D BC;
vec3D AC;

vec3D AP;
vec3D BP;
vec3D CP;

vec3D cp1;
vec3D cp2;
vec3D cp3;


AP.x = P.x - A.x;
AP.y = P.y - A.y;


BP.x = P.x - C.x;
BP.y = P.y - C.y;

AP.x = P.x - A.x;
AP.y = P.y - A.y;

AB.x = B.x - A.x;
AB.y = B.y - A.y;

BC.x = B.x - C.x;
BC.y = B.y - C.y;

AC.x = C.x - A.x;
AC.y = C.y - A.y;




cp1 = crossprodVec3D(AP, AB);
cp2 = crossprodVec3D(BP, BC);
cp3 = crossprodVec3D(CP, AC);

if ( (cp1.x < 0 && cp2.x < 0 && cp3.x < 0) && (cp1.y < 0 && cp2.y < 0 && cp3.y < 0) )
return 1;
else if( (cp1.x > 0 && cp2.x > 0 && cp3.x > 0) && (cp1.y > 0 && cp2.y > 0 && cp3.y > 0) )
return 1;
else
return 0;



}








/*
13 14 15 16
9 10 11 12
5 6 7 8
1 2 3 4



^^ playerpos positions^^

*/



int hash_func( x, y, n)
{
return (x*2185031351ul ^ y*4232417593ul) % n;
}

int getCell(float x, int width)
{
return ((int)(x/width))*width;
}



void setupLvl(Mesh * mesh)
{

float x1; // x of first point of triangle
float y1; // y of first point of triangle
float z1; // z of first point of triangle

float x2; // x of second point of triangle
float y2; // y of second point of triangle
float z2; // z of second point of triangle

float x3; // x of third point of triangle
float y3; // y of third point of triangle
float z3; // z of third point of triangle


int cell1x; // cell that the first point belongs in
int cell2x; // cell that the second point belongs in
int cell3x; // cell that the third point belongs in
int cell1z; // cell that the first point belongs in
int cell2z; // cell that the second point belongs in
int cell3z; // cell that the third point belongs in




int csizetemp=0; // size of temporary array



int x; // used for 'for' while loops
int y; // ditto
int xcomy;

int cellhash;

int counterx = 0;
int countery = 0;

int occupiedCells[CELL_WIDTH][CELL_WIDTH] [256]; // first two dimensions are the cell, second is the actual list of triangles for that cell
int occupiedCellCount[CELL_WIDTH][CELL_WIDTH]; // this is the number of cells a triangle occupies in the cell denoted by the first dimension

int vhash;

int indexref = 0; // triange index number

// formula is (int)(x/cellsize) *cellsize 10*

while (mesh->verts[indexref] != '\0')
{
x1 = mesh->verts[indexref]; //
y1 = mesh->verts[indexref+1]; //
z1 = mesh->verts[indexref+2]; //

x2 = mesh->verts[indexref+3]; //
y2 = mesh->verts[indexref+4]; // GET FIRST TRIANGLE
z2 = mesh->verts[indexref+5]; //

x3 = mesh->verts[indexref+6]; //
y3 = mesh->verts[indexref+7]; //
z3 = mesh->verts[indexref+8]; //

printf("Getting triangle of index %i with points (%f,%f,%f)(%f,%f,%f)(%f,%f,%f)\n",indexref,x1,y1,z1,x2,y2,z2,x3,y3,z3);

//x = 0;

// WE NOW HAVE TO SORT A LIST OF ALL THE CELLS THE TRIANGLE OCCUPIES

cell1x = getCell(x1,CELL_WIDTH);
cell1z = getCell(z1, CELL_WIDTH);

cell2x = getCell(x2,CELL_WIDTH);
cell2z = getCell(z2, CELL_WIDTH);

cell3x = getCell(x3,CELL_WIDTH);
cell3z = getCell(z3, CELL_WIDTH);

if (cell1x < 0)
cell1x = -cell1x;
if (cell1z < 0)
cell1z = -cell1z;

if (cell2x < 0)
cell2x = -cell2x;
if (cell2z < 0)
cell2z = -cell2z;

if (cell3x < 0)
cell3x = -cell3x;
if (cell3z < 0)
cell3z = -cell3z;

if (occupiedCells[cell1x][cell1z][(occupiedCellCount[cell1x][cell1z])] == '\0')
return 2;

occupiedCellCount[cell1x][cell1z]++;
occupiedCells[cell1x][cell1z][(occupiedCellCount[cell1x][cell1z])] = indexref; // adds this triangle to the occupied cell list of this grid cell.


if (cell1x != cell2x && cell1z != cell2z) // if the next point is in the same triangle there's no point in adding it...
occupiedCells[cell2x][cell2z][occupiedCellCount[cell2x][cell2z]] = indexref; // adds this triangle to the occupied cell list of this grid cell.


if (cell3x != cell2x && cell3z != cell2z && cell1x != cell3x && cell1z != cell3z)
occupiedCells[cell3x][cell3z][occupiedCellCount[cell3x][cell3z]] = indexref; // adds this triangle to the occupied cell list of this grid cell.


while (x < CELL_GRID) // loop through all cells
{
cell1x = ((int)(x1/CELL_WIDTH))*CELL_WIDTH; // pick cell that first point is in
cell1y = ((int)(y1/CELL_WIDTH))*CELL_WIDTH;

if (cell1 < 0)
cell1 = -cell1;
if (cell1 == x) // the first point is inside the cell we are checking...
{
NSBeep();
occupiedCells[x][occupiedCellCount[x]] = indexref; // ... so add it to the list of cells occupied by this triangle!
occupiedCellCount[x]++; // increment the count (number of cells that the triangle occupies)
}

cell2 = (CELL_SIZE * (int) (z2/CELL_SIZE)) + (int) (x2/CELL_SIZE);
if (cell2 < 0)
cell2 = -cell2;
if (cell2 == x && cell1 != x) // the second point is inside the cell we are checking...
{
occupiedCells[x][occupiedCellCount[x]] = indexref; // ... so add it to the list of cells occupied by this triangle!
occupiedCellCount[x]++; // increment the count (number of cells that the triangle occupies)
}

cell3 = (CELL_SIZE * (int) (z3/CELL_SIZE)) + (int) (x3/CELL_SIZE);
if (cell3 < 0)
cell3 = -cell3;
if (cell3 == x && cell2 != x) // the third point is inside the cell we are checking...
{
occupiedCells[x][occupiedCellCount[x]] = indexref; // ... so add it to the list of cells occupied by this triangle!
occupiedCellCount[x]++; // increment the count (number of cells that the triangle occupies)
}


x++;
}

// now we are finished with this point and have sorted the triangle into occupied cells

y = 0;
x = 0;



while(x < CELL_WIDTH) // loops through cell's x values
{
while (y < CELL_WIDTH)
{

xcomy = (y * CELL_WIDTH) + x;


grid_list[xcomy].arr = occupiedCells[x]; // resizes the grid_list array for this cell to the number of triangles that are in the cell.
grid_list[xcomy].size = occupiedCellCount[x]; // not releated to above!!!

// now copy array from occupiedCells[x] to grid_list[x].arr

grid_list[xcomy].arr = (int *)calloc(occupiedCellCount[x][y], sizeof(int));
memcpy(grid_list[xcomy].arr, occupiedCells[x][y], occupiedCellCount[x][y]);

y++;

}

y = 0;


x++;

}

// now we're finished with occupiedCells!!!!! (this is deleted anyway at the end of the function
x = 0;
indexref += 9;


}
// at this point we have a nice list (occupiedcells[]) of cells that the triangle occupies!!!!! And, the list doesn't contain any duplicates!





}




I think the problem is with the point in triangle function but I can't find another way of doing this? Please feel free to use this code.

Share this post


Link to post
Share on other sites
im working on an fps game as well. well not just a shooter due to sword play, but you get the point, my approach was to set the camera to the players coordinates, with the y a few units above the player, and for collision, all i did was:



if (player.y <= obj.y){
player.y = obj.y;
}


if your using some other other floor effect like me such as tiling, i did this:



if (player.y <= obj.y && player.x <= obj.x + obj.length/2 && player.x >= obj.x - obj.length/2 && player.z <= obj.z + obj.width/2 && player.z >= obj.z - obj.width/2){
player.y = 0bj.y;
}


if you are using a height map, then average the y of the vertices surrounding the player or use another method and convert the vertices to quads and do the like.


please note that there is more to my code. i have a boolean jump status so if the player touches the ground, i set it to false to enable jumping again.

Share this post


Link to post
Share on other sites
I've always used something like:

xCell = floor(pos.x/cellSize);
zCell = floor(pos.z/cellSize);

this gives you the position in the grid, then to get the height at the specific part of the cell you're in, use something like (psudocode)

xHeight = heightOfLeftSideOfCell + ( (heightOfRightSideOfCell - heightOfLeftSideOfCell) * percentThroughCell);

do the same for the zHeight using heightOfBackSideOfCell, and heightOfFrontSideOfCell, then take the average of xHeight and zHeight, or the min.

Share this post


Link to post
Share on other sites
Well the data for the model has to be stored somewhere, right? There's a fair chance that it's simply a square grid. Are you loading the .obj yourself? or using a model loader of some sort?

Looking at the code you posted, it looks like you're trying to pinpoint an exact coordinate, when a much simpler task would be to pinpoint the grid cell you're in, and iterpolate to get the height.

for instance, say you use a linear list of triangles to represent a 2D terrain grid, with 2 triangles per quad.

now say you want to find the cell at (25x, 34z) (completely arbitrary by the way). Assuming you're going left to right, far to near, the triangles that make this cell will be found at
triangleList [ (trisPerQuad * gridWidth * 34) + (trisPerQuad * 25)] and
triangleList [ (trisPerQuad * gridWidth * 34) + (trisPerQuad * 25) + 1]. you then get the heights from these triangles, depending on how they're ordered and wound, and you interpolate and average between them.


EDIT: Keep in mind that's all untested, ballpark stuff, but it gives you a rough idea how to go about that.

Share this post


Link to post
Share on other sites
One of the easier ways to accomplish this is by casting a ray down from the camera position towards the ground and get the distance to the ground from that position. If that distance is smaller than the wanted distance you move the player up, if it's bigger than you move the player down. So what you'd have to do is something like this:


for every triangle in the map
{
//project camera position in the 0,-1,0 direction on the triangle plane
if the point lies in the triangle
{
//check if this is the smallest or the first distance you've found and if so store it
}
}

//move camera in y direction by 'wanted distance from ground' - 'distance found'


this will put your camera on the 'ground', you could put a cap on the maximum distance you can go down to simulate gravity.

A link you will find interesting is this one. It's more advanced and harder to implement but that's kinda how it's done in most FPS games. It also contains some code to project a point on a plane.

Share this post


Link to post
Share on other sites
Quote:
Original post by mikenovemberoscar
Yes, I did think of both of these methods, but I have had problems with them. I used to use this code:
*** Source Snippet Removed ***

I think the problem is with the point in triangle function but I can't find another way of doing this? Please feel free to use this code.


First problem is that you appear to be assigning p1.x, p1.y, and p1.y (the last one should be p2.z, right?). Same for p2 and p3. Once you fix this, though, you have another problem.

Your getFloor function returns the average heights of the triangle vertices regardless of *where* the point is inside the triangle. When the point is near the vertex of maximum height, your getFloor function will underestimate the height.

You should compute the barycentric coordinates of the point P relative to the triangle vertices V0, V1, and V2. That is, (P.x,P.y) = b0*(V0.x,V0.y) + b1*(V1.x,V1.y) + b2*(V2.x,V2.y) where b0 + b1 + b2 = 1. P is inside the triangle when b0 >= 0 and b1 >= 0 and b2 >= 0. The height at P is P.z = b0*V0.z + b1*V1.z + b2*V2.z.

Share this post


Link to post
Share on other sites
I wanted to mention one more thing. Ray casting was suggested to determine on which triangle the character is standing. If you have a large set of triangles, you do not want to simply iterate over all triangles to search for the containing triangle.

Instead, take advantage of space-time coherency. Given an index array for the triangles, {i0,i1,i2,i3,i4,i5,...}, where triangle 0 is <V(i0),V(i1),V(i2)>, triangle 1 is <V(i3),V(i4),V(i5)>, and so on. It is relatively easy to build an adjacency list, {a0,a1,a2,...}. a0 is the index of the triangle that is adjacent to the triangle 0 edge <i0,i1>. a1 is the index of the triangle that is adjacent to the triangle 0 edge <i1,i2>. a2 is the index of the triangle that is adjacent to the triangle 0 edge <i2,i0>. If there is no adjacent triangle, set the adjacent index to -1.

If the character is currently in triangle t and the character moves, start the search at triangle t. If the motion is small relative to the size of the triangle, the probability is large that you are still in triangle t. However, if the character is not in triangle t, use a breadth-first search for the containing triangle by using the adjacency index list. The expected time for the search is O(1) [constant time] assuming the motion is small relative to the triangle size.

Share this post


Link to post
Share on other sites
Quote:
There's a fair chance that it's simply a square grid


It's not. I am trying to make the engine work as best as possible with any OBJ file so the landscape model includes houses, etc, etc.

Quote:
f that distance is smaller than the wanted distance you move the player up, if it's bigger than you move the player down


I am trying to perform the function as little as possible, that is, if the player hasn't moved the function isn't called.
Thanks for the link, It seems to be just what I'm looking for.

Dave Eberly, I am finding the average to keep the function as simple as possible for now; adding in the interpolation just complicates the already complex situation. It is quite difficult to arrange the triangles in this way; but I will keep that in mind.

Another problem is that what happens with two-storey buildings? Just searching x and z won't be enough!

Share this post


Link to post
Share on other sites
You need to consider the building as a mesh, and build a BVH (bounding volume hierarchy) around it. That will return you the triangle close to your character. Then you do collision detection, using a sphere, a box, or some simple bounding volume around your character.

Share this post


Link to post
Share on other sites
Quote:
Original post by mikenovemberoscar
Another problem is that what happens with two-storey buildings? Just searching x and z won't be enough!


Well, your subject title is "Walking on a terrain ..." :)

When you throw in other objects such as buildings, then you have a problem that requires collision avoidance and path finding. I described such a system in "3D Game Engine Design, 2nd edition" (section 8.5). I had implemented this for a contract (so I cannot make the code public) and it worked well. You could walk around the terrain, into and out of buildings, stay on floors, stairs, etc.

Share this post


Link to post
Share on other sites
Do you suggest I write my own level editor, so I can place a 'terrain' then add houses, etc? Or have two seperate OBJ meshes, one for terrain and one for buildings?

When I wrote terrain I actually meant anything, but I couldn't come up with a better word.

My original method was finding the closest triangle, so doing that shouldn't be a problem.

Share this post


Link to post
Share on other sites
Quote:
Original post by mikenovemberoscar
Do you suggest I write my own level editor, so I can place a 'terrain' then add houses, etc? Or have two seperate OBJ meshes, one for terrain and one for buildings?

When I wrote terrain I actually meant anything, but I couldn't come up with a better word.

My original method was finding the closest triangle, so doing that shouldn't be a problem.


"Terrain" means something specific to me. And it is important to know whether the terrain mesh is a height field (only one triangle can be intersected by a vertical ray) or otherwise (you might have an overhanging cliff).

What I am suggesting is that staying on a height-field terrain is relatively simple. When you add world objects, the scene is more complicated and the problems you need to solve are more difficult.

For a terrain (height field with z-axis the up vector), you can cast a vertical ray and iterate over all triangles to determine the triangle that contains your character's (x,y) location. Then you can adjust the height (z-value) so that the character is standing on the triangle.

Consider now a character that is about to ascend a flight of stairs. You want the character to stay on the stairs as he ascends. You cast a ray to find out that it intersects *three* triangles. A stair triangle, a floor triangle (the floor is below the stairs), and a ceiling triangle (the ceiling is above the stairs). Which triangle do you select? Well, you have knowledge that the character is about to ascend the stairs, so you'll select the stair triangle.

Now let's make it slightly more difficult. The stairs are exposed, say, a wrought-iron fire escape. The character is not standing at the base of the stairs; rather, he wants to walk *under* the stairs. You cast a ray and find that it intersects three triangles (floor, stairs, ceiling). Which triangle do you select? Once again, you need knowledge of what the character wants to do. In this case, you'll select the floor triangle.

The problem is that your mesh is not a height field. And keeping the character on the correct triangles as he moves requires more than just geometric information.

Share this post


Link to post
Share on other sites
Quote:
height field


Aha, I was looking for a term to describe that!

Quote:
The problem is that your mesh is not a height field. And keeping the character on the correct triangles as he moves requires more than just geometric information.


So what do you suggest I do then?

Share this post


Link to post
Share on other sites
Quote:
Original post by Dave Eberly
Consider now a character that is about to ascend a flight of stairs. You want the character to stay on the stairs as he ascends. You cast a ray to find out that it intersects *three* triangles. A stair triangle, a floor triangle (the floor is below the stairs), and a ceiling triangle (the ceiling is above the stairs). Which triangle do you select? Well, you have knowledge that the character is about to ascend the stairs, so you'll select the stair triangle.

Now let's make it slightly more difficult. The stairs are exposed, say, a wrought-iron fire escape. The character is not standing at the base of the stairs; rather, he wants to walk *under* the stairs. You cast a ray and find that it intersects three triangles (floor, stairs, ceiling). Which triangle do you select? Once again, you need knowledge of what the character wants to do. In this case, you'll select the floor triangle.

The problem is that your mesh is not a height field. And keeping the character on the correct triangles as he moves requires more than just geometric information.


This problem(s) is pretty easily solved by shortening the ray used for casting. Typically a ray casted from the character's pelvic bone down to at most an inch below his feet will be sufficient. You then place the character at the first hit always.

This will lift your character up onto anything reasonable in height. You can choose to reject or otherwise handle step changes over a threshold.

If the ray does not intersect something, the character has left the ground and enters a physics based falling mode.

Lastly, ray cast against everything. Often physics engines are highly optimized ray casting machines, but ideally that would happen at the graphics detail level. It doesn't make sense when bullets or characters are hovering over the graphics because they're colliding with the physics primitive.

Share this post


Link to post
Share on other sites
So the code is essentialy what I have been trying to get, only by checking distance to triangle then if the distance is less that X metres (my game units are metres) BELOW the player? I had code which nearly accomplished this bar the point in triangle function. Has anyone got a working one?

Share this post


Link to post
Share on other sites
The odds of you getting a response to that are pretty slim. There's two very seperate things here. Ray casting, and character control. The ray casting part is a trivial math and data organization exercise that you'll benefit from doing yourself.

The ray casting should accept a world space ray (origin and extent) and return the first intersection point along that ray.

Then you simply put your character on this intersection point.

Math::Ray ray( characterPelvicPos, characterFeetPos - characterPelvicPos );
RayCaster caster( ray );

world.RayCast( caster );

if( caster.HitSomething )
{
Vec3 worldPos = ray.Evaluate( caster.TimeOfFirstHit );
MoveCharactersFeetToPos( worldPos );
}


The time of hit is refering to a zero to one value along the ray. So Evaluate looks like this:

Vec3 Ray::Evaluate( float t ) { return Origin + Extent * t; }

Share this post


Link to post
Share on other sites
Right, so is this OK:

(pseudo-code)
SetupLevel()


sort triangles into XZ grid cells





GetFloor()


1.Get player's position

2.Get player's cell

3.Loop through triangles casting a ray downwards and getting the distance to each ray collision

4.Get the triangle with the smallest distance

5.Interpolate heights from three points




If this is OK, there is one main area of problem:
1. Step Three in Getfloor : how do I cast a ray? Can anyone help with a point in triangle function?

Share this post


Link to post
Share on other sites
You'll need to check each triangle, as you said. But the ray vs triangle intersection starts first as a ray vs plane intersection to get the point on the ray, then checks are done to check that the point lies within the triangle.

Share this post


Link to post
Share on other sites
Thanks, but I tried to get a point in triangle (and ray-plane for that matter) function but couldn't find any guides. Anyone know of any tutorials/guides/examples?
Another problem was sorting the triangles into grid cells at the start of the program. How can I do a triangle - square test?

Share this post


Link to post
Share on other sites
Quote:
Original post by bzroom
If the ray does not intersect something, the character has left the ground and enters a physics based falling mode.

...such as when you get (un)lucky and the cast ray happens to intersect (near) the edge shared by two triangles and neither triangle "reports" an intersection (due to numerical round-off errors). To avoid this might require having collision geometry in addition to the geometry you need for visualizing.

Quote:

Lastly, ray cast against everything. Often physics engines are highly optimized ray casting machines, but ideally that would happen at the graphics detail level. It doesn't make sense when bullets or characters are hovering over the graphics because they're colliding with the physics primitive.


Ray casting everything can be expensive. Moreover, you are not guaranteed to avoid a collision. You hope is that the probability of missing a collision when there is one is small.

As I said, there is more to this than geometry and simple ray-triangle tests.

Share this post


Link to post
Share on other sites
Quote:
Original post by bzroom
The ray casting should accept a world space ray (origin and extent) and return the first intersection point along that ray.


This depends. For example, you might want your character to walk into a shallow pond of water. If you store only the first intersection along the ray, you'll have your character walking on water. Well, maybe that is what your game is about.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this