From cf0ec43631bf8adb72988ace4419b6e9a9d31eca Mon Sep 17 00:00:00 2001 From: blubs Date: Wed, 2 Aug 2023 01:27:40 -0700 Subject: [PATCH] Fixes funnel algorithm for various edge cases Adds server-side navmesh v0.1.0 loading Refactors AI_Chase code --- source/client/navmesh_editor.qc | 160 +++--- source/server/ai/chase_ai.qc | 374 ++++++-------- source/server/ai/navmesh_core.qc | 82 +++- source/server/ai/pathfind_core.qc | 779 ++++++++++++++++++++++++++++++ source/server/ai/zombie_core.qc | 2 +- source/server/defs/custom.qc | 95 +++- source/server/main.qc | 15 +- source/shared/navmesh_defines.qc | 2 +- 8 files changed, 1170 insertions(+), 339 deletions(-) create mode 100644 source/server/ai/pathfind_core.qc diff --git a/source/client/navmesh_editor.qc b/source/client/navmesh_editor.qc index dd320e2..d883a4f 100644 --- a/source/client/navmesh_editor.qc +++ b/source/client/navmesh_editor.qc @@ -1479,15 +1479,15 @@ void cl_navmesh_calc_connected_polies() { cl_navmesh_polies[j].connected_polies_count++; //======================================================================================= - print("Poly "); - print(ftos(i)); - print(" is connected to "); - print(ftos(j)); - print(" and poly "); - print(ftos(j)); - print(" is connected to "); - print(ftos(i)); - print(".\n"); + // print("Poly "); + // print(ftos(i)); + // print(" is connected to "); + // print(ftos(j)); + // print(" and poly "); + // print(ftos(j)); + // print(" is connected to "); + // print(ftos(i)); + // print(".\n"); } } } @@ -1616,7 +1616,7 @@ void cl_navmesh_calc_polies_centers() //Saves the current cl_navmesh to a file void cl_navmesh_editor_save_navmesh() { int v_major = 0; - int v_minor = 0; + int v_minor = 1; int v_patch = 0; float file; string h; @@ -1878,10 +1878,15 @@ void cl_navmesh_editor_load_navmesh() { fgets(file);//1 fgets(file);//2 fgets(file);//3 - // Don't care about connected traversals - float n_traversals = stof(fgets(file)); - for(float j = 0; j < n_traversals; j++) { - fgets(file); // discard the j-th traversal index + + // If navmesh file version 0.0.0, no traversals + // FIXME - Need a better way to do this.. + if(strcmp(nav_file_version, "0.0.0") != 0) { + // Don't care about connected traversals + float n_traversals = stof(fgets(file)); + for(float j = 0; j < n_traversals; j++) { + fgets(file); // discard the j-th traversal index + } } // Load doortarget field @@ -1899,31 +1904,33 @@ void cl_navmesh_editor_load_navmesh() { // ----------------------------------------------------------------------- // Load Traversals // ----------------------------------------------------------------------- - // TODO - Increment navmesh version count, if 0.0.0, load file without traversals - //Next line contains the number of traverals - cl_navmesh_traversal_count = stof(fgets(file)); - - //The next lines are each traversal - for(float i = 0; i < cl_navmesh_traversal_count; i++) { - line = fgets(file); - temp = stov(line); - cl_navmesh_traversals[i].start_pos.x = temp.x; - cl_navmesh_traversals[i].start_pos.y = temp.y; - cl_navmesh_traversals[i].start_pos.z = temp.z; - line = fgets(file); - temp = stov(line); - cl_navmesh_traversals[i].midpoint_pos.x = temp.x; - cl_navmesh_traversals[i].midpoint_pos.y = temp.y; - cl_navmesh_traversals[i].midpoint_pos.z = temp.z; - line = fgets(file); - temp = stov(line); - cl_navmesh_traversals[i].end_pos.x = temp.x; - cl_navmesh_traversals[i].end_pos.y = temp.y; - cl_navmesh_traversals[i].end_pos.z = temp.z; - cl_navmesh_traversals[i].angle = stof(fgets(file)); - cl_navmesh_traversals[i].use_midpoint = stof(fgets(file)); - // Don't care about traversal end polygon index - fgets(file); + // If navmesh file version 0.0.0, no traversals + if(strcmp(nav_file_version, "0.0.0") != 0) { + // Next line contains the number of traverals + cl_navmesh_traversal_count = stof(fgets(file)); + + // Next lines are each traversal + for(float i = 0; i < cl_navmesh_traversal_count; i++) { + line = fgets(file); + temp = stov(line); + cl_navmesh_traversals[i].start_pos.x = temp.x; + cl_navmesh_traversals[i].start_pos.y = temp.y; + cl_navmesh_traversals[i].start_pos.z = temp.z; + line = fgets(file); + temp = stov(line); + cl_navmesh_traversals[i].midpoint_pos.x = temp.x; + cl_navmesh_traversals[i].midpoint_pos.y = temp.y; + cl_navmesh_traversals[i].midpoint_pos.z = temp.z; + line = fgets(file); + temp = stov(line); + cl_navmesh_traversals[i].end_pos.x = temp.x; + cl_navmesh_traversals[i].end_pos.y = temp.y; + cl_navmesh_traversals[i].end_pos.z = temp.z; + cl_navmesh_traversals[i].angle = stof(fgets(file)); + cl_navmesh_traversals[i].use_midpoint = stof(fgets(file)); + // Don't care about traversal end polygon index + fgets(file); + } } // ----------------------------------------------------------------------- @@ -2397,6 +2404,8 @@ void cl_pathfind_smooth_path(vector start_point, vector goal_point) { vector cur_funnel_corner = start_point; vector cur_funnel_left = cl_test_pathfind_result->portals_left_pos[0]; vector cur_funnel_right = cl_test_pathfind_result->portals_right_pos[0]; + float cur_funnel_left_portal_idx = 0; + float cur_funnel_right_portal_idx = 0; float next_portal_left_in_funnel; float next_portal_right_in_funnel; @@ -2415,51 +2424,30 @@ void cl_pathfind_smooth_path(vector start_point, vector goal_point) { // TODO < -1 outside of funnel to the left // TODO > 1 outside of funnel to the right - float cur_portal_left_in_funnel = pathfind_point_in_funnel( cur_portal_left_pos, cur_funnel_corner, cur_funnel_left, cur_funnel_right); float cur_portal_right_in_funnel = pathfind_point_in_funnel(cur_portal_right_pos, cur_funnel_corner, cur_funnel_left, cur_funnel_right); - print("Current portal: "); - print(ftos(i)); - print(" (l: "); - print(ftos(cur_portal_left_pos.x)); - print(","); - print(ftos(cur_portal_left_pos.y)); - print(", r: "); - print(ftos(cur_portal_right_pos.x)); - print(","); - print(ftos(cur_portal_right_pos.y)); - print(", t: "); - print(ftos(cur_portal_traversal)); - print(") -"); - print(" In funnel? ("); - print(ftos(cur_portal_left_in_funnel)); - print(","); - print(ftos(cur_portal_right_in_funnel)); - print(") - "); - print("Funnel: ("); - print(ftos(cur_funnel_left.x)); - print(","); - print(ftos(cur_funnel_left.y)); - print(" -> "); - print(ftos(cur_funnel_corner.x)); - print(","); - print(ftos(cur_funnel_corner.y)); - print(" -> "); - print(ftos(cur_funnel_right.x)); - print(","); - print(ftos(cur_funnel_right.y)); - print(")\n"); + print("{'portal_idx': ", ftos(i), ", 'portal': "); + print(" [(", ftos(cur_portal_left_pos.x), ",", ftos(cur_portal_left_pos.y), "), (", ftos(cur_portal_right_pos.x), ",", ftos(cur_portal_right_pos.y)); + print(")], 'traversal': ", ftos(cur_portal_traversal), ", "); + print("'funnel_result': (", ftos(cur_portal_left_in_funnel), ",", ftos(cur_portal_right_in_funnel), "), "); + print("'funnel': ["); + print("(", ftos(cur_funnel_left.x), ",", ftos(cur_funnel_left.y), "), "); + print("(", ftos(cur_funnel_corner.x), ",", ftos(cur_funnel_corner.y), "), "); + print("(", ftos(cur_funnel_right.x), ",", ftos(cur_funnel_right.y), ")"); + print("]},\n"); // print(vtos(cur_funnel_left)); // If cur portal left point is in cur funnel, narrow the cur funnel if(cur_portal_left_in_funnel >= -1 && cur_portal_left_in_funnel <= 1) { cur_funnel_left = cur_portal_left_pos; + cur_funnel_left_portal_idx = i; } // If cur portal right point is in cur funnel, narrow the cur funnel if(cur_portal_right_in_funnel >= -1 && cur_portal_right_in_funnel <= 1) { cur_funnel_right = cur_portal_right_pos; + cur_funnel_right_portal_idx = i; } // If cur portal is completely outside (to left of) current funnel @@ -2469,12 +2457,18 @@ void cl_pathfind_smooth_path(vector start_point, vector goal_point) { cl_test_pathfind_result->point_path_traversals[cl_test_pathfind_result->point_path_length] = -1; cl_test_pathfind_result->point_path_length++; + // Update funnel to start at the current funnel left endpoint, pointing to the next portal in the list // Set next funnel to start at cur funnel left point cur_funnel_corner = cur_funnel_left; - // Set next funnel left point to next portal left - cur_funnel_left = cl_test_pathfind_result->portals_left_pos[i+1]; - // Set next funnel right point to next portal right - cur_funnel_right = cl_test_pathfind_result->portals_right_pos[i+1]; + + // Advance the funnel's left point to the next portal + cur_funnel_left_portal_idx += 1; + cur_funnel_right = cl_test_pathfind_result->portals_right_pos[cur_funnel_left_portal_idx]; + cur_funnel_left = cl_test_pathfind_result->portals_left_pos[cur_funnel_left_portal_idx]; + cur_funnel_right_portal_idx = cur_funnel_left_portal_idx; + // Restart funnel algorithm from the funnel's endpoint + i = cur_funnel_left_portal_idx - 1; + continue; } // If cur portal is completely outside (to right of) current funnel else if(cur_portal_left_in_funnel > 1 && cur_portal_right_in_funnel > 1) { @@ -2483,12 +2477,18 @@ void cl_pathfind_smooth_path(vector start_point, vector goal_point) { cl_test_pathfind_result->point_path_traversals[cl_test_pathfind_result->point_path_length] = -1; cl_test_pathfind_result->point_path_length++; + // Update funnel to start at the current funnel right endpoint, pointing to the next portal in the list // Set next funnel to start at cur funnel right point cur_funnel_corner = cur_funnel_right; - // Set next funnel left point to next portal left - cur_funnel_left = cl_test_pathfind_result->portals_left_pos[i+1]; - // Set next funnel right point to next portal right - cur_funnel_right = cl_test_pathfind_result->portals_right_pos[i+1]; + + // Advance the funnel's right point to the next portal + cur_funnel_right_portal_idx += 1; + cur_funnel_right = cl_test_pathfind_result->portals_right_pos[cur_funnel_right_portal_idx]; + cur_funnel_left = cl_test_pathfind_result->portals_left_pos[cur_funnel_right_portal_idx]; + cur_funnel_left_portal_idx = cur_funnel_right_portal_idx; + // Restart funnel algorithm from the funnel's endpoint + i = cur_funnel_right_portal_idx - 1; + continue; } // If cur portal is a traversal @@ -2510,6 +2510,8 @@ void cl_pathfind_smooth_path(vector start_point, vector goal_point) { cur_funnel_corner = end_pos; cur_funnel_left = cl_test_pathfind_result->portals_left_pos[i+1]; cur_funnel_right = cl_test_pathfind_result->portals_right_pos[i+1]; + cur_funnel_left_portal_idx = i+1; + cur_funnel_right_portal_idx = i+1; } } diff --git a/source/server/ai/chase_ai.qc b/source/server/ai/chase_ai.qc index cef1922..22acf5c 100644 --- a/source/server/ai/chase_ai.qc +++ b/source/server/ai/chase_ai.qc @@ -1,36 +1,9 @@ -var struct framegroup { - float start_frame; - float end_frame; - float duration; - // void(entity) start_callback; // Called at the first frame of the range - void(entity) frame_callback; // Called at every frame of the range (after start / finish callbacks) - // void(entity) end_callback; // Called at the last frame of the range - // FIXME - Increasing the size of this framegroup by one more function pointer causes chaos... - // I assume I run out of space to pass framegroups along as arguments at any point... - // It starts to write into weird memory locations, like overriding self / world... things just break down. - // TODO - How can I refactor this to work? - void(entity) think_callback; // Called at each think invocation - // -- Maybe I can get rid of start / end callbacks? idk - // I should think about how I actually plan on using these... - // TODO - Get rid of start / end callback, add fields to the AI_Chase class that denotes - // TODO - These callbacks will be executed at the same time as frame anyways. -}; // FIXME - This framegroup nonsense is causing chaos... the struct is too large apparently... what can I cut out? -framegroup make_empty_framegroup(__out framegroup fg) { - fg.start_frame = -1; - fg.end_frame = -1; - fg.duration = 1.0; - // fg.start_callback = SUB_Null; - fg.frame_callback = SUB_Null; - // fg.end_callback = SUB_Null; - fg.think_callback = SUB_Null; -} - void init_framegroup( float start_frame, float end_frame, float duration, void(entity) frame_callback, void(entity) think_callback, @@ -44,214 +17,157 @@ void init_framegroup( float start_frame, float end_frame, float duration, +// Constructor. Called when calling `spawn(AI_Chase);` +void() AI_Chase::AI_Chase = { + this.path_target = world; + this.path_pos = world.origin; + this.think_delta_time = 0.1; // Call `this.think();` 10x per second + this.cur_fg_idx = 0; +}; - -class AI_Chase : entity { - // framegroup_registry *th_reg; - entity path_target; // If specified, path towards entity - vector path_pos; // Otherwise, path towards location specified - float think_delta_time; // Delta time (seconds) between each `this.think();` - - // ------------------------------ - // Current Framegroup value vars - // ------------------------------ - float cur_fg_start_time; - float cur_fg_start_frame; - float cur_fg_end_frame; - float cur_fg_duration; - // void() endanimfunc = SUB_Null; - // virtual void(entity) cur_fg_start_callback = SUB_Null; - virtual void(entity) cur_fg_frame_callback = SUB_Null; - // virtual void(entity) cur_fg_end_callback = SUB_Null; - virtual void(entity) cur_fg_think_callback = SUB_Null; - // ------------------------------ - // Framegroup state vars - // ------------------------------ - float cur_fg_idx; // Counter for checking when the framegroup has been changed - float cur_fg_frame_callback_next_frame; - float cur_fg_frame_callback_is_start_frame; // Is set to true when performing cur_fg_frame_callback for the first framegroup frame - float cur_fg_frame_callback_is_end_frame; // Is set to true when performing cur_fg_frame_callback for the final framegroup frame - - // ------------------------------ - - - // Constructor. Called when calling `spawn(AI_Chase);` - virtual void() AI_Chase = { - this.path_target = world; - this.path_pos = world.origin; - this.think_delta_time = 0.1; // Call `this.think();` 10x per second - this.cur_fg_idx = 0; - }; - - - virtual void() think = { - if(this.cur_fg_start_time >= 0) { - float cur_framegroup_idx = this.cur_fg_idx; - float dt = time - this.cur_fg_start_time; - float lerp_frac = (dt / this.cur_fg_duration); - // Frame ranges are inclusive, do an un-clamped interpolation to include the final frame - float cur_frame = unclamped_lerp(this.cur_fg_start_frame, this.cur_fg_end_frame, lerp_frac); - // For rendering, keep frame number always within current animation - if(this.cur_fg_end_frame > this.cur_fg_start_frame) { - this.frame = clamp(cur_frame, this.cur_fg_start_frame, this.cur_fg_end_frame); - } - else { - this.frame = clamp(cur_frame, this.cur_fg_end_frame, this.cur_fg_start_frame); - } - - // print("Cur frame: "); - // print(ftos(this.frame)); - // print(", start_frame: "); - // print(ftos(this.cur_fg_start_frame)); - // print(", end_frame: "); - // print(ftos(this.cur_fg_end_frame)); - // print(", cur_frame: "); - // print(ftos(cur_frame)); - // print(", next frame callback: "); - // print(ftos(this.cur_fg_frame_callback_next_frame)); - // print(", duration: "); - // print(ftos(this.cur_fg_duration)); - // print("\n"); - - // TODO - round frame to nearest frame? - - // If the animation is playing forward (low frame num to high frame num) - if(this.cur_fg_end_frame > this.cur_fg_start_frame) { - if(cur_frame >= this.cur_fg_frame_callback_next_frame) { - if(cur_frame >= this.cur_fg_end_frame + 1) { - this.cur_fg_frame_callback_is_end_frame = true; - } - this.cur_fg_frame_callback(this); // FIXME - This can override and invoke new things, need to be careful - // Callback may have assigned new framegroup - if(this.cur_fg_idx == cur_framegroup_idx) { - this.cur_fg_frame_callback_is_start_frame = false; - this.cur_fg_frame_callback_is_end_frame = false; - this.cur_fg_frame_callback_next_frame = floor(cur_frame) + 1; - } - // print("\tSet next frame callback frame to: "); - // print(ftos(this.cur_fg_frame_callback_next_frame)); - // print("\n"); - } - } - // else the animation is playing in reverse (high frame num to low frame num) - else { - if(cur_frame <= this.cur_fg_frame_callback_next_frame) { - if(cur_frame <= this.cur_fg_end_frame - 1) { - this.cur_fg_frame_callback_is_end_frame = true; - } - this.cur_fg_frame_callback(this); - // Callback may have assigned new framegroup - if(this.cur_fg_idx == cur_fg_idx) { - this.cur_fg_frame_callback_is_start_frame = false; - this.cur_fg_frame_callback_is_end_frame = false; - this.cur_fg_frame_callback_next_frame = ceil(cur_frame) - 1; - } - } - } - // Callback may have assigned new framegroup - // if(this.cur_fg_idx == cur_fg_idx) { - this.cur_fg_think_callback(this); - // } - } - this.nextthink = time + this.think_delta_time; - }; - - virtual void (float duration) set_framegroup_duration = { - // If no current framegroup, stop - if(this.cur_fg_start_time < 0) { - print("Warning: Attempted to set framegroup duration without an active framegroup.\n"); - return; - } - - this.cur_fg_duration = duration; - // -------------------------------------------------------------------- - // Adjust think_delta_time so no frames are skipped - // -------------------------------------------------------------------- - float n_anim_frames = fabs(this.cur_fg_end_frame - this.cur_fg_start_frame); - float time_per_frame = duration / n_anim_frames; - // If faster than 10FPS, make thinks be called more often to not miss a frame callback - if(time_per_frame < 0.10) { - this.think_delta_time = time_per_frame * 0.5; +void() AI_Chase::think = { + if(this.cur_fg_start_time >= 0) { + float cur_framegroup_idx = this.cur_fg_idx; + float dt = time - this.cur_fg_start_time; + float lerp_frac = (dt / this.cur_fg_duration); + // Frame ranges are inclusive, do an un-clamped interpolation to include the final frame + float cur_frame = unclamped_lerp(this.cur_fg_start_frame, this.cur_fg_end_frame, lerp_frac); + // For rendering, keep frame number always within current animation + if(this.cur_fg_end_frame > this.cur_fg_start_frame) { + this.frame = clamp(cur_frame, this.cur_fg_start_frame, this.cur_fg_end_frame); } else { - this.think_delta_time = 0.1; + this.frame = clamp(cur_frame, this.cur_fg_end_frame, this.cur_fg_start_frame); } - // -------------------------------------------------------------------- - }; - virtual void (framegroup fgroup) set_framegroup = { - // Increment framegroup counter. Reset every 100, we only use this to check when it has changed - this.cur_fg_idx = (this.cur_fg_idx + 1) % 100; - - // FIXME - frame callback isn't guaranteed to be called for every frame because sometimes we skip over frames - // FIXME - I should modify the think delta time to run at 80% of the time between frames, or 0.10 seconds, whichever is greater. - // FIXME - That way, if are playing an animation really fast, it'll call think more often - this.cur_fg_start_frame = fgroup.start_frame; - this.cur_fg_end_frame = fgroup.end_frame; - this.cur_fg_frame_callback = fgroup.frame_callback; - this.cur_fg_think_callback = fgroup.think_callback; - this.set_framegroup_duration(fgroup.duration); - - // Start the animation now - this.cur_fg_frame_callback_is_start_frame = true; - this.cur_fg_frame_callback_is_end_frame = false; - this.frame = this.cur_fg_start_frame; - this.cur_fg_start_time = time; - this.cur_fg_frame_callback_next_frame = this.cur_fg_start_frame; - // print("Done setting framegroup! Start frame: "); - // print(ftos(fgroup.start_frame)); - // print(", End frame: "); - // print(ftos(fgroup.end_frame)); + // print("Cur frame: "); + // print(ftos(this.frame)); + // print(", start_frame: "); + // print(ftos(this.cur_fg_start_frame)); + // print(", end_frame: "); + // print(ftos(this.cur_fg_end_frame)); + // print(", cur_frame: "); + // print(ftos(cur_frame)); + // print(", next frame callback: "); + // print(ftos(this.cur_fg_frame_callback_next_frame)); + // print(", duration: "); + // print(ftos(this.cur_fg_duration)); // print("\n"); - }; + // TODO - round frame to nearest frame? - virtual void () fg_die = {}; - virtual void () fg_walk = {}; - virtual void () fg_attack = {}; - virtual void () fg_idle = {}; - - - virtual void (float dist) do_walk_to_goal = { - if(dist == 0) { - return; + // If the animation is playing forward (low frame num to high frame num) + if(this.cur_fg_end_frame > this.cur_fg_start_frame) { + if(cur_frame >= this.cur_fg_frame_callback_next_frame) { + if(cur_frame >= this.cur_fg_end_frame + 1) { + this.cur_fg_frame_callback_is_end_frame = true; + } + this.cur_fg_frame_callback(this); // FIXME - This can override and invoke new things, need to be careful + // Callback may have assigned new framegroup + if(this.cur_fg_idx == cur_framegroup_idx) { + this.cur_fg_frame_callback_is_start_frame = false; + this.cur_fg_frame_callback_is_end_frame = false; + this.cur_fg_frame_callback_next_frame = floor(cur_frame) + 1; + } + // print("\tSet next frame callback frame to: "); + // print(ftos(this.cur_fg_frame_callback_next_frame)); + // print("\n"); + } } - - vector goal_pos = path_pos; - if(this.path_target != world) { - goal_pos = this.path_target.origin; + // else the animation is playing in reverse (high frame num to low frame num) + else { + if(cur_frame <= this.cur_fg_frame_callback_next_frame) { + if(cur_frame <= this.cur_fg_end_frame - 1) { + this.cur_fg_frame_callback_is_end_frame = true; + } + this.cur_fg_frame_callback(this); + // Callback may have assigned new framegroup + if(this.cur_fg_idx == cur_fg_idx) { + this.cur_fg_frame_callback_is_start_frame = false; + this.cur_fg_frame_callback_is_end_frame = false; + this.cur_fg_frame_callback_next_frame = ceil(cur_frame) - 1; + } + } } + // Callback may have assigned new framegroup + // if(this.cur_fg_idx == cur_fg_idx) { + this.cur_fg_think_callback(this); + // } + } + this.nextthink = time + this.think_delta_time; +}; - this.ideal_yaw = vectoyaw(goal_pos - this.origin); - ChangeYaw(); - vector new_velocity; - float dist_to_goal = vlen(goal_pos - this.origin); - if(dist > dist_to_goal) { - dist = dist_to_goal; - } - new_velocity = normalize(goal_pos - this.origin) * dist; - new_velocity_z = this.velocity_z; - this.velocity = new_velocity; - }; +void (float duration) AI_Chase::set_framegroup_duration = { + // If no current framegroup, stop + if(this.cur_fg_start_time < 0) { + print("Warning: Attempted to set framegroup duration without an active framegroup.\n"); + return; + } + + this.cur_fg_duration = duration; + // -------------------------------------------------------------------- + // Adjust think_delta_time so no frames are skipped + // -------------------------------------------------------------------- + float n_anim_frames = fabs(this.cur_fg_end_frame - this.cur_fg_start_frame); + float time_per_frame = duration / n_anim_frames; + // If faster than 10FPS, make thinks be called more often to not miss a frame callback + if(time_per_frame < 0.10) { + this.think_delta_time = time_per_frame * 0.5; + } + else { + this.think_delta_time = 0.1; + } + // -------------------------------------------------------------------- +}; + +void (framegroup fgroup) AI_Chase::set_framegroup = { + // Increment framegroup counter. Reset every 100, we only use this to check when it has changed + this.cur_fg_idx = (this.cur_fg_idx + 1) % 100; + + // FIXME - frame callback isn't guaranteed to be called for every frame because sometimes we skip over frames + // FIXME - I should modify the think delta time to run at 80% of the time between frames, or 0.10 seconds, whichever is greater. + // FIXME - That way, if are playing an animation really fast, it'll call think more often + this.cur_fg_start_frame = fgroup.start_frame; + this.cur_fg_end_frame = fgroup.end_frame; + this.cur_fg_frame_callback = fgroup.frame_callback; + this.cur_fg_think_callback = fgroup.think_callback; + this.set_framegroup_duration(fgroup.duration); + + // Start the animation now + this.cur_fg_frame_callback_is_start_frame = true; + this.cur_fg_frame_callback_is_end_frame = false; + this.frame = this.cur_fg_start_frame; + this.cur_fg_start_time = time; + this.cur_fg_frame_callback_next_frame = this.cur_fg_start_frame; + // print("Done setting framegroup! Start frame: "); + // print(ftos(fgroup.start_frame)); + // print(", End frame: "); + // print(ftos(fgroup.end_frame)); + // print("\n"); }; +void (float dist) AI_Chase::do_walk_to_goal = { + if(dist == 0) { + return; + } + vector goal_pos = path_pos; + if(this.path_target != world) { + goal_pos = this.path_target.origin; + } - -class AI_Zombie : AI_Chase { - entity enemy; // If near, attack - - // Constructor. Called when calling `spawn(AI_Zombie);` - // virtual void() AI_Zombie = {}; - // This should be called explicitly: - virtual void(vector org) init; - - virtual void () fg_die; - virtual void () fg_walk; - virtual void () fg_attack; - virtual void () fg_idle; + this.ideal_yaw = vectoyaw(goal_pos - this.origin); + ChangeYaw(); + vector new_velocity; + float dist_to_goal = vlen(goal_pos - this.origin); + if(dist > dist_to_goal) { + dist = dist_to_goal; + } + new_velocity = normalize(goal_pos - this.origin) * dist; + new_velocity_z = this.velocity_z; + this.velocity = new_velocity; }; @@ -264,6 +180,7 @@ class AI_Zombie : AI_Chase { + framegroup fg_zombie_walk_0; framegroup fg_zombie_walk_1; framegroup fg_zombie_walk_2; @@ -410,18 +327,17 @@ void create_framegroups() { // death2 139 148 // In blender, it's frames 38 to 53, inclusive - // make_empty_framegroup(fg_zombie_walk_0); init_framegroup( 38, 53, 1.0, zombie_walk_frame_callback, zombie_walk_think_callback, fg_zombie_walk_0); - init_framegroup( 53, 66, 10.0, SUB_Null, SUB_Null, fg_zombie_walk_1); - init_framegroup( 67, 82, 10.0, SUB_Null, SUB_Null, fg_zombie_walk_2); - init_framegroup( 116, 129, 10.0, SUB_Null, SUB_Null, fg_zombie_jog_0); - init_framegroup( 78, 85, 10.0, SUB_Null, SUB_Null, fg_zombie_run_0); - init_framegroup( 0, 12, 10.0, SUB_Null, SUB_Null, fg_zombie_idle_0); - init_framegroup( 86, 90, 10.0, SUB_Null, SUB_Null, fg_zombie_attack_0); - init_framegroup( 91, 96, 10.0, SUB_Null, SUB_Null, fg_zombie_attack_1); - init_framegroup( 123, 133, 10.0, SUB_Null, SUB_Null, fg_zombie_die_0); - init_framegroup( 134, 138, 10.0, SUB_Null, SUB_Null, fg_zombie_die_1); - init_framegroup( 139, 148, 10.0, SUB_Null, SUB_Null, fg_zombie_die_2); + init_framegroup( 53, 66, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_walk_1); + init_framegroup( 67, 82, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_walk_2); + init_framegroup( 116, 129, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_jog_0); + init_framegroup( 78, 85, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_run_0); + init_framegroup( 0, 12, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_idle_0); + init_framegroup( 86, 90, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_attack_0); + init_framegroup( 91, 96, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_attack_1); + init_framegroup( 123, 133, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_die_0); + init_framegroup( 134, 138, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_die_1); + init_framegroup( 139, 148, 10.0, (void(entity)) SUB_Null, (void(entity)) SUB_Null, fg_zombie_die_2); }; diff --git a/source/server/ai/navmesh_core.qc b/source/server/ai/navmesh_core.qc index ecae556..1b18113 100644 --- a/source/server/ai/navmesh_core.qc +++ b/source/server/ai/navmesh_core.qc @@ -6,6 +6,10 @@ float sv_navmesh_vert_count; navmesh_poly sv_navmesh_polies[NAV_MAX_POLIES]; float sv_navmesh_poly_count; +// List of all navmesh traversals +navmesh_traversal sv_navmesh_traversals[NAV_MAX_TRAVERSALS]; +float sv_navmesh_traversal_count; + void () Navmesh_Editor_Logic = { if (!cl_navmesh_edit_mode) { @@ -62,16 +66,16 @@ void () Navmesh_Editor_Logic = }; -void sv_load_navmesh_data() -{ +void sv_load_navmesh_data() { string filepath; float file; filepath = strcat(mappath, ".nav"); + print("Loading server-side navmesh from file \"", filepath, "\"\n"); + file = fopen(filepath,FILE_READ); - - if(file == -1) - { + + if(file == -1) { print("Error: file \"",filepath,"\" not found.\n"); return; } @@ -83,8 +87,7 @@ void sv_load_navmesh_data() string line = fgets(file); float vert_count = stof(line); - if(vert_count > sv_navmesh_verts.length) - { + if(vert_count > sv_navmesh_verts.length) { print("Error: navmesh file \"",filepath,"\" has an invalid vert count. (" , line, " > ", ftos(sv_navmesh_verts.length),").\n"); fclose(file); return; @@ -98,8 +101,7 @@ void sv_load_navmesh_data() //Reading all of the vertex positions - for(float i = 0; i < sv_navmesh_vert_count; i++) - { + for(float i = 0; i < sv_navmesh_vert_count; i++) { line = fgets(file); temp = stov(line); sv_navmesh_verts[i].pos.x = temp.x; @@ -111,47 +113,89 @@ void sv_load_navmesh_data() sv_navmesh_poly_count = stof(fgets(file)); //The next lines are each polygon - for(float i = 0; i < sv_navmesh_poly_count; i++) - { - //Getting vertex count + for(float i = 0; i < sv_navmesh_poly_count; i++) { + // Get vertex count sv_navmesh_polies[i].vert_count = stof(fgets(file)); - //Getting vertices + // Get vertices sv_navmesh_polies[i].verts[0] = stof(fgets(file)); sv_navmesh_polies[i].verts[1] = stof(fgets(file)); sv_navmesh_polies[i].verts[2] = stof(fgets(file)); sv_navmesh_polies[i].verts[3] = stof(fgets(file)); - //Getting polygon center + // Get polygon center temp = stov(fgets(file)); sv_navmesh_polies[i].center.x = temp.x; sv_navmesh_polies[i].center.y = temp.y; sv_navmesh_polies[i].center.z = temp.z; - //Getting link count + // Get link count sv_navmesh_polies[i].connected_polies_count = stof(fgets(file)); - //Getting links + // Get links sv_navmesh_polies[i].connected_polies[0] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies[1] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies[2] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies[3] = stof(fgets(file)); - //Getting link edge left vertex + // Get link edge left vertex sv_navmesh_polies[i].connected_polies_left_vert[0] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_left_vert[1] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_left_vert[2] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_left_vert[3] = stof(fgets(file)); - //Getting link edge right vertex + // Get link edge right vertex sv_navmesh_polies[i].connected_polies_right_vert[0] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_right_vert[1] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_right_vert[2] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_right_vert[3] = stof(fgets(file)); - //Get link edge neighbor entrance edge index + // Get link edge neighbor entrance edge index sv_navmesh_polies[i].connected_polies_neighbor_edge_index[0] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_neighbor_edge_index[1] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_neighbor_edge_index[2] = stof(fgets(file)); sv_navmesh_polies[i].connected_polies_neighbor_edge_index[3] = stof(fgets(file)); + + // If navmesh file version 0.0.0, no traversals + // FIXME - Need a better way to do this.. + if(strcmp(nav_file_version, "0.0.0") != 0) { + // Get connected traversals + sv_navmesh_polies[i].connected_traversals_count = stof(fgets(file)); + for(float j = 0; j < sv_navmesh_polies[i].connected_traversals_count; j++) { + sv_navmesh_polies[i].connected_traversals[j] = stof(fgets(file)); + } + } //Get polygon doortarget sv_navmesh_polies[i].doortarget = fgets(file); //Get entrance edge sv_navmesh_polies[i].entrance_edge = stoi(fgets(file)); } + + // ----------------------------------------------------------------------- + // Load Traversals + // ----------------------------------------------------------------------- + // If navmesh file version 0.0.0, no traversals + // FIXME - Need a better way to do this.. + if(strcmp(nav_file_version, "0.0.0") != 0) { + //Next line contains the number of traverals + sv_navmesh_traversal_count = stof(fgets(file)); + + //The next lines are each traversal + for(float i = 0; i < sv_navmesh_traversal_count; i++) { + line = fgets(file); + temp = stov(line); + sv_navmesh_traversals[i].start_pos.x = temp.x; + sv_navmesh_traversals[i].start_pos.y = temp.y; + sv_navmesh_traversals[i].start_pos.z = temp.z; + line = fgets(file); + temp = stov(line); + sv_navmesh_traversals[i].midpoint_pos.x = temp.x; + sv_navmesh_traversals[i].midpoint_pos.y = temp.y; + sv_navmesh_traversals[i].midpoint_pos.z = temp.z; + line = fgets(file); + temp = stov(line); + sv_navmesh_traversals[i].end_pos.x = temp.x; + sv_navmesh_traversals[i].end_pos.y = temp.y; + sv_navmesh_traversals[i].end_pos.z = temp.z; + sv_navmesh_traversals[i].angle = stof(fgets(file)); + sv_navmesh_traversals[i].use_midpoint = stof(fgets(file)); + sv_navmesh_traversals[i].end_poly = stof(fgets(file)); + } + } + // ----------------------------------------------------------------------- fclose(file); } diff --git a/source/server/ai/pathfind_core.qc b/source/server/ai/pathfind_core.qc new file mode 100644 index 0000000..d53cf95 --- /dev/null +++ b/source/server/ai/pathfind_core.qc @@ -0,0 +1,779 @@ +/* + server/ai/pathfind_core.qc + + Navmesh-based pathfinding + + Copyright (C) 2021 NZ:P Team + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to: + + Free Software Foundation, Inc. + 59 Temple Place - Suite 330 + Boston, MA 02111-1307, USA + +*/ + +//=========================================================================================================== +//==================================== Navmesh-based Pathfinding Functions ================================== +//=========================================================================================================== + +//Calculates the heuristic h score value (Our best guess for how far this node is from the goal node) +float sv_pathfind_calc_h_score(float current, float goal) +{ + //FIXME: we could just as easily return vlen()^2 for comparisons... (saves a sqrt operation) + return vlen(sv_navmesh_polies[goal].center - sv_navmesh_polies[current].center); +} + +//Returns the polygon with the lowest f score from polygons the open set +float sv_pathfind_get_lowest_f_score(navmesh_pathfind_result* res) +{ + //TODO: implement a better algorithm for finding the lowest score + float best_score = 100000; + float best_score_index = -1; + + for(float i = 0; i < sv_navmesh_poly_count; i++) + { + if(res->poly_set[i] == PATHFIND_POLY_SET_OPEN) + { + if(res->poly_f_score[i] < best_score) + { + best_score = res->poly_f_score[i]; + best_score_index = i; + } + } + } + return best_score_index; +} + +void sv_pathfind_clear_result_data(navmesh_pathfind_result* res) { + //Technically we only need to iterate over navmesh_poly_count... + for(float i = 0; i < NAV_MAX_POLIES; i++) { + res->poly_set[i] = PATHFIND_POLY_SET_NONE; + res->poly_prev_poly[i] = -1; + res->poly_prev_traversal[i] = -1; + res->poly_g_score[i] = 0; + res->poly_f_score[i] = 0; + + res->path_polygons[i] = -1; + res->path_traversals[i] = -1; + + res->portals_left_pos[i].x = 0; + res->portals_left_pos[i].y = 0; + res->portals_left_pos[i].z = 0; + res->portals_right_pos[i].x = 0; + res->portals_right_pos[i].y = 0; + res->portals_right_pos[i].z = 0; + res->portals_traversal[i] = -1; + + res->point_path_points[i].x = 0; + res->point_path_points[i].y = 0; + res->point_path_points[i].y = 0; + res->point_path_traversals[i] = -1; + } + for(float i = 0;i < NAV_MAX_TRAVERSALS; i++) { + res->traversal_set[i] = PATHFIND_POLY_SET_NONE; + res->traversal_prev_poly[i] = -1; + res->traversal_g_score[i] = 0; + res->traversal_f_score[i] = 0; + } + res->path_length = 0; + res->portals_length = 0; + res->point_path_length = 0; +} + +//Applies a funnel algorithm to the path defined by the array res->result_node_path +// and populates pathfind result path +void sv_pathfind_smooth_path(vector start_point, vector goal_point, navmesh_pathfind_result* res) {}; +// void sv_pathfind_smooth_path(vector start_point, vector goal_point, navmesh_pathfind_result* res) { +// //Evaluate portals and identify left / right portal vertices +// for(float i = 1; i < res->result_node_length; i++) +// { +// float prev_poly = res->result_node_path[i-1]; +// float poly = res->result_node_path[i]; + +// //TODO: find link index of portal + +// //Finding what index prev_poly is linked to poly + +// float link_index = -1; + +// for(float j = 0; j < sv_navmesh_polies[prev_poly].connected_polies_count; j++) +// { +// if(sv_navmesh_polies[prev_poly].connected_polies[j] == poly) +// { +// link_index = j; +// break; +// } +// } + +// //Use that index to get left and right vertices +// res->portals_left_vert[res->portals_length] = sv_navmesh_polies[prev_poly].connected_polies_left_vert[link_index]; +// res->portals_right_vert[res->portals_length++] = sv_navmesh_polies[prev_poly].connected_polies_right_vert[link_index]; +// } +// //the last left edge / right edge is the goal position + +// /*print("Portals: "); +// for(float i = 0; i < res->portals_length; i++) +// { +// print("["); +// print(ftos(i)); +// print("] = ("); +// print(ftos(res->portals_left_vert[i])); +// print(" , "); +// print(ftos(res->portals_right_vert[i])); +// print(") , "); +// }*/ + +// //starting at start_pos (not at start center point) +// //get vectors that point to first left edge and first right edge + +// vector funnel_apex = start_point; +// float funnel_left_index = 0; +// //Index of the funnel's left vertex in the portal list +// vector funnel_left = sv_navmesh_verts[res->portals_left_vert[0]].pos; +// //Index of the funnel's right vertex in the portal list +// float funnel_right_index = 0; +// vector funnel_right = sv_navmesh_verts[res->portals_right_vert[0]].pos; + +// vector next_funnel_left = '0 0 0'; +// vector next_funnel_right = '0 0 0'; +// vector corner; +// int portal_idx; + + +// float inside_right_edge; +// float inside_left_edge; + +// while(1) +// { +// //print("=============Funnel iteration.==========\n"); +// //Check if we have reached the end of the portals, consider the goal position as the last portal +// //If we are not at the last index of the left portal + +// //Keeping track of whether or not advancing the left edge or right edge narrows the funnel +// float advanced_left = FALSE; +// float advanced_right = FALSE; + + + +// //Consider the end goal point as the last portal +// //================ Checking left funnel edge ================= +// if(funnel_left_index < res->portals_length) +// { +// //print("Trying to advance left edge.\n"); + +// if(funnel_left_index < res->portals_length - 1) +// { +// next_funnel_left = sv_navmesh_verts[res->portals_left_vert[funnel_left_index+1]].pos; +// // print("Next left edge is vert: ",ftos(res->portals_left_vert[funnel_left_index+1],".\n"); +// } +// else +// { +// // print("Trying next left edge as goal point.\n"); +// //If funnel_left is pointing to the last portal in the array, consider the goal_point the last portal +// next_funnel_left = goal_point; +// } + +// inside_right_edge = pathfind_point_is_to_left(funnel_apex,funnel_right,next_funnel_left); +// //If the next left edge crosses the current right edge +// if(inside_right_edge < 0) +// { +// //print("next left edge crosses current right edge.\n"); +// //Add funnel right point to path +// // res->result_path[res->result_length++] = funnel_right; +// corner = funnel_right; +// // ------------------------------------------------------------ +// // Instead of adding the funnel's corner to the path, +// // move away from the corner by some small amount +// // ------------------------------------------------------------ +// // Find the first portal along the path with this corner +// portal_idx = funnel_right_index; +// for(int i = funnel_right_index; i >= 0; i--) { +// if(res->portals_right_vert[i] == res->portals_right_vert[funnel_right_index]) { +// portal_idx = i; +// continue; +// } +// break; +// } +// // For every portal with this corner, add a point to the path +// for(int i = portal_idx; i <= funnel_right_index; i++) { +// // Find the vertex opposite this portal edge +// vector opposite_corner = sv_navmesh_verts[res->portals_left_vert[i]].pos; +// // Move 20 qu or 20% of the edge length, whichever is smaller: +// float edge_len = vlen(opposite_corner - corner); +// vector edge_dir = normalize(opposite_corner - corner); +// vector delta_corner = edge_dir * min(edge_len * 0.2, 20); +// res->result_path[res->result_length++] = corner + delta_corner; +// } +// // ------------------------------------------------------------ +// // res->result_path[res->result_length++] = corner; + + +// // TODO - Move 10% of the way the funnel's right portal vert to the left + +// //Check if we are at the end of our portal list +// if(funnel_right_index >= res->portals_length - 1) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } + + +// //Restart algorithm with the portal after the right point +// funnel_apex = funnel_right; + +// funnel_left_index = funnel_right_index + 1; +// funnel_left = sv_navmesh_verts[res->portals_left_vert[funnel_left_index]].pos; +// funnel_right_index = funnel_right_index + 1; +// funnel_right = sv_navmesh_verts[res->portals_right_vert[funnel_right_index]].pos; +// continue; +// } +// //If the next left edge is in the funnel +// inside_left_edge = pathfind_point_is_to_left(funnel_left,funnel_apex,next_funnel_left); +// if(inside_left_edge >= 0 && inside_right_edge >= 0) +// { +// //print("next left edge is in the funnel.\n"); +// //Advance the left edge +// funnel_left = next_funnel_left; +// funnel_left_index++; + +// advanced_left = TRUE; + +// //Check if the portal we just added was the goal point (will be after the list) +// if(funnel_left_index >= res->portals_length) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } +// } +// } +// //================ Checking right funnel edge ================= +// if(funnel_right_index < res->portals_length) +// { +// //print("Trying to advance right edge:.\n"); + +// if(funnel_right_index < res->portals_length - 1) +// { +// next_funnel_right = sv_navmesh_verts[res->portals_right_vert[funnel_right_index+1]].pos; +// //print("Next right edge is vert: ",ftos(res->portals_right_vert[funnel_right_index+1]),".\n"); +// } +// else +// { +// //print("Trying next right edge as goal point.\n"); +// //If funnel_right is pointing to the last portal in the array, consider the goal_point the last portal +// next_funnel_right = goal_point; +// } + +// //If the next right edge crosses the current left edge +// inside_left_edge = pathfind_point_is_to_left(funnel_left,funnel_apex,next_funnel_right); +// if(inside_left_edge < 0) +// { +// //print("next right edge crosses current left edge.\n"); +// //Add funnel left point to path +// // res->result_path[res->result_length++] = funnel_left; +// corner = funnel_left; +// // ------------------------------------------------------------ +// // Instead of adding the funnel's corner to the path, +// // move away from the corner by some small amount +// // ------------------------------------------------------------ +// // Find the first portal along the path with this corner +// portal_idx = funnel_left_index; +// for(int i = funnel_left_index; i >= 0; i--) { +// if(res->portals_left_vert[i] == res->portals_left_vert[funnel_left_index]) { +// portal_idx = i; +// continue; +// } +// break; +// } +// // For every portal with this corner, add a point to the path +// for(int i = portal_idx; i <= funnel_left_index; i++) { +// // Find the vertex opposite this portal edge +// vector opposite_corner = sv_navmesh_verts[res->portals_right_vert[i]].pos; +// // Move 20 qu or 20% of the edge length, whichever is smaller: +// float edge_len = vlen(opposite_corner - corner); +// vector edge_dir = normalize(opposite_corner - corner); +// vector delta_corner = edge_dir * min(edge_len * 0.2, 20); +// res->result_path[res->result_length++] = corner + delta_corner; +// } +// // ------------------------------------------------------------ +// // res->result_path[res->result_length++] = corner; + +// //Check if we are at the end of our portal list +// if(funnel_left_index >= res->portals_length - 1) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } + + +// //Restart algorithm with the portal after the right point +// funnel_apex = funnel_left; + +// funnel_right_index = funnel_left_index + 1; +// funnel_right = sv_navmesh_verts[res->portals_right_vert[funnel_right_index]].pos; +// funnel_left_index = funnel_left_index + 1; +// funnel_left = sv_navmesh_verts[res->portals_left_vert[funnel_left_index]].pos; +// continue; +// } +// //If the next right edge is in the funnel +// inside_right_edge = pathfind_point_is_to_left(funnel_apex,funnel_right,next_funnel_right); +// if(inside_left_edge >= 0 && inside_right_edge >= 0) +// { +// //print("next right edge is in the funnel.\n"); +// //Advance the left edge +// funnel_right = next_funnel_right; +// funnel_right_index++; + +// advanced_right = TRUE; + +// //Check if the portal we just added was the goal point (will be after the list) +// if(funnel_right_index >= res->portals_length) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } +// } +// } +// if(advanced_left == FALSE && advanced_right == FALSE) +// { +// //print("Hit funnel freeze condition.\n"); + +// float left_vert = -1; +// float left_type = -1; +// float left_portal_index = -1;//index of the vertex in the list of portals + +// float right_vert = -1; +// float right_type = -1; +// float right_portal_index = -1;//index of the vertex in the list of portals + +// float crossed = 1;//Indicates the vert crossed the funnel +// float contained = 2;//Indicates a vert is in the funnel + +// float last_vert = -1;//used to skip calculating the same vertex more than once + + +// //Find the closest vertex in the portal left with the lowest index that crosses the funnel or is in the funnel +// for(float i = funnel_left_index + 1; i < res->portals_length + 1; i++) +// { +// if(i < res->portals_length - 1) +// { +// //Don't calculate the same vertex again +// if(last_vert == res->portals_left_vert[i]) +// continue; +// last_vert = res->portals_left_vert[i]; +// next_funnel_left = sv_navmesh_verts[last_vert].pos; +// } +// else//consider goal pos as last portal edge +// { +// next_funnel_left = goal_point; +// } + +// inside_right_edge = pathfind_point_is_to_left(funnel_apex,funnel_right,next_funnel_left); + +// //If the left vertex crosses the funnel (left vertex is outside the funnel's right edge) +// if(inside_right_edge < 0) +// { +// left_vert = res->portals_left_vert[i]; +// left_type = crossed; +// break; +// } + +// inside_left_edge = pathfind_point_is_to_left(funnel_left,funnel_apex,next_funnel_left); + +// //If the left vertex is within the funnel +// if(inside_left_edge >= 0 && inside_right_edge >= 0) +// { +// left_vert = res->portals_left_vert[i]; +// left_type = contained; +// left_portal_index = i; +// break; +// } +// } + +// last_vert = -1; + +// //Find the closest vertex in the portal right with the lowest index that crosses the funnel or is in the funnel +// for(float i = funnel_right_index + 1; i < res->portals_length; i++) +// { +// if(i < res->portals_length - 1) +// { +// //Don't calculate the same vertex again +// if(last_vert == res->portals_right_vert[i]) +// continue; +// last_vert = res->portals_right_vert[i]; +// next_funnel_right = sv_navmesh_verts[last_vert].pos; +// } +// else//consider goal pos as last portal edge +// { +// next_funnel_right = goal_point; +// } + +// inside_left_edge = pathfind_point_is_to_left(funnel_left,funnel_apex,next_funnel_right); + +// //If the right vertex crosses the funnel (right vertex is outside the funnel's left edge) +// if(inside_left_edge < 0) +// { +// right_vert = res->portals_right_vert[i]; +// right_type = crossed; +// break; +// } + +// inside_right_edge = pathfind_point_is_to_left(funnel_apex,funnel_right,next_funnel_right); + +// //If the right vertex is within the funnel +// if(inside_left_edge >= 0 && inside_right_edge >= 0) +// { +// right_vert = res->portals_right_vert[i]; +// right_type = contained; +// right_portal_index = i; +// break; +// } +// } + +// //If no vertices were found, goal is reachable from here +// if(left_vert == -1 && right_vert == -1) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } + + +// float use_left; + +// //If both verts were found +// //Find which vertex has a lower index +// if(left_vert != -1 && right_vert != -1) +// { +// if(left_portal_index < right_portal_index) +// { +// use_left = TRUE; +// } +// else +// { +// use_left = FALSE; +// } +// } + +// //if left vert was found +// else if(left_vert != -1) +// { +// use_left = TRUE; +// } +// //else right vert was found +// else +// { +// use_left = FALSE; +// } + +// if(use_left) +// { +// if(left_type == crossed) +// { +// //Place corner at current right funnel edge +// res->result_path[res->result_length++] = funnel_right; + +// //Restart algorithm with the portal after the right point +// funnel_apex = funnel_right; + +// //Check if portal after this portal is goal: +// funnel_right_index++; +// if(funnel_right_index >= res->portals_length) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } + +// funnel_right = sv_navmesh_verts[res->portals_right_vert[funnel_right_index]].pos; +// funnel_left_index = funnel_right_index; +// funnel_left = sv_navmesh_verts[res->portals_left_vert[funnel_left_index]].pos; +// continue; +// } +// else//in funnel +// { +// //If the next funnel right is the goal, we are done +// //Check if portal after this portal is goal: +// if(right_portal_index >= res->portals_length) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } +// //Update current left funnel edge to use this vertex +// funnel_left = next_funnel_left; +// funnel_left_index = left_portal_index; + +// continue; +// } +// } +// else//use right +// { +// if(right_type == crossed) +// { +// //Place corner at current left funnel edge +// res->result_path[res->result_length++] = funnel_left; + +// //Restart algorithm with the portal after the left point +// funnel_apex = funnel_left; + +// //Check if portal after this portal is goal: +// funnel_left_index++; +// if(funnel_left_index >= res->portals_length) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } + +// funnel_left = sv_navmesh_verts[res->portals_left_vert[funnel_left_index]].pos; +// funnel_right_index = funnel_left_index; +// funnel_right = sv_navmesh_verts[res->portals_right_vert[funnel_right_index]].pos; +// continue; +// } +// else//in funnel +// { +// //If the next funnel right is the goal, we are done +// //Check if portal after this portal is goal: +// if(right_portal_index >= res->portals_length) +// { +// res->result_path[res->result_length++] = goal_point; +// return; +// } +// //Update current right funnel edge to use this vertex +// funnel_right = next_funnel_right; +// funnel_right_index = right_portal_index; + +// continue; +// } +// } +// } +// //print("==========================\n"); +// } +// } + +//Evaluates the path that is found in the pathfinding algorithm and populates an array with the nodes in the path from start to goal +void sv_pathfind_trace_path(float start, float goal, navmesh_pathfind_result* res) {} +// void sv_pathfind_trace_path(float start, float goal, navmesh_pathfind_result* res) +// { +// float current = start; +// //Count the length of the path (how many polygons the path traverses) +// current = goal; +// res->result_node_length = 1; +// do +// { +// //print("Poly "); +// //print(ftos(current)); +// //print(" came from "); +// //print(ftos(res->poly_prev[current])); +// //print(".\n"); +// current = res->poly_prev[current]; +// res->result_node_length++; +// } while(current != start); + + +// //Starting at goal waypoint, add the traversed waypoints to the result path in reverse order +// current = goal; +// for(float i = 0; i < res->result_node_length; i++) +// { +// res->result_node_path[res->result_node_length - 1 - i] = current; +// current = res->poly_prev[current]; +// } + + +// // print("Pathfind success, path length: "); +// // print(ftos(res->result_node_length)); +// // print(", Path: ["); +// // for(float i = 0; i < res->result_node_length; i++) +// // { +// // if(i > 0) +// // print(" , "); +// // print(ftos(res->result_node_path[i])); +// // } +// // print(" ].\n"); +// } + +//Accepts start polygon and goal polygon +//Returns 1 on success. +//Returns 0 on fail. +float sv_navmesh_pathfind_start(float start, float goal, vector start_pos, vector end_pos, navmesh_pathfind_result* res) {} +// float sv_navmesh_pathfind_start(float start, float goal, vector start_pos, vector end_pos, navmesh_pathfind_result* res) { +// if(start == -1) { +// //print("Error: pathfind start node invalid.\n"); +// return 0; +// } +// if(goal == -1) { +// //print("Error: pathfind goal node invalid.\n"); +// return 0; +// } +// if(start == goal) { +// //Calculating node path +// res->result_node_path[0] = start; +// res->result_node_length = 1; +// // print("Pathind success: trivial case (start = goal).\n"); + +// //Calculating vector based path (go directly to goal) +// res->result_path[0].x = end_pos.x; +// res->result_path[0].y = end_pos.y; +// res->result_path[0].z = end_pos.z; +// res->result_length = 1; +// return 1; +// } + +// //Clearing previous data +// sv_pathfind_clear_result_data(res); + +// //Adding start polygon to the open set +// res->poly_set[start] = PATHFIND_POLY_SET_OPEN; +// res->poly_g_score[start] = 0; +// res->poly_f_score[start] = 0 + sv_pathfind_calc_h_score(start , goal); + +// //Fields that need to be set: +// //res->poly_set[NAV_MAX_POLIES]; +// //res->prev_poly[NAV_MAX_POLIES]; +// //res->poly_g_score[NAV_MAX_POLIES]; +// //res->poly_f_score[NAV_MAX_POLIES]; + +// // print("Pathfind init. Start: "); +// // print(ftos(start)); +// // print(" , Goal: "); +// // print(ftos(goal)); +// // print(".\n"); + +// float current = start; + +// float pathfind_success = FALSE; + +// while(current != -1) { +// if(current == goal) { +// //print("Current is now goal. Breaking.\n"); +// pathfind_success = TRUE; +// break; +// } +// //Add current node to the closed set +// res->poly_set[current] = PATHFIND_POLY_SET_CLOSED; +// //Add connected nodes to the open set +// for(float i = 0; i < sv_navmesh_polies[current].connected_polies_count; i++) { +// float neighbor = sv_navmesh_polies[current].connected_polies[i]; + +// //print("Checking poly "); +// //print(ftos(current)); +// //print("'s neighbor "); +// //print(ftos(neighbor)); +// //print(".\n"); + +// // ---------------------------------------------------------------- +// // Door check +// // ---------------------------------------------------------------- +// // If this polygon references a door: +// if(sv_navmesh_polies[neighbor].doortarget != "") { +// entity door; +// door = find(world, wayTarget, sv_navmesh_polies[neighbor].doortarget); +// if(door != world) { +// // If the referenced door is closed, don't consider this polygon. +// if(door.state == STATE_BOTTOM) { +// continue; +// } +// } +// } +// // ---------------------------------------------------------------- + +// // ---------------------------------------------------------------- +// // Entrance edge +// // ---------------------------------------------------------------- +// // Check if we can enter the neighbor polygon from the current polygon +// // across the edge connecting the current and neighbor polygons. + +// // If entrance_edge != -1, we can only enter the polygon from the edge at index "entrance_edge" +// if(sv_navmesh_polies[neighbor].entrance_edge != -1) { +// // Check if the edge we're crossing from current to neighbor is the entrance edge +// if(sv_navmesh_polies[neighbor].entrance_edge != sv_navmesh_polies[current].connected_polies_neighbor_edge_index[i]) { +// // If it's not the entrance edge, skip this neighbor. +// // We can't walk from current to neighbor. +// continue; +// } +// } +// // ---------------------------------------------------------------- + + +// if(res->poly_set[neighbor] != PATHFIND_POLY_SET_CLOSED) { +// //print("Neighbor is not in closed list.\n"); +// //Calculate tentative f score +// //Calculate tentative g score (distance from start to current + distance from current to neighbor) +// float tentative_g_score = res->poly_g_score[current] + vlen(sv_navmesh_polies[neighbor].center - sv_navmesh_polies[current].center); + +// if(res->poly_set[neighbor] != PATHFIND_POLY_SET_OPEN || tentative_g_score < res->poly_g_score[neighbor]) { +// //print("Neighbor is not in open list, or a better g score has been found.\n"); +// //Adding neighbor to open set +// res->poly_set[neighbor] = PATHFIND_POLY_SET_OPEN; + +// //Updating scores for neighbor node +// float tentative_f_score = tentative_g_score + sv_pathfind_calc_h_score(neighbor , goal); +// res->poly_g_score[neighbor] = tentative_g_score; +// res->poly_f_score[neighbor] = tentative_f_score; + +// //Linking neighbor node to current node (for tracing path) +// res->poly_prev[neighbor] = current; + +// //print("Assigning Poly "); +// //print(ftos(neighbor)); +// //print(" came from "); +// //print(ftos(current)); +// //print(".\n"); +// } + +// } +// } + +// current = sv_pathfind_get_lowest_f_score(res); +// } + + +// //Tracing the pathfind results +// if(pathfind_success == TRUE) { +// sv_pathfind_trace_path(start,goal,res); +// sv_pathfind_smooth_path(start_pos,end_pos,res); + +// return 1; +// } +// print("Pathfind fail"); +// return 0; +// } + +float sv_navmesh_pathfind(entity from, entity to) {} +// float sv_navmesh_pathfind(entity from, entity to) { +// // TODO ... How do I get...? +// // Every AI_Chase entity gets an integer +// // TODO - In pathfinding, each entity needs a place to store its results... +// // TODO - I should really make a MAX_AI number +// // TODO - I can have an array of pre-allocated pathfind results, and dynamically use them? +// // TODO - Or maybe I have one reserved per AI, and one supplemental one reesrved per AI... + + + +// // FIXME - Will need to pre-allocate a certain number of AI_Chase ents and reuse them... or maybe +// // FIXME I need to pre-allocate a certain number of zombies, and dogs? +// // FIXME I wanted to subclass zombies to crawlers to override their behaviors... + + +// // We can perform pathfinding logic without losing our current path. + +// // TODO - In pathfinding, I need a reference to the calling entity to know which traversal types this entity can use +// AI_Chase chase_ent = (AI_Chase) from; +// navmesh_pathfind_result* res = &(sv_zombie_pathfind_result[chase_ent.pathfind_result_idx]); + +// float start = sv_navmesh_get_containing_poly(from.origin); +// float goal = sv_navmesh_get_containing_poly(to.origin); + +// return sv_navmesh_pathfind_start(start,goal,from.origin,to.origin,res); +// } + +//=========================================================================================================== diff --git a/source/server/ai/zombie_core.qc b/source/server/ai/zombie_core.qc index 48a242e..eb4b9f6 100644 --- a/source/server/ai/zombie_core.qc +++ b/source/server/ai/zombie_core.qc @@ -1593,7 +1593,7 @@ float() getMaxZombies = if (dogWave == true) { return 2; } else { - return MAX_ZOMB; + return MAX_ZOMBIES; } } diff --git a/source/server/defs/custom.qc b/source/server/defs/custom.qc index 184ce41..2eb3ee4 100644 --- a/source/server/defs/custom.qc +++ b/source/server/defs/custom.qc @@ -230,9 +230,9 @@ void() SUB_Null2 = {}; #define VEC_VIEW_OFS '0 0 32' #ifndef HANDHELD -#define MAX_ZOMB 24 +#define MAX_ZOMBIES 24 #else -#define MAX_ZOMB 12 +#define MAX_ZOMBIES 12 #endif // HANDHELD vector trace_plane_normal; @@ -477,14 +477,99 @@ string world_fog; #ifdef PC void() Navmesh_Editor_Logic; void() sv_load_navmesh_data; +//One struct for temp pathfinding calculations per zombie +//navmesh_pathfind_result sv_zombie_pathfind_result[MAX_ZOMBIES]; +navmesh_pathfind_result *sv_zombie_pathfind_result; // Used as primary. The one that's followed +navmesh_pathfind_result *sv_zombie_pathfind_result_aux; // Used as secondary, so we can pathfind without losing current path #endif // PC -// //One struct for temp pathfinding calculations per zombie -// //pathfind_result sv_zombie_pathfind_result[MAX_ZOMBIES]; -// pathfind_result *sv_zombie_pathfind_result; +//================================================================= +//========================== AI defs ============================== +var struct framegroup { + float start_frame; + float end_frame; + float duration; + // void(entity) start_callback; // Called at the first frame of the range + void(entity) frame_callback; // Called at every frame of the range (after start / finish callbacks) + // void(entity) end_callback; // Called at the last frame of the range + // FIXME - Increasing the size of this framegroup by one more function pointer causes chaos... + // I assume I run out of space to pass framegroups along as arguments at any point... + // It starts to write into weird memory locations, like overriding self / world... things just break down. + // TODO - How can I refactor this to work? + void(entity) think_callback; // Called at each think invocation + // -- Maybe I can get rid of start / end callbacks? idk + // I should think about how I actually plan on using these... + + // TODO - Get rid of start / end callback, add fields to the AI_Chase class that denotes + // TODO - These callbacks will be executed at the same time as frame anyways. +}; + +class AI_Chase : entity { + // framegroup_registry *th_reg; + entity path_target; // If specified, path towards entity + vector path_pos; // Otherwise, path towards location specified + float think_delta_time; // Delta time (seconds) between each `this.think();` + + // ------------------------------ + // Current Framegroup value vars + // ------------------------------ + float cur_fg_start_time; + float cur_fg_start_frame; + float cur_fg_end_frame; + float cur_fg_duration; + // void() endanimfunc = SUB_Null; + // virtual void(entity) cur_fg_start_callback = SUB_Null; + virtual void(entity) cur_fg_frame_callback = (void(entity)) SUB_Null; + // virtual void(entity) cur_fg_end_callback = SUB_Null; + virtual void(entity) cur_fg_think_callback = (void(entity)) SUB_Null; + // ------------------------------ + // Framegroup state vars + // ------------------------------ + float cur_fg_idx; // Counter for checking when the framegroup has been changed + float cur_fg_frame_callback_next_frame; + float cur_fg_frame_callback_is_start_frame; // Is set to true when performing cur_fg_frame_callback for the first framegroup frame + float cur_fg_frame_callback_is_end_frame; // Is set to true when performing cur_fg_frame_callback for the final framegroup frame + // ------------------------------ + + float pathfind_result_idx; // TODO - Need to increment this on instantiation + + + // Constructor. Called when calling `spawn(AI_Chase);` + virtual void() AI_Chase; + + + virtual void() think; + virtual void (float duration) set_framegroup_duration; + virtual void (framegroup fgroup) set_framegroup; + + + virtual void () fg_die = {}; + virtual void () fg_walk = {}; + virtual void () fg_attack = {}; + virtual void () fg_idle = {}; + + virtual void (float dist) do_walk_to_goal; +}; + + +class AI_Zombie : AI_Chase { + entity enemy; // If near, attack + + // Constructor. Called when calling `spawn(AI_Zombie);` + // virtual void() AI_Zombie = {}; + // This should be called explicitly: + virtual void(vector org) init; + + virtual void () fg_die; + virtual void () fg_walk; + virtual void () fg_attack; + virtual void () fg_idle; +}; + //================================================================= + //Misc patch definitions .string teddyremovetarget; diff --git a/source/server/main.qc b/source/server/main.qc index e684815..481f8d0 100644 --- a/source/server/main.qc +++ b/source/server/main.qc @@ -340,12 +340,17 @@ void() worldspawn = mappath = strzone(mappath); LoadWaypointData(); - // sv_load_navmesh_data(); +// TODO - Add PSP equivalent for loading navmesh +#ifdef PC + sv_load_navmesh_data(); + //Allocating memory for the zombie's pathfinding data + sv_zombie_pathfind_result = memalloc(sizeof(navmesh_pathfind_result) * MAX_ZOMBIES); + sv_zombie_pathfind_result_aux = memalloc(sizeof(navmesh_pathfind_result) * MAX_ZOMBIES); + // I should technically deallocate the memory somewhere, but I've nowhere to put this, and it SHOULD be cleared automatically upon vm reset + // memfree(sv_zombie_pathfind_result); + // memfree(sv_zombie_pathfind_result_aux); +#endif // PC - // //Allocating memory for the zombie's pathfinding data - // sv_zombie_pathfind_result = memalloc(sizeof(pathfind_result) * MAX_ZOMBIES); - //I should technically deallocate the memory, but I've nowhere to put this, and it SHOULD be cleared automatically upon vm reset - //memfree(sv_zombie_pathfind_result); //set game elements diff --git a/source/shared/navmesh_defines.qc b/source/shared/navmesh_defines.qc index 9f89c93..e7949bb 100644 --- a/source/shared/navmesh_defines.qc +++ b/source/shared/navmesh_defines.qc @@ -147,7 +147,7 @@ struct navmesh_pathfind_result { float portals_traversal[NAV_MAX_POLIES]; // --------------------------------------------------------- - + // --------------------------------------------------------- // Final path is a list of 3D points and traversals // ---------------------------------------------------------