Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
branch: master
315 lines (260 sloc) 15.605 kb
/*
*********************************************************************************
Pebble 3D FPS Engine v0.2b
Created by Rob Spiess (robisodd@gmail.com) on June 23, 2014
*********************************************************************************
v0.1b: Initial Release
v0.2b: Converted all floating points to int32_t which increased framerate substancially
Gets overflow errors, so when a number squared is negative, sets it to 2147483647.
Added door blocks for another example
*********************************************************************************
Created by reading this website: http://www.playfuljs.com/a-first-person-engine-in-265-lines/
CC Copyright (c) 2014 All Right Reserved
THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
http://creativecommons.org/licenses/by/3.0/legalcode
*/
#include "pebble.h"
#define ACCEL_STEP_MS 10 // Update frequency
#define root_depth 20 // How many iterations square root function performs
#define mapsize 20 // Map is 20x20 squres, or whatever number is here
#define range 10000 // Distance player can see
#define idclip false // Walk thru walls
#define view_border true // Draw border around viewing window
#define draw_textbox true // Draw textbox or not
//----------------------------------//
// Viewing Window Size and Position //
//----------------------------------//
// beneficial to: (set fov as divisible by view_w) and (have view_w evenly divisible by 2)
// e.g.: view_w=144 is good since 144/2=no remainder. Set fov = 13104fov (since it = 144w x 91 and is close to 20% of 65536)
// Full Screen (You should also comment out drawing the text box)
//#define view_x 0 // View Left Edge
//#define view_y 0 // View Top Edge
//#define view_w 144 // View Width in pixels
//#define view_h 168 // View Hight in pixels
//#define fov 13104 // Field of view angle (20% of a circle is good) (TRIG_MAX_RATIO = 0x10000 or 65536) * 20%
// Smaller square
//#define view_x 20 // View Left Edge
//#define view_y 30 // View Top Edge
//#define view_w 100 // View Width in pixels
//#define view_h 100 // View Hight in pixels
//#define fov 13100 // Field of view angle (20% of a circle is good) (TRIG_MAX_RATIO = 0x10000 or 65536) * 20%
//Nearly full screen
#define view_x 1 // View Left Edge
#define view_y 25 // View Top Edge
#define view_w 142 // View Width in pixels
#define view_h 140 // View Hight in pixels
#define fov 13064 // Field of view angle (20% of a circle is good) (TRIG_MAX_RATIO = 0x10000 or 65536) * 20%
//----------------------------------//
#define fov_over_w fov/view_w // Do math now so less during execution
#define half_view_w view_w/2 //
//----------------------------------//
typedef struct PlayerVar {
int32_t x; // Player's X Position x1000
int32_t y; // Player's Y Position x1000
int32_t facing; // Player Direction Facing (from 0 - TRIG_MAX_ANGLE)
} PlayerVar;
typedef struct RayVar {
int32_t x; // Origin X
int32_t y; // Origin Y
int32_t dist; // Distance
int8_t hit; // Hit (What on the map it hit)
int32_t offset; // Offset (used for wall texture)
} RayVar;
static Window *window;
static GRect window_frame;
static Layer *graphics_layer;
static AppTimer *timer;
static int8_t map[mapsize * mapsize]; // int8 means cells can be from -127 to 128
static PlayerVar player;
//float sine_rad(float theta) {return (float)sin_lookup(TRIG_MAX_ANGLE * theta / (MATH_PI * 2)) / (float)TRIG_MAX_RATIO;} //sin(radians=0-2pi)
//float cosine_rad(float theta) {return (float)cos_lookup(TRIG_MAX_ANGLE * theta / (MATH_PI * 2)) / (float)TRIG_MAX_RATIO;} //cos(radians=0-2pi)
//float floor_float(float a){int32_t b=(int32_t)a; if((float)b!=a) if(a<0)b--; return (float)b;}
//float ceil_float(float a){int32_t b=(int32_t)a; if((float)b!=a) if(a>0)b++; return (float)b;}
int32_t floor_int(int32_t a){int32_t b=(a-a%1000); if(b!=a) if(a<0)b-=1000; return b;}
int32_t ceil_int(int32_t a){int32_t b=(a-a%1000); if(b!=a) if(a>0)b+=1000; return b;}
int32_t sqrt_int(int32_t a) {int32_t b=a; for(int8_t i=0; i<root_depth; i++) b=(b+(a/b))/2; return b;} // Square Root
// ------------------------------------------------------------------------ //
// Map Functions
// ------------------------------------------------------------------------ //
void GenerateMap() {
for (int16_t i=0; i<mapsize*mapsize; i++) map[i] = rand() % 3 == 0 ? 1 : 0; // Randomly 1/3 of spots are blocks
for (int16_t i=0; i<mapsize*mapsize; i++) if(map[i]==1 && rand()%4==0) map[i]=2; // Changes blocks to stripey blocks
for (int16_t i=0; i<mapsize*mapsize; i++) if(map[i]==1 && rand()%4==0) map[i]=3; // Changes blocks to door blocks
}
int8_t getmap(int32_t x, int32_t y) {
if (x<0 || x>=(mapsize*1000) || y<0 || y>=(mapsize*1000)) return -1;
return map[(((y - y%1000)/1000) * mapsize) + ((x - x%1000)/1000)];
return map[((y/1000) * mapsize) + (x/1000)]; // This might work due to integer math dropping remainder
}
/*
// Set map code. Not used, but could be useful. :\
void setmap(int16_t x, int16_t y, int8_t value) {
if ((x >= 0) && (x < mapsize) && (y >= 0) && (y < mapsize))
map[y * mapsize + x] = value;
}
*/
// ------------------------------------------------------------------------ //
void walk(int32_t distance) {
int32_t dx = (cos_lookup(player.facing) * distance) / TRIG_MAX_RATIO;
int32_t dy = (sin_lookup(player.facing) * distance) / TRIG_MAX_RATIO;
if(getmap(floor_int(player.x + dx), floor_int(player.y)) <= 0 || idclip) player.x += dx;
if(getmap(floor_int(player.x), floor_int(player.y + dy)) <= 0 || idclip) player.y += dy;
}
static void main_loop(void *data) {
AccelData accel=(AccelData){.x=0, .y=0, .z=0}; // All three are int16_t
accel_service_peek(&accel); // Read Accelerometer
player.facing += (10 * accel.x); // Spin based on accel.x
walk((int32_t)accel.y); // Walk based on accel.y
//walk(accel.y * 1000 / 1000); // Technically above line is this
layer_mark_dirty(graphics_layer); // Tell pebble to draw when it's ready
}
// ------------------------------------------------------------------------ //
static void graphics_layer_update_proc(Layer *me, GContext *ctx) {
RayVar ray;
int32_t coltop, colbot, angle, sin, cos;
int32_t Xdx=0, Xdy=0, Ydx=0, Ydy=0, z, Xlen, Ylen;
//int32_t looper = 0;
z=0; //delete this
time_t sec1, sec2; uint16_t ms1, ms2; int32_t dt; // time snapshot variables
//time_t sec3; uint16_t ms3; int32_t dt2; // time snapshot variables
time_ms(&sec1, &ms1); //1st Time Snapshot
// Draw black background over whole canvas
// Not needed. Maybe draw some sort of other background?
// Draw Box around view (not needed if fullscreen, i.e. view_w=144 and view_h=168)
if(view_border) {graphics_context_set_stroke_color(ctx, 1); graphics_draw_rect(ctx, GRect(view_x-1, view_y-1, view_w+2, view_h+2));} //White Rectangle Border
// Bring me the horizon
graphics_context_set_stroke_color(ctx, 1); graphics_draw_line(ctx, GPoint(view_x, view_y + (int)(view_h/2)), GPoint(view_x + view_w,view_y + (int)(view_h/2)));
// Begin RayTracing Loop
for(int16_t col = 0; col < view_w; col++) {
//angle = (int32_t)(fov * (((float)col/view_w) - 0.5));
angle = fov_over_w * (col - half_view_w); // Same equation as above, but less math = faster but confusing to people
sin = sin_lookup(player.facing + angle);
cos = cos_lookup(player.facing + angle);
ray = (RayVar){.x=player.x, .y=player.y, .dist=0}; //Shoot rays out of player's eyes. pew pew.
bool going = true; // Loop until something causes you to stop
while(going) {
//time_ms(&sec3, &ms3); //1st Time Snapshot
//looper++;
// Calculate distance to next X gridline in the ray's direction
if (cos == 0) Xlen = 2147483647; // If ray is vertical, will never hit next X
else {
Xdx = cos > 0 ? floor_int(ray.x + 1000) - ray.x : ceil_int(ray.x - 1000) - ray.x;
Xdy = (Xdx * sin)/cos;
Xlen = Xdx * Xdx + Xdy * Xdy; // Multiplying 2 32bit numbers might overflow
if(Xlen<0) Xlen = 2147483647; // Overflow detected so just make length the max
}
// Calculate distance to next Y gridline in the ray's direction
if (sin == 0) Ylen = 2147483647; // If ray is horizontal, will never hit next Y
else {
Ydy = sin > 0 ? floor_int(ray.y + 1000) - ray.y : ceil_int(ray.y - 1000) - ray.y;
Ydx = (Ydy * cos)/sin;
Ylen = Ydx * Ydx + Ydy * Ydy;
if(Ylen<0) Ylen = 2147483647; // Overflow detected so just make length the max
}
// move ray to next step whichever is closer
if(Xlen < Ylen) {
ray.x += Xdx;
ray.y += Xdy;
ray.hit = getmap(floor_int(ray.x - (cos<0?1000:0)), floor_int(ray.y));
ray.dist = ray.dist + sqrt_int(Xlen);
ray.offset = ray.y;
} else {
ray.x += Ydx;
ray.y += Ydy;
ray.hit = getmap(floor_int(ray.x),floor_int(ray.y - (sin<0?1000:0)));
ray.dist = ray.dist + sqrt_int(Ylen);
ray.offset = ray.x;
}
if (ray.hit > 0) { // if ray hits a wall
going = false; // stop ray
ray.offset = ray.offset%1000; // Get fractional part of offset: offset is where on wall ray hits: 000(left) to 999(right)
z = (ray.dist * cos_lookup(angle)) / TRIG_MAX_RATIO; //z = distance
colbot = ((view_h/2) * (z+1000)) / z; // y coordinate of bottom of column to draw
coltop = colbot - ((view_h*1000) / z); // y coordinate of top of column to draw
if(colbot>=view_h) colbot=view_h-1; if(coltop<0) coltop=0; // Make sure line isn't drawn beyond bounding box
// Draw Wall Column
//graphics_context_set_stroke_color(ctx, 1); // Black = 0, White = 1
if(ray.offset<50 || ray.offset > 950) graphics_context_set_stroke_color(ctx, 0); // Black edges on left and right 5% of block (Comment this line to remove edges)
//Note: Line below is for stripey blocks. The "*9)%2" means 9 Stripes and 2 is every other. (2 is now 2000 due to integer math making everything x1000)
/*
if(ray.hit==2){if(floor_int((ray.offset*9)%2000)== 0) graphics_context_set_stroke_color(ctx, 1); else graphics_context_set_stroke_color(ctx, 0);} // Stripey Blocks
if(ray.hit==3){if(ray.offset>250 && ray.offset<750){ // If door block and if on door part
graphics_context_set_stroke_color(ctx, 0); graphics_draw_line(ctx, GPoint((int)col + view_x,coltop + ((colbot-coltop)/3) + view_y), GPoint((int)col + view_x,colbot + view_y)); //bottom third black
colbot = coltop + ((colbot-coltop)/3); graphics_context_set_stroke_color(ctx, 1); // Draw white top third
} }
*/
//graphics_draw_line(ctx, GPoint((int)col + view_x,coltop + view_y), GPoint((int)col + view_x,colbot + view_y)); //Draw the line
if(ray.offset<50 || ray.offset > 950){ // If hit 5% edge, draw a black line border
graphics_context_set_stroke_color(ctx, 0);
graphics_draw_line(ctx, GPoint((int)col + view_x,coltop + view_y), GPoint((int)col + view_x,colbot + view_y)); //Draw the line
} else { // else draw a shaded line, shaded based on distance (z)
coltop += view_y;
colbot += view_y - coltop;
z -= 1000; if (z<0) z=0; // Make everything closer (solid white without having to be nearly touching)
z=sqrt_int(z) / 10; // Distance. z was 0-10000, now z = 0 to 10: 0=close 10=distant. Square Root makes it logarithmic
for(int16_t i=0; i<colbot;i++) {
if((i+coltop+(col*6))%9>=z) graphics_context_set_stroke_color(ctx, 1); else graphics_context_set_stroke_color(ctx, 0);
//if(rand()%z==0) graphics_context_set_stroke_color(ctx, 1); else graphics_context_set_stroke_color(ctx, 0); // Random noise blocks, further=darker
graphics_draw_pixel(ctx, GPoint(col, i+coltop));
}
}
} // End if hit
if((sin<0&&ray.y<0)||(sin>0&&ray.y>=(mapsize*1000))||(cos<0&&ray.x<0)||(cos>0&&ray.x>=(mapsize*1000))) going=false; // stop if ray is out of bounds AND going wrong way
if(ray.dist > range) going=false; // Stop ray after traveling too far
} //End While
} //End For (End RayTracing Loop)
time_ms(&sec2, &ms2); //2nd Time Snapshot
dt = ((int32_t)1000*(int32_t)sec2 + (int32_t)ms2) - ((int32_t)1000*(int32_t)sec1 + (int32_t)ms1); //ms between two time snapshots
//-----------------//
// Display TextBox //
//-----------------//
if(draw_textbox) {
GRect textframe = GRect(0, 0, 143, 20); // Text Box Position and Size
graphics_context_set_fill_color(ctx, 0); graphics_fill_rect(ctx, textframe, 0, GCornerNone); //Black Solid Rectangle
graphics_context_set_stroke_color(ctx, 1); graphics_draw_rect(ctx, textframe); //White Rectangle Border
static char text[40]; //Buffer to hold text
snprintf(text, sizeof(text), " (%d.%d,%d.%d) %dms %dfps", (int)(floor_int(player.x)/1000),(int)(((player.x<0?(-1*player.x):player.x)%1000)/100), (int)(floor_int(player.y)/1000),(int)(((player.y<0?(-1*player.y):player.y)%1000)/100),(int)dt, (int)(1000/dt)); // What text to draw
graphics_context_set_text_color(ctx, 1); // White Text
graphics_draw_text(ctx, text, fonts_get_system_font(FONT_KEY_GOTHIC_14), textframe, GTextOverflowModeWordWrap, GTextAlignmentLeft, NULL); //Write Text
}
//Done. Set a timer to restart loop
timer = app_timer_register(ACCEL_STEP_MS, main_loop, NULL);
// Perhaps clean up variables here?
}
// ------------------------------------------------------------------------ //
static void window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
window_frame = layer_get_frame(window_layer);
graphics_layer = layer_create(window_frame);
layer_set_update_proc(graphics_layer, graphics_layer_update_proc);
layer_add_child(window_layer, graphics_layer);
}
static void window_unload(Window *window) {
layer_destroy(graphics_layer);
}
static void init(void) {
window = window_create();
window_set_window_handlers(window, (WindowHandlers) {
.load = window_load,
.unload = window_unload
});
window_set_fullscreen(window, true); // Get rid of the top bar
window_stack_push(window, false /* False = Not Animated */);
window_set_background_color(window, GColorBlack);
accel_data_service_subscribe(0, NULL); // Start accelerometer
srand(time(NULL)); // Seed randomizer so different map every time
player = (PlayerVar){.x=5000, .y=-2000, .facing=10000}; // Seems like a good place to start
GenerateMap(); // Randomly generate a map
timer = app_timer_register(ACCEL_STEP_MS, main_loop, NULL); // Begin main loop
}
static void deinit(void) {
accel_data_service_unsubscribe();
window_destroy(window);
}
int main(void) {
init();
app_event_loop();
deinit();
}
Jump to Line
Something went wrong with that request. Please try again.