diff --git a/.gitmodules b/.gitmodules index 3bf29df..26bb664 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "libs/SDL_image"] path = libs/SDL_image url = https://github.com/libsdl-org/SDL_image.git +[submodule "libs/SDL_net"] + path = libs/SDL_net + url = https://github.com/libsdl-org/SDL_net.git diff --git a/build.bat b/build.bat index e4bb962..748a65f 100644 --- a/build.bat +++ b/build.bat @@ -17,17 +17,17 @@ set auto_compile_flags= if "%asan%"=="1" set auto_compile_flags=%auto_compile_flags% -fsanitize=address && echo [asan enabled] :: --- Compile/Link Line Definitions ------------------------------------------ -set cl_common= /I..\src\ /I..\libs\SDL\include\ /I..\libs\SDL_image\include\ /nologo /FC /Z7 /MD /W4 /wd4244 /wd4201 -set clang_common= -I..\src\ -I..\libs\SDL\include\ -I..\libs\SDL_image\include\ -fdiagnostics-absolute-paths -Wall -Wno-unused-variable -Wno-missing-braces -Wno-unused-function -Wno-microsoft-static-assert -Wno-c2x-extensions +set cl_common= /I..\src\ /I..\libs\SDL\include\ /I..\libs\SDL_image\include\ -I..\libs\SDL_net\include\ /nologo /FC /Z7 /MD /W4 /wd4244 /wd4201 +set clang_common= -I..\src\ -I..\libs\SDL\include\ -I..\libs\SDL_image\include\ -I..\libs\SDL_net\include\ -fdiagnostics-absolute-paths -Wall -Wno-unused-variable -Wno-missing-braces -Wno-unused-function -Wno-microsoft-static-assert -Wno-c2x-extensions set cl_debug= call cl /Od /Ob1 /DBUILD_DEBUG=1 %cl_common% %auto_compile_flags% set cl_release= call cl /O2 /DBUILD_DEBUG=0 %cl_common% %auto_compile_flags% -set cl_libs= User32.lib Advapi32.lib Shell32.lib Gdi32.lib Version.lib OleAut32.lib Imm32.lib Ole32.lib Cfgmgr32.lib Setupapi.lib Winmm.lib ..\libs\SDL\build\win\Debug\SDL3-static.lib ..\libs\SDL_image\build\win\Debug\SDL3_image-static.lib +set cl_libs= User32.lib Advapi32.lib Shell32.lib Gdi32.lib Version.lib OleAut32.lib Imm32.lib Ole32.lib Cfgmgr32.lib Setupapi.lib Winmm.lib Ws2_32.lib Iphlpapi.lib ..\libs\SDL\build\win\Debug\SDL3-static.lib ..\libs\SDL_image\build\win\Debug\SDL3_image-static.lib ..\libs\SDL_net\build\win\Debug\SDL3_net-static.lib set cl_link= /link /SUBSYSTEM:WINDOWS /MANIFEST:EMBED /INCREMENTAL:NO /pdbaltpath:%%%%_PDB%%%% %cl_libs% set cl_out= /out: set clang_debug= call clang -g -O0 -DBUILD_DEBUG=1 %clang_common% %auto_compile_flags% set clang_release= call clang -g -O2 -DBUILD_DEBUG=0 %clang_common% %auto_compile_flags% -set clang_libs= -lUser32.lib -lAdvapi32.lib -lShell32.lib -lGdi32.lib -lVersion.lib -lOleAut32.lib -lImm32.lib -lOle32.lib -lCfgmgr32.lib -lSetupapi.lib -lWinmm.lib ..\libs\SDL\build\win\Debug\SDL3-static.lib ..\libs\SDL_image\build\win\Debug\SDL3_image-static.lib +set clang_libs= -lUser32.lib -lAdvapi32.lib -lShell32.lib -lGdi32.lib -lVersion.lib -lOleAut32.lib -lImm32.lib -lOle32.lib -lCfgmgr32.lib -lSetupapi.lib -lWinmm.lib -lWs2_32.lib -lIphlpapi.lib ..\libs\SDL\build\win\Debug\SDL3-static.lib ..\libs\SDL_image\build\win\Debug\SDL3_image-static.lib ..\libs\SDL_net\build\win\Debug\SDL3_net-static.lib set clang_link= -fuse-ld=lld -Xlinker /SUBSYSTEM:WINDOWS -Xlinker /MANIFEST:EMBED -Xlinker /pdbaltpath:%%%%_PDB%%%% %clang_libs% set clang_out= -o @@ -78,6 +78,9 @@ if "%sdl%"=="1" ( pushd libs\SDL_image (cmake -S . -B build\win -DSDLIMAGE_VENDORED=OFF -DBUILD_SHARED_LIBS=OFF "-DSDL3_DIR=..\SDL\build\win" && cmake --build build\win) || exit /b 1 popd + pushd libs\SDL_net + (cmake -S . -B build\win -DBUILD_SHARED_LIBS=OFF "-DSDL3_DIR=..\SDL\build\win" && cmake --build build\win) || exit /b 1 + popd ) else ( echo "SDL directory not found! Make sure to initialize git submodules." ) diff --git a/build.sh b/build.sh index 3e94673..92225ca 100755 --- a/build.sh +++ b/build.sh @@ -16,15 +16,15 @@ if [ -v gcc ]; then compiler="${CC:-gcc}"; echo "[gcc compile]"; fi auto_compile_flags='' # --- Compile/Link Line Definitions ------------------------------------------- -clang_common='-I../src/ -I../libs/SDL/include/ -I../libs/SDL_image/include/ -g -fdiagnostics-absolute-paths -Wall -Wno-unused-variable -std=c23' +clang_common='-I../src/ -I../libs/SDL/include/ -I../libs/SDL_image/include/ -I../libs/SDL_net/include/ -g -fdiagnostics-absolute-paths -Wall -Wno-unused-variable -std=c23' clang_debug="$compiler -O0 -DBUILD_DEBUG=1 ${clang_common} ${auto_compile_flags}" clang_release="$compiler -O2 -DBUILD_DEBUG=0 ${clang_common} ${auto_compile_flags}" -clang_link="../libs/SDL/build/libSDL3.a ../libs/SDL_image/build/libSDL3_image.a -lm" +clang_link="../libs/SDL/build/libSDL3.a ../libs/SDL_image/build/libSDL3_image.a ../libs/SDL_net/build/libSDL3_net.a -lm" clang_out="-o" -gcc_common='-I../src/ -I../libs/SDL/include/ -I../libs/SDL_image/include/ -g -Wall -Wno-unused-variable -std=c23' +gcc_common='-I../src/ -I../libs/SDL/include/ -I../libs/SDL_image/include/ -I../libs/SDL_net/include/ -g -Wall -Wno-unused-variable -std=c23' gcc_debug="$compiler -O0 -DBUILD_DEBUG=1 ${gcc_common} ${auto_compile_flags}" gcc_release="$compiler -O2 -DBUILD_DEBUG=0 ${gcc_common} ${auto_compile_flags}" -gcc_link="../libs/SDL/build/libSDL3.a ../libs/SDL_image/build/libSDL3_image.a -lm" +gcc_link="../libs/SDL/build/libSDL3.a ../libs/SDL_image/build/libSDL3_image.a ../libs/SDL_net/build/libSDL3_net.a -lm" gcc_out="-o" # --- Choose Compile/Link Lines ----------------------------------------------- @@ -62,6 +62,13 @@ if [ -v sdl ]; then else echo "SDL_image directory not found! Make sure to initialize git submodules." fi + if [ -d "libs/SDL_net" ]; then + cd libs/SDL_net + cmake -S . -B build -DBUILD_SHARED_LIBS=OFF "-DSDL3_DIR=..\SDL\build" && cmake --build build + cd ../../ + else + echo "SDL_net directory not found! Make sure to initialize git submodules." + fi fi cd build diff --git a/libs/SDL_net b/libs/SDL_net new file mode 160000 index 0000000..c407036 --- /dev/null +++ b/libs/SDL_net @@ -0,0 +1 @@ +Subproject commit c4070364158b4249d6b0b52e9c00db5b1079e4db diff --git a/src/de_main.c b/src/de_main.c index 9628a8f..4a9dd57 100644 --- a/src/de_main.c +++ b/src/de_main.c @@ -3,230 +3,6 @@ // and it should be isolated from platform specific // stuff when it's reasonable. // -static void Game_AdvanceSimulation(AppState *app) -{ - // update prev_p - ForU32(obj_id, app->object_count) - { - Object *obj = app->object_pool + obj_id; - obj->prev_p = obj->p; - } - - // animate special wall - { - Object *obj = Object_Get(app, app->special_wall); - obj->collision_rotation = WrapF(0.f, 1.f, obj->collision_rotation + TIME_STEP); - obj->sprite_rotation = obj->collision_rotation; - Object_UpdateCollisionVerticesAndNormals(obj); - } - - // player input - ForArray(player_id_index, app->player_ids) - { - Object *player = Object_PlayerFromIndex(app, player_id_index); - - V2 dir = {0}; - if (player_id_index == 0) - { - if (app->keyboard[SDL_SCANCODE_W] || app->keyboard[SDL_SCANCODE_UP]) dir.y += 1; - if (app->keyboard[SDL_SCANCODE_S] || app->keyboard[SDL_SCANCODE_DOWN]) dir.y -= 1; - if (app->keyboard[SDL_SCANCODE_A] || app->keyboard[SDL_SCANCODE_LEFT]) dir.x -= 1; - if (app->keyboard[SDL_SCANCODE_D] || app->keyboard[SDL_SCANCODE_RIGHT]) dir.x += 1; - } - else if (player_id_index == 1) - { - // for vim fans - if (app->keyboard[SDL_SCANCODE_K]) dir.y += 1; - if (app->keyboard[SDL_SCANCODE_J]) dir.y -= 1; - if (app->keyboard[SDL_SCANCODE_H]) dir.x -= 1; - if (app->keyboard[SDL_SCANCODE_L]) dir.x += 1; - } - dir = V2_Normalize(dir); - - bool ice_skating_dlc = (player_id_index == 1); - if (ice_skating_dlc) - { - // @todo(mg): this is bad, we need a fixed timestep - float player_speed = 0.005f * TIME_STEP; - V2 player_ddp = V2_Scale(dir, player_speed); - player->dp = V2_Add(player->dp, player_ddp); - - float drag = -0.999f * TIME_STEP; - V2 player_drag = V2_Scale(player->dp, drag); - player->dp = V2_Add(player->dp, player_drag); - } - else - { - float player_speed = 5.f * TIME_STEP; - player->dp = V2_Scale(dir, player_speed); - } - } - - // movement & collision - ForU32(obj_id, app->object_count) - { - Object *obj = app->object_pool + obj_id; - obj->has_collision = false; - } - ForU32(obj_id, app->object_count) - { - Object *obj = app->object_pool + obj_id; - if (!(obj->flags & ObjectFlag_Move)) continue; - - obj->p = V2_Add(obj->p, obj->dp); - Object_UpdateCollisionVerticesAndNormals(obj); - - ForU32(collision_iteration, 8) // support up to 8 overlapping wall collisions - { - float closest_obstacle_separation_dist = FLT_MAX; - V2 closest_obstacle_wall_normal = {0}; - - ForU32(obstacle_id, app->object_count) - { - Object *obstacle = app->object_pool + obstacle_id; - if (!(obstacle->flags & ObjectFlag_Collide)) continue; - if (obj == obstacle) continue; - - float biggest_dist = -FLT_MAX; - V2 wall_normal = {0}; - - // @info(mg) SAT algorithm needs 2 iterations - // from the perspective of the obj - // and from the perspective of the obstacle. - ForU32(sat_iteration, 2) - { - Object *normal_obj; - Object *projected_obstacle; - CollisionProjectionResult a; - CollisionProjectionResult b; - if (sat_iteration == 0) - { - normal_obj = obj; - projected_obstacle = obstacle; - } - else - { - normal_obj = obstacle; - projected_obstacle = obj; - } - - a = Object_CollisionProjection(normal_obj->collision_normals, normal_obj->collision_vertices); // @speed(mg) we could cache these per object - b = Object_CollisionProjection(normal_obj->collision_normals, projected_obstacle->collision_vertices); - - ForArray(i, a.arr) - { - static_assert(ArrayCount(a.arr) == ArrayCount(obj->collision_normals.arr)); - V2 normal = normal_obj->collision_normals.arr[i]; - - V2 obj_col = Object_GetAvgCollisionPos(normal_obj); - V2 obstacle_col = Object_GetAvgCollisionPos(projected_obstacle); - V2 obstacle_dir = V2_Sub(obstacle_col, obj_col); - if (V2_Inner(normal, obstacle_dir) < 0) - { - continue; - } - - float d = RngF_MaxDistance(a.arr[i], b.arr[i]); - if (d > 0.f) - { - // @info(mg) We can exit early from checking this - // obstacle since we found an axis that has - // a separation between obj and obstacle. - goto skip_this_obstacle; - } - - if (d > biggest_dist) - { - biggest_dist = d; - if (normal_obj == obj) - { - wall_normal = V2_Reverse(normal); - } - else - { - wall_normal = normal; - } - } - } - } - - if (closest_obstacle_separation_dist > biggest_dist) - { - closest_obstacle_separation_dist = biggest_dist; - closest_obstacle_wall_normal = wall_normal; - } - - obj->has_collision |= (biggest_dist < 0.f); - obstacle->has_collision |= (biggest_dist < 0.f); - - skip_this_obstacle:; - } - - if (closest_obstacle_separation_dist < 0.f) - { - V2 move_out_dir = closest_obstacle_wall_normal; - float move_out_magnitude = -closest_obstacle_separation_dist; - - V2 move_out = V2_Scale(move_out_dir, move_out_magnitude); - obj->p = V2_Add(obj->p, move_out); - - // remove all velocity on collision axis - // we might want to do something different here! - if (move_out.x) obj->dp.x = 0; - if (move_out.y) obj->dp.y = 0; - - Object_UpdateCollisionVerticesAndNormals(obj); - } - else - { - // Collision not found, stop iterating - break; - } - } // collision_iteration - } // obj_id - - - // animate textures - ForU32(obj_id, app->object_count) - { - Object *obj = app->object_pool + obj_id; - if (Sprite_Get(app, obj->sprite_id)->frame_count <= 1) continue; - - Uint32 frame_index_map[8] = - { - 0, 1, 2, 1, - 0, 3, 4, 3 - }; - - bool in_idle_frame = (0 == frame_index_map[obj->sprite_animation_index]); - - float distance = V2_Length(V2_Sub(obj->p, obj->prev_p)); - float anim_speed = (16.f * TIME_STEP); - anim_speed += (3200.f * distance * TIME_STEP); - - if (!distance && in_idle_frame) - { - anim_speed = 0.f; - } - obj->sprite_animation_t += anim_speed; - - float period = 1.f; - while (obj->sprite_animation_t > period) - { - obj->sprite_animation_t -= period; - obj->sprite_animation_index += 1; - } - - obj->sprite_animation_index %= ArrayCount(frame_index_map); - obj->sprite_frame_index = frame_index_map[obj->sprite_animation_index]; - } - - // move camera - { - Object *player = Object_Get(app, app->player_ids[0]); - app->camera_p = player->p; - } -} static void Game_VerticesCameraTransform(AppState *app, V2 verts[4], float camera_scale, V2 window_transform) { @@ -243,7 +19,7 @@ static void Game_VerticesCameraTransform(AppState *app, V2 verts[4], float camer verts[i].y += window_transform.y; // fix y axis direction to +Y up (SDL uses +Y down, -Y up) - verts[i].y = app->height - verts[i].y; + verts[i].y = app->window_height - verts[i].y; } } @@ -271,10 +47,10 @@ static void Game_IssueDrawCommands(AppState *app) { float camera_scale = 1.f; { - float wh = Max(app->width, app->height); // pick bigger window dimension + float wh = Max(app->window_width, app->window_height); // pick bigger window dimension camera_scale = wh / app->camera_range; } - V2 window_transform = (V2){app->width*0.5f, app->height*0.5f}; + V2 window_transform = (V2){app->window_width*0.5f, app->window_height*0.5f}; ForU32(object_index, app->object_count) { @@ -379,6 +155,19 @@ static void Game_IssueDrawCommands(AppState *app) } } + // draw debug networking stuff + { + ColorF green = ColorF_RGB(0, 1, 0); + ColorF red = ColorF_RGB(1, 0, 0); + + SDL_FRect rect = { 0, 0, 30, 30 }; + if (!app->net.is_server) rect.x = 40; + + ColorF color = app->net.err ? red : green; + SDL_SetRenderDrawColorFloat(app->renderer, color.r, color.g, color.b, color.a); + SDL_RenderFillRect(app->renderer, &rect); + } + // draw mouse { static float r = 0; @@ -399,11 +188,13 @@ static void Game_IssueDrawCommands(AppState *app) static void Game_Iterate(AppState *app) { { + app->frame_id += 1; + Uint64 new_frame_time = SDL_GetTicks(); Uint64 delta_time = new_frame_time - app->frame_time; app->frame_time = new_frame_time; app->dt = delta_time * (0.001f); - app->physics_time_accumulator += Min(app->dt, 1.f); // clamp dt to 1s + app->tick_dt_accumulator += Min(app->dt, 1.f); // clamp dt to 1s if (app->debug.fixed_dt) { @@ -411,13 +202,16 @@ static void Game_Iterate(AppState *app) } } + Net_Iterate(app); + bool run_simulation = (!app->debug.pause_on_every_frame || !app->debug.paused_frame); if (run_simulation) { - while (app->physics_time_accumulator > TIME_STEP) + while (app->tick_dt_accumulator > TIME_STEP) { - app->physics_time_accumulator -= TIME_STEP; - Game_AdvanceSimulation(app); + app->tick_id += 1; + app->tick_dt_accumulator -= TIME_STEP; + Tick_Iterate(app); } } Game_IssueDrawCommands(app); @@ -432,6 +226,8 @@ static void Game_Init(AppState *app) app->debug.draw_collision_box = true; } + Net_Init(app); + app->frame_time = SDL_GetTicks(); app->object_count += 1; // reserve object under index 0 as special 'nil' value app->sprite_count += 1; // reserve sprite under index 0 as special 'nil' value diff --git a/src/de_main.h b/src/de_main.h index 3f65fc9..3f8eb7d 100644 --- a/src/de_main.h +++ b/src/de_main.h @@ -2,7 +2,11 @@ // Constants // --- #define ScaleMetersPerPixel 0.025f -#define TIME_STEP 1.f / 128.f +#define TIME_STEP (1.f / 128.f) + +#define NET_DEFAULT_SEVER_PORT 21037 +#define NET_MAGIC_VALUE 0xfda0'dead'beef'1234llu +#define NET_MAX_TICK_HISTORY 4096 typedef enum { Axis2_X, @@ -68,22 +72,44 @@ typedef struct bool has_collision; } Object; +typedef struct +{ + SDLNet_Address *address; + Uint16 port; +} Net_User; + +typedef struct +{ + V2 move_dir; + // action buttons etc will be added here +} TickInput; + typedef struct { // SDL, window stuff SDL_Window* window; SDL_Renderer* renderer; - int width, height; + int window_width, window_height; + int window_px, window_py; // initial window px, py specified by cmd options, 0 if wasn't set + bool window_on_top; + bool window_borderless; // user input V2 mouse; SDL_MouseButtonFlags mouse_keys; bool keyboard[SDL_SCANCODE_COUNT]; // true == key is down + // circular buffer with tick inputs + TickInput tick_input_buf[NET_MAX_TICK_HISTORY]; + Uint64 tick_input_min; + Uint64 tick_input_max; // one past last + // time + Uint64 frame_id; Uint64 frame_time; float dt; - float physics_time_accumulator; + Uint64 tick_id; + float tick_dt_accumulator; // objects Object object_pool[4096]; @@ -103,8 +129,32 @@ typedef struct // float camera_scale = Max(width, height) / camera_range float camera_range; + // networking + struct + { + bool err; // tracks if network is in error state + bool is_server; + SDLNet_DatagramSocket *socket; + + Uint8 buf[1024 * 1024 * 1]; // 1 MB scratch buffer for network payload construction + Uint32 buf_used; + bool buf_err; // true on overflows + + struct + { + Net_User users[16]; + Uint32 user_count; + } server; + struct + { + SDLNet_Address *server_address; + Uint16 server_port; + } client; + } net; + // debug - struct { + struct + { float fixed_dt; bool pause_on_every_frame; bool paused_frame; diff --git a/src/de_network.c b/src/de_network.c new file mode 100644 index 0000000..f4a2c83 --- /dev/null +++ b/src/de_network.c @@ -0,0 +1,201 @@ +typedef struct +{ + Uint64 magic_value; + Uint64 hash; // of all values post first 16 bytes +} Net_BufHeader; + +static Uint8 *Net_BufAlloc(AppState *app, Uint32 size) +{ + Uint8 *result = app->net.buf + app->net.buf_used; + { + // return ptr to start of the buffer + // on overflow + Uint8 *end = (app->net.buf + sizeof(app->net.buf)); + if (end < (result + size)) + { + Assert(false); + result = app->net.buf; + app->net.buf_err = true; + } + } + app->net.buf_used += size; + return result; +} + +static Uint8 *Net_BufMemcpy(AppState *app, void *data, Uint32 size) +{ + Uint8 *result = Net_BufAlloc(app, size); + memcpy(result, data, size); + return result; +} + + +static const char *Net_Label(AppState *app) +{ + return app->net.is_server ? "SERVER" : "CLIENT"; +} + +static Net_User *Net_FindUser(AppState *app, SDLNet_Address *address) +{ + ForU32(i, app->net.server.user_count) + { + if (app->net.server.users[i].address == address) + return app->net.server.users + i; + } + return 0; +} + +static Net_User *Net_AddUser(AppState *app, SDLNet_Address *address, Uint16 port) +{ + if (app->net.server.user_count < ArrayCount(app->net.server.users)) + { + Net_User *user = app->net.server.users + app->net.server.user_count; + app->net.server.user_count += 1; + user->address = SDLNet_RefAddress(address); + user->port = port; + return user; + } + return 0; +} + +static void Net_ReceiveData(AppState *app) +{ + for (;;) + { + SDLNet_Datagram *dgram = 0; + int receive = SDLNet_ReceiveDatagram(app->net.socket, &dgram); + if (!receive) break; + if (!dgram) break; + + SDL_Log("%s: got %d-byte datagram from %s:%d", + Net_Label(app), + (int)dgram->buflen, + SDLNet_GetAddressString(dgram->addr), + (int)dgram->port); + + if (!app->net.is_server) + { + if ((dgram->port != app->net.client.server_port) || + (SDLNet_CompareAddresses(dgram->addr, app->net.client.server_address) != 0)) + { + SDL_Log("%s: Ignoring message from non-server address %s:%d", + Net_Label(app), + SDLNet_GetAddressString(dgram->addr), (int)dgram->port); + goto datagram_cleanup; + } + } + + SDL_Log("%s MSG: %.*s", + Net_Label(app), + (int)dgram->buflen, (char *)dgram->buf); + + if (app->net.is_server) + { + if (!Net_FindUser(app, dgram->addr)) + { + SDL_Log("%s: saving user with port: %d", + Net_Label(app), (int)dgram->port); + Net_AddUser(app, dgram->addr, dgram->port); + } + } + + datagram_cleanup: + SDLNet_DestroyDatagram(dgram); + } +} + +static void Net_SendData(AppState *app) +{ + bool is_server = app->net.is_server; + bool is_client = !app->net.is_server; + + if (is_client) + { + Net_BufHeader header = {}; + header.magic_value = NET_MAGIC_VALUE; + + Uint8 *buf_header = Net_BufAlloc(app, sizeof(Net_BufHeader)); + (void)buf_header; + + + + } + + + if (is_client && ((app->tick_id % 640) == 0)) + { + char send_buf[] = "I'm a client and I like sending messages."; + bool send_res = SDLNet_SendDatagram(app->net.socket, + app->net.client.server_address, + app->net.client.server_port, + send_buf, sizeof(send_buf)); + SDL_Log("%s: SendDatagram result: %s", + Net_Label(app), send_res ? "success" : "fail"); + } + + if (is_server && ((app->tick_id % 1400) == 123)) + { + char send_buf[] = "Hello, I'm a server."; + ForU32(i, app->net.server.user_count) + { + Net_User *user = app->net.server.users + i; + + bool send_res = SDLNet_SendDatagram(app->net.socket, + user->address, + user->port, + send_buf, sizeof(send_buf)); + SDL_Log("%s: SendDatagram result: %s", + Net_Label(app), send_res ? "success" : "fail"); + } + } +} + +static void Net_Iterate(AppState *app) +{ + if (app->net.err) return; + Net_ReceiveData(app); + Net_SendData(app); +} + +static void Net_Init(AppState *app) +{ + SDL_Log(app->net.is_server ? "Launching as server" : "Launching as client"); + + if (!app->net.is_server) + { + const char *hostname = "localhost"; + SDL_Log("%s: Resolving server hostname '%s' ...", + Net_Label(app), hostname); + app->net.client.server_address = SDLNet_ResolveHostname(hostname); + app->net.client.server_port = NET_DEFAULT_SEVER_PORT; + if (app->net.client.server_address) + { + if (SDLNet_WaitUntilResolved(app->net.client.server_address, -1) < 0) + { + SDLNet_UnrefAddress(app->net.client.server_address); + app->net.client.server_address = 0; + } + } + + if (!app->net.client.server_address) + { + app->net.err = true; + SDL_Log("%s: Failed to resolve server hostname '%s'", + Net_Label(app), hostname); + } + } + + Uint16 port = (app->net.is_server ? NET_DEFAULT_SEVER_PORT : 0); + app->net.socket = SDLNet_CreateDatagramSocket(0, port); + if (!app->net.socket) + { + app->net.err = true; + SDL_Log("%s: Failed to create socket", + Net_Label(app)); + } +} + +static void Net_Deinit() +{ + Assert(!"@todo"); +} diff --git a/src/de_tick.c b/src/de_tick.c new file mode 100644 index 0000000..58ee0d6 --- /dev/null +++ b/src/de_tick.c @@ -0,0 +1,236 @@ +// +// Physic tick update +// @todo this should run on a separate thread +// +static TickInput *Tick_PollInput(AppState *app) +{ + // select slot from circular buffer + Uint64 current = app->tick_input_max % ArrayCount(app->tick_input_buf); + { + app->tick_input_max += 1; + Uint64 max = app->tick_input_max % ArrayCount(app->tick_input_buf); + Uint64 min = app->tick_input_min % ArrayCount(app->tick_input_buf); + if (min == max) + app->tick_input_min += 1; + } + + TickInput *input = app->tick_input_buf + current; + + V2 dir = {0}; + if (app->keyboard[SDL_SCANCODE_W] || app->keyboard[SDL_SCANCODE_UP]) dir.y += 1; + if (app->keyboard[SDL_SCANCODE_S] || app->keyboard[SDL_SCANCODE_DOWN]) dir.y -= 1; + if (app->keyboard[SDL_SCANCODE_A] || app->keyboard[SDL_SCANCODE_LEFT]) dir.x -= 1; + if (app->keyboard[SDL_SCANCODE_D] || app->keyboard[SDL_SCANCODE_RIGHT]) dir.x += 1; + input->move_dir = V2_Normalize(dir); + + return input; +} + +static void Tick_Iterate(AppState *app) +{ + TickInput *input = Tick_PollInput(app); + + // update prev_p + ForU32(obj_id, app->object_count) + { + Object *obj = app->object_pool + obj_id; + obj->prev_p = obj->p; + } + + // animate special wall + { + Object *obj = Object_Get(app, app->special_wall); + obj->collision_rotation = WrapF(0.f, 1.f, obj->collision_rotation + TIME_STEP); + obj->sprite_rotation = obj->collision_rotation; + Object_UpdateCollisionVerticesAndNormals(obj); + } + + // player input + ForArray(player_id_index, app->player_ids) + { + Object *player = Object_PlayerFromIndex(app, player_id_index); + + bool ice_skating_dlc = (player_id_index == 1); + if (ice_skating_dlc) + { + // @todo(mg): this is bad, we need a fixed timestep + float player_speed = 0.005f * TIME_STEP; + V2 player_ddp = V2_Scale(input->move_dir, player_speed); + player->dp = V2_Add(player->dp, player_ddp); + + float drag = -0.999f * TIME_STEP; + V2 player_drag = V2_Scale(player->dp, drag); + player->dp = V2_Add(player->dp, player_drag); + } + else + { + float player_speed = 5.f * TIME_STEP; + player->dp = V2_Scale(input->move_dir, player_speed); + } + } + + // movement & collision + ForU32(obj_id, app->object_count) + { + Object *obj = app->object_pool + obj_id; + obj->has_collision = false; + } + ForU32(obj_id, app->object_count) + { + Object *obj = app->object_pool + obj_id; + if (!(obj->flags & ObjectFlag_Move)) continue; + + obj->p = V2_Add(obj->p, obj->dp); + Object_UpdateCollisionVerticesAndNormals(obj); + + ForU32(collision_iteration, 8) // support up to 8 overlapping wall collisions + { + float closest_obstacle_separation_dist = FLT_MAX; + V2 closest_obstacle_wall_normal = {0}; + + ForU32(obstacle_id, app->object_count) + { + Object *obstacle = app->object_pool + obstacle_id; + if (!(obstacle->flags & ObjectFlag_Collide)) continue; + if (obj == obstacle) continue; + + float biggest_dist = -FLT_MAX; + V2 wall_normal = {0}; + + // @info(mg) SAT algorithm needs 2 iterations + // from the perspective of the obj + // and from the perspective of the obstacle. + ForU32(sat_iteration, 2) + { + Object *normal_obj; + Object *projected_obstacle; + CollisionProjectionResult a; + CollisionProjectionResult b; + if (sat_iteration == 0) + { + normal_obj = obj; + projected_obstacle = obstacle; + } + else + { + normal_obj = obstacle; + projected_obstacle = obj; + } + + a = Object_CollisionProjection(normal_obj->collision_normals, normal_obj->collision_vertices); // @speed(mg) we could cache these per object + b = Object_CollisionProjection(normal_obj->collision_normals, projected_obstacle->collision_vertices); + + ForArray(i, a.arr) + { + static_assert(ArrayCount(a.arr) == ArrayCount(obj->collision_normals.arr)); + V2 normal = normal_obj->collision_normals.arr[i]; + + V2 obj_col = Object_GetAvgCollisionPos(normal_obj); + V2 obstacle_col = Object_GetAvgCollisionPos(projected_obstacle); + V2 obstacle_dir = V2_Sub(obstacle_col, obj_col); + if (V2_Inner(normal, obstacle_dir) < 0) + { + continue; + } + + float d = RngF_MaxDistance(a.arr[i], b.arr[i]); + if (d > 0.f) + { + // @info(mg) We can exit early from checking this + // obstacle since we found an axis that has + // a separation between obj and obstacle. + goto skip_this_obstacle; + } + + if (d > biggest_dist) + { + biggest_dist = d; + if (normal_obj == obj) + { + wall_normal = V2_Reverse(normal); + } + else + { + wall_normal = normal; + } + } + } + } + + if (closest_obstacle_separation_dist > biggest_dist) + { + closest_obstacle_separation_dist = biggest_dist; + closest_obstacle_wall_normal = wall_normal; + } + + obj->has_collision |= (biggest_dist < 0.f); + obstacle->has_collision |= (biggest_dist < 0.f); + + skip_this_obstacle:; + } + + if (closest_obstacle_separation_dist < 0.f) + { + V2 move_out_dir = closest_obstacle_wall_normal; + float move_out_magnitude = -closest_obstacle_separation_dist; + + V2 move_out = V2_Scale(move_out_dir, move_out_magnitude); + obj->p = V2_Add(obj->p, move_out); + + // remove all velocity on collision axis + // we might want to do something different here! + if (move_out.x) obj->dp.x = 0; + if (move_out.y) obj->dp.y = 0; + + Object_UpdateCollisionVerticesAndNormals(obj); + } + else + { + // Collision not found, stop iterating + break; + } + } // collision_iteration + } // obj_id + + + // animate textures + ForU32(obj_id, app->object_count) + { + Object *obj = app->object_pool + obj_id; + if (Sprite_Get(app, obj->sprite_id)->frame_count <= 1) continue; + + Uint32 frame_index_map[8] = + { + 0, 1, 2, 1, + 0, 3, 4, 3 + }; + + bool in_idle_frame = (0 == frame_index_map[obj->sprite_animation_index]); + + float distance = V2_Length(V2_Sub(obj->p, obj->prev_p)); + float anim_speed = (16.f * TIME_STEP); + anim_speed += (3200.f * distance * TIME_STEP); + + if (!distance && in_idle_frame) + { + anim_speed = 0.f; + } + obj->sprite_animation_t += anim_speed; + + float period = 1.f; + while (obj->sprite_animation_t > period) + { + obj->sprite_animation_t -= period; + obj->sprite_animation_index += 1; + } + + obj->sprite_animation_index %= ArrayCount(frame_index_map); + obj->sprite_frame_index = frame_index_map[obj->sprite_animation_index]; + } + + // move camera + { + Object *player = Object_Get(app, app->player_ids[0]); + app->camera_p = player->p; + } +} diff --git a/src/main.c b/src/main.c index b2b0fc1..fc5b2ec 100644 --- a/src/main.c +++ b/src/main.c @@ -8,15 +8,18 @@ #include #include #include +#include -#define WINDOW_HEIGHT 900 -#define WINDOW_WIDTH 1600 +#define WINDOW_HEIGHT 640 +#define WINDOW_WIDTH 854 #include "de_base.h" #include "de_math.h" #include "de_main.h" #include "de_sprite.c" #include "de_object.c" +#include "de_network.c" +#include "de_tick.c" #include "de_main.c" SDL_AppResult SDL_AppIterate(void* appstate) @@ -59,8 +62,8 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) case SDL_EVENT_WINDOW_RESIZED: { - app->width = event->window.data1; - app->height = event->window.data2; + app->window_width = event->window.data1; + app->window_height = event->window.data2; } break; case SDL_EVENT_KEY_UP: @@ -86,10 +89,70 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) return SDL_APP_CONTINUE; } +static void Game_ParseCmd(AppState *app, int argc, char** argv) +{ + for (int i = 1; i < argc; i += 1) + { + const char *arg = argv[i]; + if (0 == strcmp(arg, "-server")) + { + app->net.is_server = true; + } + else if (0 == strcmp(arg, "-top")) + { + app->window_on_top = true; + } + else if (0 == strcmp(arg, "-b")) + { + app->window_borderless = true; + } + else if (0 == strcmp(arg, "-w") || + 0 == strcmp(arg, "-h") || + 0 == strcmp(arg, "-px") || + 0 == strcmp(arg, "-py")) + { + bool found_number = false; + if (i + 1 < argc) + { + i += 1; + const char *next_arg = argv[i]; + + int number = SDL_strtoul(next_arg, 0, 0); + if (number > 0) + { + found_number = true; + if (0 == strcmp(arg, "-w")) app->window_width = number; + else if (0 == strcmp(arg, "-h")) app->window_height = number; + else if (0 == strcmp(arg, "-px")) app->window_px = number; + else if (0 == strcmp(arg, "-py")) app->window_py = number; + } + } + + if (!found_number) + { + SDL_Log("%s needs to be followed by positive number", arg); + } + } + else + { + SDL_Log("Unhandled argument: %s", arg); + } + } +} + SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) { - (void)argc; - (void)argv; + *appstate = SDL_calloc(1, sizeof(AppState)); + AppState *app = (AppState *)*appstate; + if (!app) + { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Failed to allocate appstate", SDL_GetError(), NULL); + return SDL_APP_FAILURE; + } + app->window_width = WINDOW_WIDTH; + app->window_height = WINDOW_HEIGHT; + + Game_ParseCmd(app, argc, argv); if (!SDL_Init(SDL_INIT_VIDEO)) { @@ -97,26 +160,36 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) return SDL_APP_FAILURE; } - *appstate = SDL_calloc(1, sizeof(AppState)); - AppState* state = (AppState*)*appstate; - if (!state) + if (!SDLNet_Init()) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Failed to allocate appstate", SDL_GetError(), NULL); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Failed to initialize SDL3 Net.", SDL_GetError(), NULL); return SDL_APP_FAILURE; } - state->width = WINDOW_WIDTH; - state->height = WINDOW_HEIGHT; - if (!SDL_CreateWindowAndRenderer("demongus", state->width, state->height, - SDL_WINDOW_RESIZABLE, &state->window, &state->renderer)) + SDL_WindowFlags window_flags = SDL_WINDOW_RESIZABLE|SDL_WINDOW_HIDDEN; + window_flags |= (app->window_on_top ? SDL_WINDOW_ALWAYS_ON_TOP : 0); + window_flags |= (app->window_borderless ? SDL_WINDOW_BORDERLESS : 0); + + if (!SDL_CreateWindowAndRenderer("demongus", + app->window_width, app->window_height, + window_flags, + &app->window, &app->renderer)) { SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Failed to create window/renderer", SDL_GetError(), NULL); return SDL_APP_FAILURE; } - SDL_SetRenderDrawBlendMode(state->renderer, SDL_BLENDMODE_BLEND); + if (app->window_px || app->window_py) + { + int x = (app->window_px ? app->window_px : SDL_WINDOWPOS_UNDEFINED); + int y = (app->window_py ? app->window_py : SDL_WINDOWPOS_UNDEFINED); + SDL_SetWindowPosition(app->window, x, y); + } + + SDL_ShowWindow(app->window); - Game_Init(state); + SDL_SetRenderDrawBlendMode(app->renderer, SDL_BLENDMODE_BLEND); + Game_Init(app); return SDL_APP_CONTINUE; }