Commit 02a78305 authored by Benjamin REED's avatar Benjamin REED

first commit!

parents
Pipeline #3029 failed with stages
# LARUTAN
LARUTAN (Natural backwards) is a raycasting game in the style of Wolfenstein 3D inspired (the rendering code is entirely copied in fact) by the tutorial by [https://lodev.org/cgtutor/raycasting.html](Lodev). The game is implemented in [https://c3-lang.org](C3), which is a work-in-progress evolution of the C language with built-in vectors and cool macro stuff.
LARUTAN (NATUREL à l'envers) est un jeu de 'raycasting' dans le style de Wolfenstein 3D, inspiré (en très grande partie, notamment pour le rendu graphique) par le tutoriel de [https://lodev.org/cgtutor/raycasting.html](Lodev). Le jeu est écrit en [https://c3-lang.org](C3), une évolution du langage C avec support pour des vecteurs multi-dimensionnels et de la magie avec les macros qui n'est pas disponible en C.
# Controls
ZQSD (WASD) to move around (ZS/WS to move backwards and forwards, QD/AD to turn), hold LEFT-ALT to strafe (move from side to side), and space (TODO) to fire with the weapon you're currently holding.
# Building
In order to build LARUTAN you will need the C3 compiler, which can be found on the [https://c3-lang.org/install-c3/prebuilt-binaries/](c3 website). Follow that tutorial and once you have the compiler installed (no worries if its in the same directory as the project, that may make things easier, especially if you get a problem with finding the standard library).
You will also need raylib 5.0, which can be found on the [](raylib website), although I reccommend finding the binaries in the [https://github.com/raysan5/raylib/releases](github releases page) and installing them in the same directory as this folder. My file structure looks like this:
```
*-- LARUTAN
|
\-- src/
\-- raylib-5.0\
|
\-- lib/
|
\-- libraylib.so.500
\-- ...
\-- include
\-- ...
\-- ...
```
If you installed raylib under a different name then edit the makefile to take this into account, change the variable ```RAYLIB_LIBRARY_SEARCH_PATH``` to the name of your folder containing raylib binaries. If you installed c3 into this directory, then set the ```C3_STD``` variable to be empty.
Then just run make and hope for the best. Linker errors will be something to do with the folders being called the wrong thing. If you can't get it to work, try just using c3c.
Your command should look something like this:
```
user$ c3c compile-run src/*.c3 -lraylib -L<your path to raylib's binaries>
```
If you have any further issues, please open an issue.
# NOTES
The project is in its very early stages... not many things work but the very basic engine. Plans are to optimise (very slow) and add real gameplay, notably using weapons, picking up weapons, enemy ai that works, and a physics implementation that isn't simply: 'try everything against everything else and hope for the best.' Thank you for your patience.
# CREDITS
Credit for brick wall texture: [https://www.the3rdsequence.com/texturedb/texture/9/brick+wall/](TODO: find this person's name)
TODO:
- Version française.
File added
RAYLIB_LIBRARY_SEARCH_PATH = ./raylib-5.0/lib
RAYLIB_LIB_NAME = raylib
C3_STD = --stdlib /usr/local/lib
OUTPUT_NAME = main
fast: src/main.c3
c3c compile src/*.c3 -O3 -l $(RAYLIB_LIB_NAME) -L $(RAYLIB_LIBRARY_SEARCH_PATH) $(C3_STD) -o $(OUTPUT_NAME)
run: main
./$(OUTPUT_NAME)
main: src/main.c3
c3c compile src/*.c3 -l $(RAYLIB_LIB_NAME) -L $(RAYLIB_LIBRARY_SEARCH_PATH) $(C3_STD) -o $(OUTPUT_NAME)
clean:
rm $(OUTPUT_NAME)
import std::sort;
import std::io;
import std::math;
import std::core::mem;
import libc;
import std::thread;
import std::collections::list;
import raylib;
import rendering;
import math;
def SpriteList = List(<Sprite*>);
def EntityList = List(<Entity>);
def IndexList = List(<usz>);
const Colour RED = {255, 0, 0, 255};
const Colour WHITE = {255, 255, 255, 255};
const Colour BACKGROUND_COL = {0x18, 0x18, 0x18, 0xFF};
const float PLAYER_SIDE_LENGTH = 0.2f;
const usz NUM_COLLISION_NEIGHBOURS = 9;
const usz NUM_ADDITIONAL_COLLISION_RECT = 10; // how many more things we can collide with outside of 9 neighbours
const float PLAYER_ACCELERATION = 50.0; // Grid squares per second
const float PLAYER_VEL_DAMPING = 0.9;
const float PLAYER_TURN_ACCELERATION = math::PI_2; // RADIANS per second per second
const float PLAYER_TURN_DAMPING = 0.7; // RADIANS per second
const double FOV_ANGLE = 30 * (math::PI/180);//math::PI/2.0;
const double PITCH_ANGLE = 0.66;
enum Weapon : int (String sprite_path)
{
NONE = "assets/missing.png",
PISTOL = "assets/weapons/pistol.png",
}
fn bool Weapon.has_altfire(Weapon weapon) {
switch(weapon) {
case PISTOL:
return false;
default:
return false;
}
}
const int WIDTH_FACTOR = 16;
const int HEIGHT_FACTOR = 9;
const int WINDOW_SCALING_FACTOR = 100;
const float RENDER_SCREEN_PERCENT = 0.5;
const uint RENDER_WINDOW_WIDTH = (uint)((float)(WIDTH_FACTOR * WINDOW_SCALING_FACTOR) * RENDER_SCREEN_PERCENT);
const uint RENDER_WINDOW_HEIGHT = (uint)((float)(HEIGHT_FACTOR * WINDOW_SCALING_FACTOR) * RENDER_SCREEN_PERCENT);
const uint WINDOW_WIDTH = WIDTH_FACTOR * WINDOW_SCALING_FACTOR;
const uint WINDOW_HEIGHT = HEIGHT_FACTOR * WINDOW_SCALING_FACTOR;
def MapCell = short;
const uint MAP_WIDTH = 30;
const uint MAP_HEIGHT = MAP_WIDTH;
const Rect MAP_RECT = Rect { 0, 0, MAP_WIDTH, MAP_HEIGHT };
const uint MAP_GRID_SIZE = 1;
const float MINIMAP_SCALE_FACTOR = 50.0;
const double MAP_AABB_RAYCAST_CHECK_RAY_STEP = 0.05;
const uint COLUMN_PIXEL_WIDTH = 1;
const uint PIXEL_HEIGHT = 1;
const uint TEXTURE_COUNT = 1;
// DEBUG
bool draw_minimap = false;
bool draw_floor_and_ceiling = true;
//------
const usz WORLD_TEXTURES_COUNT = 1;
const usz SPRITE_TEXTURES_COUNT = 1;
const usz WEAPON_TEXTURES_COUNT = 1;
struct Assets {
Image* world_textures;
Image* sprite_textures;
Image* weapon_textures;
Image missing;
}
macro Assets.@get(&self, category, idx) {
switch (category) {
case "ws":
case "weapons":
if (idx < 0 || idx >= WEAPON_TEXTURES_COUNT) return &self.missing;
return &self.weapon_textures[idx];
break;
case "w":
case "wrld":
case "world":
if (idx < 0 || idx >= WORLD_TEXTURES_COUNT) return &self.missing;
return &self.world_textures[idx];
break;
case "s":
case "sprite":
if (idx < 0 || idx >= SPRITE_TEXTURES_COUNT) return &self.missing;
return &self.sprite_textures[idx];
break;
default:
return &self.missing;
}
return &self.missing;
}
fn Image* Assets.load_images(Assets this, String[] paths) {
io::printn("TODO: don't do this, make it similar to the '@get' interface, and make the sizes not be constants because that's a bit schewpid in the year of our lord twenty twenty four");
foreach (path: paths) {
// TODO: don't do this, make it similar to the '@get'
// interface, and make the sizes not be constants because that's a
// bit schewpid in the year of our lord twenty twenty four
}
return null;
}
fn void Assets.free(Assets this) {
io::printfn("TODO: IMPLEMENT FREEING ASSETS");
//for (int i = 0; i < TEXTURE_COUNT; i ++) raylib::unload_image(textures[i]);
}
fn bool Assets.assert_loaded(Assets this) {
io::printfn("TODO: IMPLEMENT CHECKING ASSETS LOADED CORRECTLY");
return true;
//for (int i = 0; i < TEXTURE_COUNT; i ++) {
// if (textures[i].data == null) {
// io::printfn("Failed to load '%s', file not found or corrupt.", image_paths[i ]);
// return -1;
// }
// else if (textures[i].format != 7) {
// io::printfn("Failed to load '%s', file is in the wrong pixel format (need R8G8B8A8, soit 32bpp)\n", image_paths[i]);
// return -1;
// }
//}
}
struct World {
MapCell* map;
// list of cells that have a sprite in them, so that when raycasting into
// the world we can check if we hit a sprite when entering
// a cell. (presumably (haven't implemented yet) this is gonna have
// similar bug as in doom where you won't raycast hit a bit sprite bc it overlaps
// collision 'sectors')
IndexList occupied_cells;
}
fn bool World.ray_hits(World* this, Vec2 og, Vec2 dir, EntityList* ents) {
RaycastHit hit = rendering::cast_ray(this.map, og, dir);
// just checking if it hit or not...
if (hit.hit) return true;
// check for entities and stuff now
MapCell* temp_map = mem::new_array(MapCell, MAP_WIDTH*MAP_HEIGHT);
defer free(temp_map);
foreach(cell: this.occupied_cells) {
temp_map[cell] = 1;
}
// check if a cell that contains an entity is hit with a raycast
hit = rendering::cast_ray(temp_map, og, dir);
if (hit.hit) {
// FIXME: extremely naive and frankly shit implementation
Vec2 hit_pt = og + dir * hit.dist;
foreach(ent:ents) {
for (int i = 0; i < (int)(1.0 / MAP_AABB_RAYCAST_CHECK_RAY_STEP); i ++) {
if (ent.body.centred_box(ent.pos).contains(hit_pt)) {
return true;
}
hit_pt += MAP_AABB_RAYCAST_CHECK_RAY_STEP * dir;
}
}
}
return false;
}
struct Sprite {
usz texture_index;
Vec2 uv_scale_division;
double z;
// effectively private variables
Vec2 pos;
double player_dist;
}
fn Sprite new_sprite(usz texture_index, double z, Vec2 uv_scale_division) {
Sprite new = Sprite {
.texture_index = texture_index,
.z = z,
.uv_scale_division = uv_scale_division,
.pos = {0, 0},
.player_dist = 0.0,
};
return new;
}
struct Body {
bool simulated;
bool collides;
Vec2 vel;
Vec2 acc;
Vec2 scale; // every hitbox is a rectangle or a box, so we just need width and height
// to create a hitbox centred on Entity.pos
}
fn Body new_body(double w, double h) {
Body body = Body {
.simulated = true,
.collides = true,
.vel = {0.0, 0.0},
.acc = {0.0, 0.0},
.scale = {w, h},
};
return body;
}
fn Rect Body.centred_box(Body* this, Vec2 pos) {
Rect rect = {
(float)pos.x - (float)this.scale.x / 2.0,
(float)pos.y - (float)this.scale.y / 2.0,
(float)this.scale.x,
(float)this.scale.y,
};
return rect;
}
struct Entity {
Sprite sprite;
Vec2 pos;
EntityType type;
Body body;
bool dead;
}
fn Sprite* Entity.get_sprite(Entity* this) {
this.sprite.pos = this.pos;
return &this.sprite;
}
enum EntityType {
INDESTRUCTIBLE_DECORATION,
PICKUP,
FOLLOWER,
}
struct Player {
Vec2 pos;
Body body;
Vec2 camera_plane;
// in radians
float dir;
float dir_vel;
// inv
Weapon gun;
}
fn Rect Player.centred_hitbox(Player* this) {
Rect rect = {
(float)this.pos.x - PLAYER_SIDE_LENGTH / 2.0f,
(float)this.pos.y - PLAYER_SIDE_LENGTH / 2.0f,
PLAYER_SIDE_LENGTH,
PLAYER_SIDE_LENGTH,
};
return rect;
}
fn Vec2 float.angle_to_cart(float this) {
return {math::cos(this), math::sin(this)};
}
fn Vec2 double.angle_to_cart(double this) {
return {math::cos(this), math::sin(this)};
}
usz time = 0;
fn void update_entities(EntityList* entities, Player* player) {
foreach (&ent : *entities) {
if (ent.dead) continue;
switch (ent.type) {
case EntityType.INDESTRUCTIBLE_DECORATION: break;
case EntityType.PICKUP:
if (ent.body.collides == true || ent.body.simulated) {
ent.body.collides = false;
ent.body.simulated = false;
}
ent.sprite.z = math::sin(time / 20.0) * 100;
if (ent.body.centred_box(ent.pos).contains_rect(player.centred_hitbox())) {
ent.dead = true;
player.gun = PISTOL;
io::printfn("TODO: implement picking up items.");
}
break;
case EntityType.FOLLOWER:
Vec2 to_player = player.pos - ent.pos;
ent.body.vel += to_player * raylib::get_frame_time() * 10;
break;
default:
io::printfn("WARNING: ENTITY TYPE NOT IMPLEMENTED: %d", ent.type);
break;
}
}
}
fn Vec2 process_body_collisions(MapCell* map, EntityList entities, Vec2 pos, Body body, Entity* this) {
Vec2 final_pos = pos;
Vec2 vel = body.vel;
int[<2>] map_pos = (int[<2>])(pos / MAP_GRID_SIZE);
Vec2 next_pos = pos + body.vel * raylib::get_frame_time();
// don't check for map collisions outside of the map
// TODO: (add one to avoid clipping into stuff from outside)
if (next_pos.x / MAP_GRID_SIZE <= MAP_WIDTH && next_pos.x >= 0 && next_pos.y / MAP_GRID_SIZE <= MAP_HEIGHT && next_pos.y >= 0) {
Rect[NUM_COLLISION_NEIGHBOURS + NUM_ADDITIONAL_COLLISION_RECT] rects;
// loop in the 9 neighbouring squares and check which aren't empty
// to fill in rects array.
for (int y = -1; y <= 1; y ++) {
if (y + map_pos.y < 0 || y + map_pos.y >= MAP_HEIGHT) continue;
for (int x = -1; x <= 1; x ++) {
if (x + map_pos.x < 0 || x + map_pos.x >= MAP_WIDTH) continue;
if (map[(map_pos.y + y) * MAP_WIDTH + (map_pos.x + x)] > 0) {
rects[(y + 1) * 3 + (x + 1)] = {(float)((map_pos.x + x) * MAP_GRID_SIZE), (float)((map_pos.y + y) * MAP_GRID_SIZE), MAP_GRID_SIZE, MAP_GRID_SIZE};
}
else {
rects[(y + 1) * 3 + (x + 1)] = {(float)((map_pos.x + x) * MAP_GRID_SIZE), (float)((map_pos.y + y) * MAP_GRID_SIZE), 0, 0};
}
}
}
for (int i = NUM_COLLISION_NEIGHBOURS; i < NUM_COLLISION_NEIGHBOURS + NUM_ADDITIONAL_COLLISION_RECT; i ++) {
//foreach (&ent : entities) ent.sprite.player_dist = ent.pos.distance(pos);
// TODO: maybe rename player_dist? it doesn't make a lot of sense in this situation
//sort::quicksort(entities, fn int(Entity* a, Entity* b) => (int)((b.sprite.player_dist - a.sprite.player_dist) * 1000.0));
Entity* ent = entities.get_ref(i - NUM_COLLISION_NEIGHBOURS);
if (ent != null && ent.body.collides == true && !ent.dead && ent != this) {
rects[i] = ent.body.centred_box(ent.pos);
}
}
bool[<2>] can_advance = {true, true};
for (int i = 0; i < NUM_COLLISION_NEIGHBOURS + NUM_ADDITIONAL_COLLISION_RECT; i ++) {
if (rects[i].w == 0) continue; // BTEC Rust options :P
raylib::draw_rectangle((int)(rects[i].x * MINIMAP_SCALE_FACTOR), (int)(rects[i].y * MINIMAP_SCALE_FACTOR), (int)(rects[i].w * MINIMAP_SCALE_FACTOR), (int)(rects[i].h * MINIMAP_SCALE_FACTOR), RED);
Rect next_hitbox = {(float)(next_pos.x - PLAYER_SIDE_LENGTH / 2.0), (float)(next_pos.y - PLAYER_SIDE_LENGTH / 2.0), (float)PLAYER_SIDE_LENGTH, (float)PLAYER_SIDE_LENGTH};
if (rects[i].contains_rect(next_hitbox)) {
Rect intersection = rects[i].intersection(next_hitbox);
vel.x = (double)(intersection.w);
vel.y = (double)(intersection.h);
}
double next_x = pos.x + body.vel.x * raylib::get_frame_time();
Rect x_hitbox = {(float)(next_x - PLAYER_SIDE_LENGTH / 2.0), (float)(pos.y - PLAYER_SIDE_LENGTH / 2.0), (float)PLAYER_SIDE_LENGTH, (float)PLAYER_SIDE_LENGTH};
if (rects[i].contains_rect(x_hitbox)) {
can_advance.x = false;
}
double next_y = pos.y + body.vel.y * raylib::get_frame_time();
Rect y_hitbox = {(float)(pos.x - PLAYER_SIDE_LENGTH / 2.0), (float)(next_y - PLAYER_SIDE_LENGTH / 2.0), (float)PLAYER_SIDE_LENGTH, (float)PLAYER_SIDE_LENGTH};
if (rects[i].contains_rect(y_hitbox)) {
can_advance.y = false;
}
}
//can_advance = {true, true};
double next_x = pos.x + body.vel.x * raylib::get_frame_time();
double next_y = pos.y + body.vel.y * raylib::get_frame_time();
if (can_advance.x) final_pos.x = next_x;
if (can_advance.y) final_pos.y = next_y;
}
else {
final_pos = next_pos;
}
// TODO: return vel
return final_pos;
}
fn void update_world_occupied_cells(World* world, EntityList entities, Player* player) {
world.occupied_cells.clear();
// TODO: shit
for (int y = 0; y < MAP_HEIGHT; y ++) {
for (int x = 0; x < MAP_WIDTH; x++) {
Rect rect = {x, y, 1, 1};
foreach(&ent: entities) {
if (!MAP_RECT.contains(ent.pos.round())) continue;
if (rect.intersection(ent.body.centred_box(ent.pos)).equals(math::ZERO)) {
if (!world.occupied_cells.contains((ulong)(y * MAP_WIDTH + x))) {
world.occupied_cells.push((ulong)(y * MAP_WIDTH + x));
}
}
}
}
}
if (!MAP_RECT.contains(player.pos.round())) return;
usz[<2>] player_map_pos = (usz[<2>])player.pos.round();
world.occupied_cells.push(player_map_pos.y * MAP_WIDTH + player_map_pos.x);
}
// Fire is alt or main
fn void player_attack(World* world, EntityList* ents, Player* player, bool altfired) {
// if altfiring a gun w/ no altfire, don't shoot (don't shoot!!)
if ((altfired && !player.gun.has_altfire()) || player.gun == Weapon.NONE) return;
if (!altfired) {
switch(player.gun) {
case PISTOL:
if (world.ray_hits(player.pos, player.dir.angle_to_cart(), ents)) {
io::printn("Raycast hits");
}
else {
io::printn("Raycast MISSES");
}
break;
default:
io::printfn("ERROR: attempt to fire weapon that has not yet been implemented. (Sprite: %s)", player.gun.sprite_path);
break;
}
}
}
fn int main() {
raylib::init_window(WINDOW_WIDTH, WINDOW_HEIGHT, "hello from c3");
defer raylib::close_window();
raylib::set_target_fps(60);
World world;
world.map = (MapCell*)calloc(MAP_WIDTH * MAP_HEIGHT * MapCell.sizeof);
Assets assets;
assets.missing = raylib::load_image("assets/missing.png");
assets.weapon_textures = assets.load_images({"assets/weapons/pistol.png"});
assets.world_textures = assets.load_images({"assets/col_test.png","assets/brick_wall.png"});
assets.sprite_textures = assets.load_images({"assets/sprites/barrel.png","assets/sprites/boy.png"});
/*Image* textures = (Image*)calloc(TEXTURE_COUNT * Image.sizeof);
String[TEXTURE_COUNT] image_paths = {
"assets/col_test.png",
"assets/brick_wall.png",
"assets/sprites/barrel.png",
"assets/sprites/boy.png",
};
for (int i = 0; i < TEXTURE_COUNT; i++) {
textures[i] = raylib::load_image(image_paths[i]);
}*/
Image render_img = raylib::gen_image_colour(RENDER_WINDOW_WIDTH, RENDER_WINDOW_HEIGHT, WHITE);
Texture tex = raylib::load_texture_from_image(render_img);
defer raylib::unload_texture(tex);
defer raylib::unload_image(render_img);
double* z_buffer = (double*)calloc(RENDER_WINDOW_WIDTH * double.sizeof);
defer free(z_buffer);
Colour* pixels = (char[<4>]*)calloc(RENDER_WINDOW_HEIGHT * RENDER_WINDOW_WIDTH * 4);
defer free(pixels);
defer assets.free();
//defer free(textures);
if (assets.assert_loaded() == false) {
return -1;
}
for (int i = 0; i < MAP_HEIGHT; ++i) {
for (int j = 0; j < MAP_WIDTH; ++j) {
if (i == 0 && j != 0 || i == j) {
world.map[(i * MAP_WIDTH + j)] = 3;
}
}
}
world.map[5 * MAP_WIDTH + 1] = 3;
Player player = Player {
.pos = {3.0 * MAP_GRID_SIZE, 6.0f * MAP_GRID_SIZE},
.body = new_body(PLAYER_SIDE_LENGTH, PLAYER_SIDE_LENGTH),
.camera_plane = {0.0, 0.66},
.dir = math::PI,
.dir_vel = 0.0,
.gun = NONE,
};
SpriteList sprites_to_render;
EntityList entities;
entities.push(
Entity {
.type = EntityType.INDESTRUCTIBLE_DECORATION,
.pos = {2, 3},
.sprite = new_sprite(2, 0, {1.0, 1.0}),
.body = new_body(1, 1),
.dead = false,
}
);
entities.push(
Entity {
.type = EntityType.PICKUP,
.pos = {5, 7},
.sprite = new_sprite(3, 0, {0.5, 1.0}),
.body = new_body(1, 1),
.dead = false,
}
);
/*entities.push(
Entity {
.type = EntityType.FOLLOWER,
.pos = {5, 8},
.sprite = new_sprite(3, 0, {1.0, 1.0}),
.body = new_body(1, 1),
.dead = false,
}
);*/
while (!raylib::window_should_close()) {
time ++;
sprites_to_render.free();
foreach (&entity : entities) {
if (!entity.dead) sprites_to_render.push(entity.get_sprite());
}
raylib::begin_drawing();
raylib::clear_background(BACKGROUND_COL);
for (int i = 0; i < RENDER_WINDOW_HEIGHT * RENDER_WINDOW_WIDTH; i ++) pixels[i] = BACKGROUND_COL;
// ---------------------------------------------------------------------
// RAYCAST AND BLIT TEXTURE
// ---------------------------------------------------------------------
if (draw_floor_and_ceiling) rendering::raycast_floors(&player, world.map, &assets, pixels);
for (int i = 0; i < RENDER_WINDOW_WIDTH; i ++) z_buffer[i] = math::DOUBLE_MAX;
rendering::raycast_walls(&player, world.map, &assets, pixels, z_buffer);
rendering::render_sprites(&player, &sprites_to_render, &assets, pixels, z_buffer);
raylib::update_texture(tex, (char*)pixels);
if (!draw_minimap) {
raylib::draw_texture_with_params(
tex,
NPatchInfo {
.source = { 0, 0, RENDER_WINDOW_WIDTH, RENDER_WINDOW_HEIGHT },
},
{0, 0, WINDOW_WIDTH, WINDOW_HEIGHT},
{0, 0},
0.0,
WHITE
);
}
// ---------------------------------------------------------------------
// MINIMAP
// ---------------------------------------------------------------------
if (draw_minimap) {
for (int y = 0; y < MAP_HEIGHT; y ++) {
for (int x = 0; x < MAP_WIDTH; x ++) {
if (world.map[y * MAP_WIDTH + x] > 0) {
raylib::draw_rectangle(x * (int)MINIMAP_SCALE_FACTOR, y * (int)MINIMAP_SCALE_FACTOR, (int)MINIMAP_SCALE_FACTOR, (int)MINIMAP_SCALE_FACTOR, {200, 200, 200, 100});
}
}
}
}
Rect random = {5.5, 8, 1, 1};
if (draw_minimap) {
Vec2 pos = (player.pos * MINIMAP_SCALE_FACTOR);
raylib::draw_rectangle(
(int)((pos.x - PLAYER_SIDE_LENGTH/2)),
(int)((pos.y - PLAYER_SIDE_LENGTH/2)),
(int)(PLAYER_SIDE_LENGTH * MINIMAP_SCALE_FACTOR),
(int)(PLAYER_SIDE_LENGTH * MINIMAP_SCALE_FACTOR),
{255, 255, 0, 255}
);
// raylib::draw_line_ex(player.pos, (player.pos + player.dir.angle_to_cart() * 100.0), 5.0, {255, 0, 0, 255});
int[<2>] next = (int[<2>])(pos + player.dir.angle_to_cart() * 100.0);
int[<2>] perp = (int[<2>])(pos + (player.dir + math::PI_2).angle_to_cart() * RENDER_WINDOW_WIDTH / 2.0);
int[<2>] perp2 = (int[<2>])(pos + (player.dir - math::PI_2).angle_to_cart() * RENDER_WINDOW_WIDTH / 2.0);
int[<2>] fovleft = (int[<2>])(pos + (player.dir - FOV_ANGLE/2.0).angle_to_cart() * 100.0);
int[<2>] fovright = (int[<2>])(pos + (player.dir + FOV_ANGLE/2.0).angle_to_cart() * 100.0);
int[<2>] cam_plane_middle = (int[<2>])(pos + player.dir.angle_to_cart() * MINIMAP_SCALE_FACTOR);
int[<2>] cam_plane_left = (int[<2>])((double[<2>])cam_plane_middle + player.camera_plane *
MINIMAP_SCALE_FACTOR);
int[<2>] cam_plane_og = (int[<2>])(pos + player.camera_plane) * (int)MINIMAP_SCALE_FACTOR;
int[<2>] cam_plane = (int[<2>])(pos + player.camera_plane + (player.dir + math::PI_2).angle_to_cart() * 50.0);
raylib::draw_line((int)pos.x, (int)pos.y, next.x, next.y, {0, 255, 0, 255});
//raylib::draw_line((int)pos.x, (int)pos.y, perp.x, perp.y, {255, 255, 0, 255});
//raylib::draw_line((int)pos.x, (int)pos.y, perp2.x, perp2.y, {255, 255, 0, 255});
//raylib::draw_line((int)pos.x, (int)pos.y, fovleft.x, fovleft.y, {255, 255, 0, 255});
//raylib::draw_line((int)pos.x, (int)pos.y, fovright.x, fovright.y, {255, 255, 0, 255});
raylib::draw_line((int)cam_plane_middle.x, (int)cam_plane_middle.y, cam_plane_left.x, cam_plane_left.y, {255, 255, 0, 255});
}
// ---------------------------------------------------------------------
// DRAW PLAYER GUN
// ---------------------------------------------------------------------
if (player.gun != Weapon.NONE) {
}
raylib::end_drawing();
// ---------------------------------------------------------------------
// PROCESS PLAYER CONTROLS
// ---------------------------------------------------------------------
if (raylib::is_key_pressed(raylib::KEY_RIGHT_ALT)) {
//draw_floor_and_ceiling = !draw_floor_and_ceiling;
draw_minimap = !draw_minimap;
}
Vec2 acc = {0.0, 0.0};
float turn_acc = 0.0;
if (raylib::is_key_down(raylib::KEY_W)) {
acc = player.dir.angle_to_cart();
}
if (raylib::is_key_down(raylib::KEY_S)) {
acc = -player.dir.angle_to_cart();
}
if (raylib::is_key_down(raylib::KEY_D) && raylib::is_key_down(raylib::KEY_LEFT_ALT)) {
acc -= player.dir.angle_to_cart().rotate(math::PI_2);
}
else if (raylib::is_key_down(raylib::KEY_D)) {
turn_acc += PLAYER_TURN_ACCELERATION;
}
if (raylib::is_key_down(raylib::KEY_A) && raylib::is_key_down(raylib::KEY_LEFT_ALT)) {
acc -= player.dir.angle_to_cart().rotate(-math::PI_2);
}
else if (raylib::is_key_down(raylib::KEY_A)) {
turn_acc -= PLAYER_TURN_ACCELERATION;
}
if (raylib::is_key_pressed(raylib::KEY_SPACE)) {
player_attack(&world, &entities, &player, false);
}
acc = acc.normalize() * PLAYER_ACCELERATION;
player.body.vel += acc * raylib::get_frame_time();
player.pos = process_body_collisions(world.map, entities, player.pos, player.body, null);
player.body.vel *= PLAYER_VEL_DAMPING;
player.dir_vel += turn_acc * raylib::get_frame_time();
player.dir -= player.dir_vel;
player.camera_plane = player.camera_plane.rotate(-(double)player.dir_vel);
player.dir_vel *= PLAYER_TURN_DAMPING;
update_entities(&entities, &player);
foreach (&ent : entities) {
if (ent.type == EntityType.FOLLOWER) {
ent.pos = process_body_collisions(world.map, entities, ent.pos, ent.body, ent);
ent.body.vel *= PLAYER_VEL_DAMPING;
}
}
update_world_occupied_cells(&world, entities, &player);
} // GAMELOOP
return 0;
}
import std::math;
struct Rect {
float x, y, w, h;
}
enum RectCorner {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT,
}
const Rect ZERO = {0, 0, 0, 0};
macro bool Rect.equals(Rect *this, Rect that) {
return this.x == that.x && this.y == that.y && this.w == that.x && this.h == that.h;
}
fn bool Rect.contains(Rect this, Vec2 point) {
return
(float)point.x >= this.x &&
(float)point.y >= this.y &&
(float)point.x <= (this.x + this.w) &&
(float)point.y <= (this.y + this.h);
}
fn bool Rect.contains_rect(Rect this, Rect other) {
Vec2 tl = other.corner(RectCorner.TOP_LEFT);
Vec2 tr = other.corner(RectCorner.TOP_RIGHT);
Vec2 bl = other.corner(RectCorner.BOTTOM_LEFT);
Vec2 br = other.corner(RectCorner.BOTTOM_RIGHT);
return
this.contains(tl) ||
this.contains(tr) ||
this.contains(bl) ||
this.contains(br);
}
fn Rect Rect.intersection(Rect this, Rect other) {
// TODO: write working intersection check
if (!this.contains_rect(other)) {
return {0, 0, 0, 0};
}
float left = math::max(this.x, other.x);
float width = math::min(this.x + this.w, other.x + other.w) - left;
float top = math::max(this.y, other.y);
float height = math::min(this.y + this.h, other.y + other.h) - top;
return Rect { left, top, width, height };
}
fn Vec2 Rect.corner(Rect this, RectCorner corner) {
switch (corner) {
case RectCorner.TOP_LEFT:
return {this.x, this.y};
case RectCorner.BOTTOM_LEFT:
return {this.x, (double)(this.y + this.h)};
case RectCorner.BOTTOM_RIGHT:
return {(double)(this.x + this.w), (double)(this.y + this.h)};
case RectCorner.TOP_RIGHT:
return {(double)(this.x + this.w), this.y};
}
}
import std::math;
import math;
const int KEY_S = 83;
const int KEY_W = 87;
const int KEY_D = 68;
const int KEY_A = 65;
const int KEY_SPACE = 32;
const int KEY_LEFT_ALT = 342;
const int KEY_RIGHT_ALT = 346;
def Colour = char[<4>];
// Raylib Image struct
struct Image {
char[<4>] *data; // Image raw data
int width; // Image base width
int height; // Image base height
int mipmaps; // Mipmap levels, 1 by default
int format; // Data format (PixelFormat type)
}
struct Texture {
uint id; // OpenGL texture id
int width; // Texture base width
int height; // Texture base height
int mipmaps; // Mipmap levels, 1 by default
int format; // Data format (PixelFormat type)
}
struct NPatchInfo {
Rect source; // Texture source rectangle
int left; // Left border offset
int top; // Top border offset
int right; // Right border offset
int bottom; // Bottom border offset
int layout; // Layout of the n-patch: 3x3, 1x3 or 3x1
}
extern fn void init_window(int width, int height, char* title) @extern("InitWindow");
extern fn void close_window() @extern("CloseWindow");
extern fn bool window_should_close() @extern("WindowShouldClose");
extern fn void begin_drawing() @extern("BeginDrawing");
extern fn void end_drawing() @extern("EndDrawing");
extern fn void clear_background(Colour color) @extern("ClearBackground");
extern fn void draw_rectangle(int posX, int posY, int width, int height, Colour color) @extern("DrawRectangle");
fn void draw_rect(Rect rect, Colour colour) {
draw_rectangle((int)rect.x, (int)rect.y, (int)rect.w, (int)rect.h, colour);
}
extern fn void draw_line_ex(Vec2 startPos, Vec2 endPos, float thick, Colour color) @extern("DrawLineEx");
extern fn void draw_line_v(Vec2 startPos, Vec2 endPos, Colour color) @extern("DrawLineV");
extern fn void draw_line(int startPosX, int startPosY, int endPosX, int endPosY, Colour color) @extern("DrawLine");
extern fn void draw_grid(int slices, float spacing) @extern("DrawGrid"); // Draw a grid (centered at (0, 0, 0))
extern fn float get_frame_time() @extern("GetFrameTime");
extern fn void set_target_fps(int max_fps) @extern("SetTargetFPS");
extern fn bool is_key_pressed(int key) @extern("IsKeyPressed");
extern fn bool is_key_down(int key) @extern("IsKeyDown");
extern fn Image gen_image_colour(int width, int height, Colour color) @extern("GenImageColor"); // Generate image: plain color
extern fn Image load_image(char* file_path) @extern("LoadImage");
extern fn Image load_image_from_memory(char *fileType, char *fileData, int dataSize) @extern ("LoadImageFromMemory");
extern fn Image load_image_from_screen() @extern("LoadImageFromScreen"); // Load image from screen buffer and (screenshot)
extern fn void update_texture(Texture texture, char *pixels) @extern("UpdateTexture"); // Update GPU texture with new data
extern fn Texture load_texture_from_image(Image image) @extern("LoadTextureFromImage"); // Load texture from image data
extern fn void draw_texture(Texture texture, int posX, int posY, Colour tint) @extern("DrawTexture");
extern fn void unload_texture(Texture texture) @extern("UnloadTexture");
extern fn void draw_texture_with_params(Texture texture, NPatchInfo nPatchInfo, Rect dest, Vec2 origin, float rotation, Colour tint) @extern("DrawTextureNPatch"); // Draws a texture (or part of it) that stretches or shrinks nicely
extern fn void unload_image(Image image) @extern("UnloadImage");
import std::math;
import main;
import raylib;
import std::sort;
import std::io;
const uint MAX_DDA_DEPTH = 50;
const double MAX_DDA_DIST = 45.0;
const double FADEOUT_DIST = 30.0;
const bool FADEOUT = true;
struct RaycastHit {
double dist;
int side;
int[<2>] map_pos;
bool hit;
}
fn RaycastHit cast_ray(MapCell* map, Vec2 og, Vec2 dir) {
int map_x = (int)math::floor(og.x);
int map_y = (int)math::floor(og.y);
double side_dist_x = 0;
double side_dist_y = 0;
double d_dist_x = ((dir.x == 0) ? math::DOUBLE_MAX : math::abs( 1.0 / dir.x ));
double d_dist_y = ((dir.y == 0) ? math::DOUBLE_MAX : math::abs( 1.0 / dir.y ));
int step_x = 0;
int step_y = 0;
bool hit = false;
int side = 0;
// calculate step
if (dir.x < 0) {
step_x = -1;
side_dist_x = (og.x - (double)(map_x)) * d_dist_x;
}
else {
step_x = 1;
side_dist_x = ((double)(map_x) + 1.0 - og.x) * d_dist_x;
}
if (dir.y < 0) {
step_y = -1;
side_dist_y = (og.y - (double)(map_y)) * d_dist_y;
}
else {
step_y = 1;
side_dist_y = ((double)(map_y) + 1.0 - og.y) * d_dist_y;
}
uint depth = 0;
double dist = 0;
// do DDA algorithm
while (hit == false) {
if (side_dist_x < side_dist_y) {
dist = side_dist_x;
side_dist_x += d_dist_x;
map_x += step_x;
side = 0;
}
else {
dist = side_dist_y;
side_dist_y += d_dist_y;
map_y += step_y;
side = 1;
}
if (map[map_y * main::MAP_WIDTH + map_x] > 0) {
hit = true;
}
depth ++;
if (depth > MAX_DDA_DEPTH) {
break;
}
else if (dist > MAX_DDA_DIST) {
break;
}
if (map_x >= main::MAP_WIDTH || map_x < 0 || map_y < 0 || map_y >= main::MAP_HEIGHT) {
hit = false;
continue;
}
}
// FIXME: busted ass logic lmao
if (!hit) {
return RaycastHit { 0, 0, {0, 0}, false };
}
if (map_x >= main::MAP_WIDTH || map_x < 0 || map_y < 0 || map_y >= main::MAP_HEIGHT) {
return RaycastHit { math::DOUBLE_MAX, 0, {0, 0}, false };
}
return RaycastHit {
.dist = dist,
.side = side,
.map_pos = {map_x, map_y},
.hit = true
};
}
fn void raycast_walls(Player* player, MapCell* map, Assets* assets, Colour* pixels, double* z_buffer) {
int num_rays = main::RENDER_WINDOW_WIDTH;
Vec2 fov_left = player.dir.angle_to_cart() - player.camera_plane;
Vec2 fov_right = player.dir.angle_to_cart() + player.camera_plane;
for (int i = 0; i < num_rays; i ++) {
Vec2 ray_dir = fov_left.lerp(fov_right, ((double)i/(double)main::RENDER_WINDOW_WIDTH));
Vec2 player_map_pos = player.pos;
RaycastHit hit = //RaycastHit { 0, 0, {0, 0}, {0, 0}, false };
cast_ray(map, player.pos, ray_dir);
if (!hit.hit) {
continue;
}
double perp_wall_dist = hit.dist;
int side = hit.side;
int line_height = 0;
if (perp_wall_dist == 0) {
line_height = main::RENDER_WINDOW_HEIGHT;
}
else {
line_height = (int)(main::RENDER_WINDOW_HEIGHT / perp_wall_dist);
}
int draw_start = -line_height / 2 + main::RENDER_WINDOW_HEIGHT / 2;
if (draw_start < 0) {
draw_start = 0;
}
int draw_end = line_height / 2 + main::RENDER_WINDOW_HEIGHT / 2;
if (draw_end >= main::RENDER_WINDOW_HEIGHT) {
draw_end = main::RENDER_WINDOW_HEIGHT - 1;
}
int map_index = hit.map_pos.y * main::MAP_WIDTH + hit.map_pos.x;
if (map[map_index] != 0) z_buffer[i] = perp_wall_dist;
int texture_index = map[map_index];
if (texture_index <= main::TEXTURE_COUNT) {
texture_index--;
Image *texture = assets.@get("w", texture_index);
double wall_x = 0.0;
if (side == 0) {
wall_x = player_map_pos.y + perp_wall_dist * ray_dir.y;
}
else {
wall_x = player_map_pos.x + perp_wall_dist * ray_dir.x;
}
wall_x -= math::floor(wall_x);
int tex_x = (int)(wall_x * (double)texture.width);
if ((side == 0 && ray_dir.x > 0) || (side == 1 && ray_dir.y < 0)) {
tex_x = texture.width - tex_x - 1;
}
double step = 1.0 * texture.height / line_height;
double tex_pos = (double)(draw_start - main::RENDER_WINDOW_HEIGHT / 2 + line_height / 2) * step;
for (int y = draw_start; y < draw_end; y ++) {
int tex_y = (int)tex_pos & (texture.height - 1);
tex_pos += step;
int idx = (texture.width * tex_y + tex_x);
if (idx >= texture.width*texture.height || idx < 0) {
continue;
}
Colour col = texture.data[idx];
if (FADEOUT) {
float[<4>] col_lerp = (float[<4>])col / 255.0;
col_lerp = col_lerp.lerp(float[<4>] {1.0, 1.0, 1.0, 1.0}, (float)(perp_wall_dist / FADEOUT_DIST));
col_lerp *= 255.0;
pixels[y * main::RENDER_WINDOW_WIDTH + i] = (Colour)col_lerp;
}
else {
pixels[y * main::RENDER_WINDOW_WIDTH + i] = col;
}
}
}
else {
Colour col;
switch(map[map_index]) {
case 0:
col = {0, 0, 0, 255};
break;
case 2:
col = {255, 100, 100, 255};
break;
default:
col = {255, 0, 255, 255};
break;
}
if (side == 1) {
col = col;
}
for (int y = draw_start; y < draw_end; ++y) {
pixels[(y * main::RENDER_WINDOW_WIDTH + i)] = col;
}
}
}
}
fn void raycast_floors(Player* player, MapCell* map, Assets* assets, Colour* pixels) {
Vec2 fov_left = player.dir.angle_to_cart() - player.camera_plane;
Vec2 fov_right = player.dir.angle_to_cart() + player.camera_plane;
Vec2 player_map_pos = player.pos;
for (int y = 0; y < main::RENDER_WINDOW_HEIGHT; y++) {
int p = y - (main::RENDER_WINDOW_HEIGHT / 2); // y compared to horizon (h / 2)
if (p == 0) {
p = main::RENDER_WINDOW_HEIGHT/2;
}
// vertical position of the camera in "3D" space
float pos_z = 0.5 * main::RENDER_WINDOW_HEIGHT;
// horizontal distance from camera to the floor for this row
float row_distance = pos_z / p;
Vec2 floor_step = row_distance * (fov_right - fov_left) / main::RENDER_WINDOW_WIDTH;
// world coords of the leftmost collumn
Vec2 floor_pos = (player_map_pos + row_distance * fov_left);
double dist = floor_pos.distance(player.pos);
for (int x = 0; x < main::RENDER_WINDOW_WIDTH; x ++) {
int[<2>] cell = (int[<2>])math::floor(floor_pos);
int floor_tex_idx = 1;
int ceiling_tex_idx = 1;
if (cell.x % 2 == 0 && cell.y % 2 == 0) {
floor_tex_idx = 1;
}
Image* floor_texture = assets.@get("w", floor_tex_idx);
Image* ceiling_texture = assets.@get("w", ceiling_tex_idx);
// TODO: calculate separately for different sized floor & ceiling textures
int[<2>] tex_pos = {
(int)(floor_texture.width * (floor_pos.x - cell.x)) & (floor_texture.width - 1),
(int)(floor_texture.height * (floor_pos.y - cell.y)) & (floor_texture.height - 1),
};
floor_pos += floor_step;
Colour col;
col = floor_texture.data[floor_texture.width * tex_pos.y + tex_pos.x];
float[<4>] col_lerp;
if (FADEOUT) {
col_lerp = (float[<4>])col / 255.0;
col_lerp = col_lerp.lerp(float[<4>] {1.0, 1.0, 1.0, 1.0}, (float)(dist / FADEOUT_DIST));
col_lerp *= 255.0;
if (math::abs(y - main::RENDER_WINDOW_HEIGHT / 2) < 3) col_lerp = main::WHITE;
//col = (Colour)(((int)col >> 1) & 8355711); // darken colour through magic
pixels[y * main::RENDER_WINDOW_WIDTH + x] = (Colour)col_lerp;
}
else {
if (math::abs(y - main::RENDER_WINDOW_HEIGHT / 2) < 3) col = main::WHITE;
//col = (Colour)(((int)col >> 1) & 8355711); // darken colour through magic
pixels[y * main::RENDER_WINDOW_WIDTH + x] = col;
}
//pixels[y][x] = col;
col = ceiling_texture.data[ceiling_texture.width * tex_pos.y + tex_pos.x];
if (FADEOUT) {
col_lerp = (float[<4>])col / 255.0;
col_lerp = col_lerp.lerp(float[<4>] {1.0, 1.0, 1.0, 1.0}, (float)(dist / FADEOUT_DIST));
col_lerp *= 255.0;
if (math::abs(y - main::RENDER_WINDOW_HEIGHT / 2) < 2) col_lerp = main::WHITE;
//col = (Colour)(((int)col >> 1) & 8355711); // darken colour through magic
pixels[(main::RENDER_WINDOW_HEIGHT - y - 1) * main::RENDER_WINDOW_WIDTH + x] = (Colour)col_lerp;
}
else {
if (math::abs(y - main::RENDER_WINDOW_HEIGHT / 2) < 2) col = main::WHITE;
//col = (Colour)(((int)col >> 1) & 8355711); // darken colour through magic
pixels[(main::RENDER_WINDOW_HEIGHT - y - 1) * main::RENDER_WINDOW_WIDTH + x] = col;
}
}
}
}
fn void render_sprites(Player* player, SpriteList* sprites, Assets* assets, Colour* pixels, double* z_buffer) {
for (int i = 0; i < sprites.len(); i++) {
sprites.get(i).player_dist = player.pos.distance(sprites.get(i).pos);
}
sort::quicksort(*sprites, fn int(Sprite* a, Sprite* b) => (int)((b.player_dist - a.player_dist) * 1000.0));
foreach (sprite : sprites) {
Vec2 rel_pos = sprite.pos - player.pos;
// did this in maths expertes, inverse of a matrix to find A or whatever
Vec2 player_dir = player.dir.angle_to_cart();
double inverse_determinant = 1.0 / (player.camera_plane.x * player_dir.y - player_dir.x * player.camera_plane.y);
Vec2 transform;
transform.x = inverse_determinant * (player_dir.y * rel_pos.x - player_dir.x * rel_pos.y);
transform.y = inverse_determinant * (-player.camera_plane.y * rel_pos.x + player.camera_plane.x * rel_pos.y);
int z_move = (int) (sprite.z / transform.y);
if (transform.y == 0) continue;
int sprite_screen_x = (int)( (main::RENDER_WINDOW_WIDTH / 2) * ( 1 + transform.x / transform.y ) );
int sprite_height = math::abs((int)((double)main::RENDER_WINDOW_HEIGHT / transform.y));
if (sprite.uv_scale_division.y != 0) {
sprite_height = (int)( (double)sprite_height / sprite.uv_scale_division.y);
}
int draw_start_y = -sprite_height / 2 + main::RENDER_WINDOW_HEIGHT / 2 + z_move;
if (draw_start_y < 0) {
draw_start_y = 0;
}
int draw_end_y = sprite_height / 2 + main::RENDER_WINDOW_HEIGHT / 2 + z_move;
if (draw_end_y >= 0) {
draw_end_y = main::RENDER_WINDOW_HEIGHT-1;
}
int sprite_width = math::abs( (int)(main::RENDER_WINDOW_HEIGHT / transform.y) );
if (sprite.uv_scale_division.x != 0) {
sprite_width = (int)( (double)sprite_width / sprite.uv_scale_division.x);
}
int draw_start_x = -sprite_width / 2 + sprite_screen_x;
if (draw_start_x < 0) {
draw_start_x = 0;
}
int draw_end_x = sprite_width / 2 + sprite_screen_x;
if (draw_end_x >= main::RENDER_WINDOW_WIDTH) {
draw_end_x = main::RENDER_WINDOW_WIDTH - 1;
}
Image* tex = assets.@get("sprite", sprite.texture_index);
for (int stripe = draw_start_x; stripe < draw_end_x; stripe++) {
int tex_x = (int) (256 * (stripe - (-sprite_width / 2 + sprite_screen_x)) * tex.width / sprite_width) / 256;
if (transform.y > 0 && stripe > 0 && stripe < main::RENDER_WINDOW_WIDTH && transform.y < z_buffer[stripe])
{
for (int y = draw_start_y; y < draw_end_y; y++) {
int d = (y-z_move) * 256 - main::RENDER_WINDOW_HEIGHT * 128 + sprite_height * 128;
int tex_y = ((d * tex.height) / sprite_height) / 256;
if (tex_y * tex.width + tex_x >= tex.width * tex.height) continue;
Colour col = tex.data[tex_y * tex.width + tex_x];
if (FADEOUT) {
float[<4>] col_lerp = (float[<4>])col / 255.0;
col_lerp = col_lerp.lerp(float[<4>] {1.0, 1.0, 1.0, 1.0}, (float)(sprite.player_dist / FADEOUT_DIST));
col_lerp *= 255.0;
if (col.a != 0) pixels[y * main::RENDER_WINDOW_WIDTH + stripe] = (Colour)col_lerp;
}
else {
if (col.a != 0) pixels[y * main::RENDER_WINDOW_WIDTH + stripe] = col;
}
}
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment