opengl moment #1
@ -24,7 +24,10 @@ typedef struct context {
|
||||
struct rect_primitive *render_queue_rectangles;
|
||||
struct circle_primitive *render_queue_circles;
|
||||
|
||||
struct audio_channel_pair *audio_channels;
|
||||
struct mesh_batch *uncolored_mesh_batches; /* texture_cache reflected */
|
||||
struct mesh_batch_item *uncolored_mesh_batches_loners; /* path reflected */
|
||||
|
||||
struct audio_channel_item *audio_channels;
|
||||
SDL_AudioDeviceID audio_device;
|
||||
int audio_stream_frequency;
|
||||
SDL_AudioFormat audio_stream_format;
|
||||
|
@ -11,7 +11,29 @@ static void ingame_tick(struct state *state) {
|
||||
world_drawdef(scn->world);
|
||||
player_calc(scn->player);
|
||||
|
||||
get_audio_args("soundtrack")->volume -= 0.01f;
|
||||
// unfurl_triangle("/assets/title.png",
|
||||
// (t_fvec3){ 1250, 700, 0 },
|
||||
// (t_fvec3){ 0, 800, 0 },
|
||||
// (t_fvec3){ 0, 0, 0 },
|
||||
// (t_shvec2){ 0, 360 },
|
||||
// (t_shvec2){ 0, 0 },
|
||||
// (t_shvec2){ 360, 0 });
|
||||
|
||||
unfurl_triangle("/assets/red.png",
|
||||
(t_fvec3){ 0, 0, 0 },
|
||||
(t_fvec3){ RENDER_BASE_WIDTH, 0, 0 },
|
||||
(t_fvec3){ RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT, 0 },
|
||||
(t_shvec2){ 0, 0 },
|
||||
(t_shvec2){ 80, 0 },
|
||||
(t_shvec2){ 80, 80 });
|
||||
|
||||
unfurl_triangle("/assets/red.png",
|
||||
(t_fvec3){ RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT, 0 },
|
||||
(t_fvec3){ 0, RENDER_BASE_HEIGHT, 0 },
|
||||
(t_fvec3){ 0, 0, 0 },
|
||||
(t_shvec2){ 80, 80 },
|
||||
(t_shvec2){ 0, 80 },
|
||||
(t_shvec2){ 0, 0 });
|
||||
}
|
||||
|
||||
|
||||
|
27
src/main.c
27
src/main.c
@ -44,6 +44,24 @@ static void poll_events(void) {
|
||||
}
|
||||
|
||||
|
||||
static void APIENTRY opengl_log(GLenum source,
|
||||
GLenum type,
|
||||
GLuint id,
|
||||
GLenum severity,
|
||||
GLsizei length,
|
||||
const GLchar* message,
|
||||
const void* userParam)
|
||||
{
|
||||
(void)source;
|
||||
(void)type;
|
||||
(void)id;
|
||||
(void)severity;
|
||||
(void)userParam;
|
||||
|
||||
log_info("OpenGL: %.*s\n", length, message);
|
||||
}
|
||||
|
||||
|
||||
void main_loop(void) {
|
||||
/*
|
||||
if (!ctx.is_running) {
|
||||
@ -149,6 +167,7 @@ static bool initialize(void) {
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
|
||||
|
||||
/* init got far enough to create a window */
|
||||
ctx.window = SDL_CreateWindow("emerald",
|
||||
@ -178,6 +197,8 @@ static bool initialize(void) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
log_info("OpenGL context: %s\n", glGetString(GL_VERSION));
|
||||
|
||||
/* might need this to have multiple windows */
|
||||
ctx.window_id = SDL_GetWindowID(ctx.window);
|
||||
|
||||
@ -235,6 +256,12 @@ static bool initialize(void) {
|
||||
ctx.debug = false;
|
||||
#endif
|
||||
|
||||
/* hook up opengl debugging callback */
|
||||
if (ctx.debug) {
|
||||
glEnable(GL_DEBUG_OUTPUT);
|
||||
glDebugMessageCallback(opengl_log, NULL);
|
||||
}
|
||||
|
||||
/* random seeding */
|
||||
/* SDL_GetPerformanceCounter returns some platform-dependent number. */
|
||||
/* it should vary between game instances. i checked! random enough for me. */
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include "../util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
@ -31,4 +32,26 @@ struct circle_primitive {
|
||||
t_fvec2 position;
|
||||
};
|
||||
|
||||
/* batch of primitives with overlapping properties */
|
||||
struct mesh_batch {
|
||||
GLuint buffer; /* server side storage */
|
||||
uint8_t *data; /* client side storage */
|
||||
// size_t buffer_len; /* element count */
|
||||
};
|
||||
|
||||
struct mesh_batch_item {
|
||||
char *key;
|
||||
struct mesh_batch value;
|
||||
};
|
||||
|
||||
/* is structure that is in opengl vertex array */
|
||||
struct uncolored_space_triangle {
|
||||
t_fvec3 v0;
|
||||
t_shvec2 uv0;
|
||||
t_fvec3 v1;
|
||||
t_shvec2 uv1;
|
||||
t_fvec3 v2;
|
||||
t_shvec2 uv2;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
184
src/rendering.c
184
src/rendering.c
@ -6,6 +6,7 @@
|
||||
#include <stb_ds.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <tgmath.h>
|
||||
|
||||
@ -17,6 +18,12 @@ void render_queue_clear(void) {
|
||||
arrsetlen(ctx.render_queue_sprites, 0);
|
||||
arrsetlen(ctx.render_queue_rectangles, 0);
|
||||
arrsetlen(ctx.render_queue_circles, 0);
|
||||
|
||||
for (size_t i = 0; i < arrlenu(ctx.uncolored_mesh_batches); ++i)
|
||||
arrsetlen(ctx.uncolored_mesh_batches[i].data, 0);
|
||||
|
||||
for (size_t i = 0; i < shlenu(ctx.uncolored_mesh_batches_loners); ++i)
|
||||
arrsetlen(ctx.uncolored_mesh_batches_loners[i].value.data, 0);
|
||||
}
|
||||
|
||||
|
||||
@ -91,6 +98,78 @@ void push_circle(t_fvec2 position, float radius, t_color color) {
|
||||
}
|
||||
|
||||
|
||||
/* TODO: automatic handling of repeating textures */
|
||||
void unfurl_triangle(const char *path,
|
||||
t_fvec3 v0,
|
||||
t_fvec3 v1,
|
||||
t_fvec3 v2,
|
||||
t_shvec2 uv0,
|
||||
t_shvec2 uv1,
|
||||
t_shvec2 uv2)
|
||||
{
|
||||
/* corrected atlas texture coordinates */
|
||||
t_shvec2 uv0c, uv1c, uv2c;
|
||||
struct mesh_batch *batch_p;
|
||||
|
||||
textures_load(&ctx.texture_cache, path);
|
||||
|
||||
const int atlas_index = textures_get_atlas_index(&ctx.texture_cache, path);
|
||||
if (atlas_index == -1) {
|
||||
/* loners span whole texture i assume */
|
||||
uv0c = uv0;
|
||||
uv1c = uv1;
|
||||
uv2c = uv2;
|
||||
|
||||
batch_p = &shgetp(ctx.uncolored_mesh_batches_loners, path)->value;
|
||||
|
||||
} else {
|
||||
const size_t old_len = arrlenu(ctx.uncolored_mesh_batches);
|
||||
if ((size_t)atlas_index + 1 >= old_len) {
|
||||
/* grow to accommodate texture cache atlases */
|
||||
arrsetlen(ctx.uncolored_mesh_batches, atlas_index + 1);
|
||||
|
||||
/* zero initialize it all, it's a valid state */
|
||||
SDL_memset(&ctx.uncolored_mesh_batches[atlas_index],
|
||||
0,
|
||||
sizeof (struct mesh_batch) * ((atlas_index + 1) - old_len));
|
||||
}
|
||||
|
||||
const SDL_Rect srcrect = ctx.texture_cache.hash[atlas_index].value.srcrect; /* TODO: does it work? */
|
||||
|
||||
/* fixed point galore */
|
||||
const int16_t srcratx = (int16_t)srcrect.x * (INT16_MAX / TEXTURE_ATLAS_SIZE);
|
||||
const int16_t srcratw = (int16_t)srcrect.w * (INT16_MAX / TEXTURE_ATLAS_SIZE);
|
||||
const int16_t srcrath = (int16_t)srcrect.h * (INT16_MAX / TEXTURE_ATLAS_SIZE);
|
||||
const int16_t srcraty = (int16_t)srcrect.y * (INT16_MAX / TEXTURE_ATLAS_SIZE); /* flip? */
|
||||
|
||||
uv0c.x = (int16_t)(srcratx + ((uint16_t)((uv0.x * (INT16_MAX / srcrect.w)) * srcratw) >> 7));
|
||||
uv0c.y = (int16_t)(srcraty + ((uint16_t)((uv0.y * (INT16_MAX / srcrect.h)) * srcrath) >> 7));
|
||||
uv1c.x = (int16_t)(srcratx + ((uint16_t)((uv1.x * (INT16_MAX / srcrect.w)) * srcratw) >> 7));
|
||||
uv1c.y = (int16_t)(srcraty + ((uint16_t)((uv1.y * (INT16_MAX / srcrect.h)) * srcrath) >> 7));
|
||||
uv2c.x = (int16_t)(srcratx + ((uint16_t)((uv2.x * (INT16_MAX / srcrect.w)) * srcratw) >> 7));
|
||||
uv2c.y = (int16_t)(srcraty + ((uint16_t)((uv2.y * (INT16_MAX / srcrect.h)) * srcrath) >> 7));
|
||||
|
||||
batch_p = &ctx.uncolored_mesh_batches[atlas_index];
|
||||
}
|
||||
|
||||
struct uncolored_space_triangle *data = (struct uncolored_space_triangle *)batch_p->data;
|
||||
struct uncolored_space_triangle pack = {
|
||||
.v0 = v0,
|
||||
// .uv0 = uv0c,
|
||||
.uv0 = { 0, 0 },
|
||||
.v1 = v1,
|
||||
// .uv1 = uv1c,
|
||||
.uv1 = { INT16_MAX, 0 },
|
||||
.v2 = v2,
|
||||
// .uv2 = uv2c,
|
||||
.uv2 = { INT16_MAX, INT16_MAX },
|
||||
};
|
||||
arrpush(data, pack);
|
||||
|
||||
batch_p->data = (uint8_t *)data;
|
||||
}
|
||||
|
||||
|
||||
/* compare functions for the sort in render_sprites */
|
||||
static int cmp_atlases(const void *a, const void *b) {
|
||||
int index_a = ((const struct sprite_primitive *)a)->atlas_index;
|
||||
@ -312,9 +391,6 @@ static void render_circle(struct circle_primitive *circle) {
|
||||
|
||||
|
||||
static void render_sprites(void) {
|
||||
if (ctx.texture_cache.is_dirty)
|
||||
textures_update_current_atlas(&ctx.texture_cache);
|
||||
|
||||
sort_sprites(ctx.render_queue_sprites);
|
||||
|
||||
for (size_t i = 0; i < arrlenu(ctx.render_queue_sprites); ++i) {
|
||||
@ -337,36 +413,120 @@ static void render_circles(void) {
|
||||
}
|
||||
|
||||
|
||||
static void draw_uncolored_space_traingle_batch(struct mesh_batch *batch) {
|
||||
size_t data_len = arrlenu(batch->data);
|
||||
|
||||
/* create vertex array object */
|
||||
if (batch->buffer == 0)
|
||||
glGenBuffers(1, &batch->buffer);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, batch->buffer);
|
||||
|
||||
/* TODO: try using mapped buffers while building batches instead? */
|
||||
/* this way we could skip client side copy that is kept until commitment */
|
||||
/* alternatively we could commit glBufferSubData based on a threshold */
|
||||
|
||||
/* upload batched data */
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
data_len * sizeof (struct uncolored_space_triangle),
|
||||
batch->data,
|
||||
GL_STREAM_DRAW);
|
||||
|
||||
/* vertex specification*/
|
||||
glEnableClientState(GL_VERTEX_ARRAY);
|
||||
glVertexPointer(3,
|
||||
GL_FLOAT,
|
||||
offsetof(struct uncolored_space_triangle, v1),
|
||||
(void *)offsetof(struct uncolored_space_triangle, v0));
|
||||
|
||||
/* note: propagates further to where texture binding is done */
|
||||
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
glClientActiveTexture(GL_TEXTURE0);
|
||||
glTexCoordPointer(2,
|
||||
GL_SHORT,
|
||||
offsetof(struct uncolored_space_triangle, v1),
|
||||
(void *)offsetof(struct uncolored_space_triangle, uv0));
|
||||
|
||||
// for (size_t i = 0; i < data_len; ++i) {
|
||||
// struct uncolored_space_triangle t = ((struct uncolored_space_triangle *)batch->data)[i];
|
||||
// log_info("{%i, %i, %i, %i, %i, %i}\n", t.uv0.x, t.uv0.y, t.uv1.x, t.uv1.y, t.uv2.x, t.uv2.y);
|
||||
// }
|
||||
|
||||
/* commit for drawing */
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3 * (int)data_len);
|
||||
|
||||
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
glDisableClientState(GL_VERTEX_ARRAY);
|
||||
|
||||
/* invalidate the buffer immediately */
|
||||
glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
|
||||
static void render_space(void) {
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glPushMatrix();
|
||||
glLoadIdentity();
|
||||
glOrtho(0, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT, 0, -1, 1);
|
||||
|
||||
glBegin(GL_TRIANGLES);
|
||||
glColor4f(0.0, 1.0, 1.0, 1.0);
|
||||
glVertex2f(300.0,210.0);
|
||||
glVertex2f(340.0,215.0);
|
||||
glVertex2f(320.0,250.0);
|
||||
glEnd();
|
||||
glUseProgramObjectARB(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
|
||||
/* solid white, no modulation */
|
||||
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
for (size_t i = 0; i < arrlenu(ctx.uncolored_mesh_batches); ++i) {
|
||||
if (arrlenu(&ctx.uncolored_mesh_batches[i].data) > 0) {
|
||||
SDL_Texture *const atlas = textures_get_atlas(&ctx.texture_cache, (int)i);
|
||||
SDL_GL_BindTexture(atlas, NULL, NULL);
|
||||
draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches[i]);
|
||||
SDL_GL_UnbindTexture(atlas);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < shlenu(ctx.uncolored_mesh_batches_loners); ++i) {
|
||||
if (arrlenu(&ctx.uncolored_mesh_batches_loners[i].value.data) > 0) {
|
||||
SDL_Texture *const atlas = textures_get_loner(&ctx.texture_cache,
|
||||
ctx.uncolored_mesh_batches_loners[i].key);
|
||||
SDL_GL_BindTexture(atlas, NULL, NULL);
|
||||
draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches_loners[i].value);
|
||||
SDL_GL_UnbindTexture(atlas);
|
||||
}
|
||||
}
|
||||
|
||||
glPopMatrix();
|
||||
}
|
||||
|
||||
|
||||
void render(void) {
|
||||
if (ctx.texture_cache.is_dirty)
|
||||
textures_update_current_atlas(&ctx.texture_cache);
|
||||
|
||||
glClearColor((1.0f / 255) * 230,
|
||||
(1.0f / 255) * 230,
|
||||
(1.0f / 255) * 230, 1);
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClear(GL_COLOR_BUFFER_BIT |
|
||||
GL_DEPTH_BUFFER_BIT |
|
||||
GL_STENCIL_BUFFER_BIT);
|
||||
|
||||
render_space();
|
||||
// glDisable(GL_CULL_FACE);
|
||||
// glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_BLEND);
|
||||
|
||||
/* TODO: write with no depth test, just fill it in */
|
||||
render_sprites();
|
||||
render_rectangles();
|
||||
render_circles();
|
||||
|
||||
// SDL_RenderPresent(ctx.renderer);
|
||||
// glEnable(GL_CULL_FACE);
|
||||
// glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
/* TODO: use depth test to optimize gui regions away */
|
||||
render_space();
|
||||
|
||||
SDL_RenderFlush(ctx.renderer);
|
||||
SDL_GL_SwapWindow(ctx.window);
|
||||
}
|
||||
|
@ -35,6 +35,34 @@ void push_rectangle(t_frect rect, t_color color);
|
||||
/* pushes a filled circle onto the circle render queue */
|
||||
void push_circle(t_fvec2 position, float radius, t_color color);
|
||||
|
||||
/* pushes a textured 3d triangle onto the render queue */
|
||||
/* vertices are in absolute coordinates, relative to world origin */
|
||||
/* texture coordinates are in pixels */
|
||||
void unfurl_triangle(const char *path,
|
||||
/* */
|
||||
t_fvec3 v0,
|
||||
t_fvec3 v1,
|
||||
t_fvec3 v2,
|
||||
t_shvec2 uv0,
|
||||
t_shvec2 uv1,
|
||||
t_shvec2 uv2);
|
||||
|
||||
/* TODO: */
|
||||
/* pushes a colored textured 3d triangle onto the render queue */
|
||||
// void unfurl_colored_triangle(const char *path,
|
||||
// t_fvec3 v0,
|
||||
// t_fvec3 v1,
|
||||
// t_fvec3 v2,
|
||||
// t_shvec2 uv0,
|
||||
// t_shvec2 uv1,
|
||||
// t_shvec2 uv2,
|
||||
// t_color c0,
|
||||
// t_color c1,
|
||||
// t_color c2);
|
||||
|
||||
/* TODO: billboarding */
|
||||
// http://www.lighthouse3d.com/opengl/billboarding/index.php?billCheat2
|
||||
|
||||
/* renders the background, then the primitives in all render queues */
|
||||
void render(void);
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
static SDL_Surface *image_to_surface(char *path) {
|
||||
static SDL_Surface *image_to_surface(const char *path) {
|
||||
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
||||
if (handle == NULL)
|
||||
goto fail;
|
||||
@ -236,7 +236,7 @@ void textures_dump_atlases(struct texture_cache *cache) {
|
||||
}
|
||||
|
||||
|
||||
void textures_load(struct texture_cache *cache, char *path) {
|
||||
void textures_load(struct texture_cache *cache, const char *path) {
|
||||
/* no need to do anything if it was loaded already */
|
||||
if (shgeti(cache->hash, path) >= 0 || shgeti(cache->loner_hash, path) >= 0)
|
||||
return;
|
||||
@ -300,18 +300,19 @@ void textures_update_current_atlas(struct texture_cache *cache) {
|
||||
}
|
||||
|
||||
|
||||
SDL_Rect textures_get_srcrect(struct texture_cache *cache, char *path) {
|
||||
SDL_Rect textures_get_srcrect(struct texture_cache *cache, const char *path) {
|
||||
struct texture_cache_item *texture = shgetp_null(cache->hash, path);
|
||||
if (texture == NULL) {
|
||||
CRY("Texture lookup failed.",
|
||||
"Tried to get texture that isn't loaded.");
|
||||
return (SDL_Rect){ 0, 0, 0, 0 };
|
||||
}
|
||||
|
||||
return texture->value.srcrect;
|
||||
}
|
||||
|
||||
|
||||
int textures_get_atlas_index(struct texture_cache *cache, char *path) {
|
||||
int textures_get_atlas_index(struct texture_cache *cache, const char *path) {
|
||||
struct texture_cache_item *texture = shgetp_null(cache->hash, path);
|
||||
|
||||
/* it might be a loner texture */
|
||||
@ -339,7 +340,7 @@ SDL_Texture *textures_get_atlas(struct texture_cache *cache, int index) {
|
||||
}
|
||||
|
||||
|
||||
SDL_Texture *textures_get_loner(struct texture_cache *cache, char *path) {
|
||||
SDL_Texture *textures_get_loner(struct texture_cache *cache, const char *path) {
|
||||
struct texture_cache_item *texture = shgetp_null(cache->loner_hash, path);
|
||||
|
||||
if (texture == NULL) {
|
||||
|
@ -51,25 +51,25 @@ void textures_dump_atlases(struct texture_cache *cache);
|
||||
/* loads an image if it isn't in the cache, otherwise a no-op. */
|
||||
/* can be called from anywhere at any time after init, useful if you want to */
|
||||
/* preload textures you know will definitely be used */
|
||||
void textures_load(struct texture_cache *cache, char *path);
|
||||
void textures_load(struct texture_cache *cache, const char *path);
|
||||
|
||||
/* repacks the current texture atlas based on the texture cache */
|
||||
void textures_update_current_atlas(struct texture_cache *cache);
|
||||
|
||||
/* returns a rect in a texture cache atlas based on a path, for drawing */
|
||||
/* if the texture is not found, returns a zero-filled rect (so check w or h) */
|
||||
SDL_Rect textures_get_srcrect(struct texture_cache *cache, char *path);
|
||||
SDL_Rect textures_get_srcrect(struct texture_cache *cache, const char *path);
|
||||
|
||||
/* returns which atlas the texture in the path is in, starting from 0 */
|
||||
/* if the texture is not found, returns INT_MIN */
|
||||
int textures_get_atlas_index(struct texture_cache *cache, char *path);
|
||||
int textures_get_atlas_index(struct texture_cache *cache, const char *path);
|
||||
|
||||
/* returns a pointer to the atlas at `index` */
|
||||
/* if the index is out of bounds, returns NULL. */
|
||||
/* you can get the index via texture_get_atlas_index */
|
||||
SDL_Texture *textures_get_atlas(struct texture_cache *cache, int index);
|
||||
|
||||
SDL_Texture *textures_get_loner(struct texture_cache *cache, char *path);
|
||||
SDL_Texture *textures_get_loner(struct texture_cache *cache, const char *path);
|
||||
|
||||
/* returns the number of atlases in the cache */
|
||||
size_t textures_get_num_atlases(struct texture_cache *cache);
|
||||
|
13
src/util.h
13
src/util.h
@ -109,6 +109,19 @@ typedef struct fvec2 {
|
||||
} t_fvec2;
|
||||
|
||||
|
||||
/* a point in some three dimension space (floating point) */
|
||||
/* y goes up, x goes to the right */
|
||||
typedef struct fvec3 {
|
||||
float x, y, z;
|
||||
} t_fvec3;
|
||||
|
||||
|
||||
/* a point in some space (short) */
|
||||
typedef struct shvec2 {
|
||||
short x, y;
|
||||
} t_shvec2;
|
||||
|
||||
|
||||
/* decrements an lvalue (which should be an int), stopping at 0 */
|
||||
/* meant for tick-based timers in game logic */
|
||||
/*
|
||||
|
Loading…
Reference in New Issue
Block a user