Skip to content

Commit

Permalink
Redesign fire spread and stop logic.
Browse files Browse the repository at this point in the history
This might fix #844.
  • Loading branch information
Viech committed Nov 27, 2015
1 parent 6e0f37b commit 69cb0ff
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 45 deletions.
122 changes: 86 additions & 36 deletions src/sgame/components/IgnitableComponent.cpp
Expand Up @@ -26,26 +26,31 @@ along with Unvanquished Source Code. If not, see <http://www.gnu.org/licenses/>

static Log::Logger fireLogger("sgame.fire");

const float IgnitableComponent::SELF_DAMAGE = 10.0f;
const float IgnitableComponent::SPLASH_DAMAGE = 20.0f;
const float IgnitableComponent::SPLASH_DAMAGE_RADIUS = 60.0f;
const int IgnitableComponent::MIN_BURN_TIME = 2500;
const int IgnitableComponent::STOP_CHECK_TIME = 2500;
const float IgnitableComponent::STOP_CHANCE = 0.5f;
const float IgnitableComponent::STOP_RADIUS = 150.0f;
const int IgnitableComponent::SPREAD_CHECK_TIME = 2500;
const float IgnitableComponent::SPREAD_RADIUS = 120.0f;
const float IgnitableComponent::SELF_DAMAGE = 12.5f;
const float IgnitableComponent::SPLASH_DAMAGE = 20.0f;
const float IgnitableComponent::SPLASH_DAMAGE_RADIUS = 60.0f;
const int IgnitableComponent::MIN_BURN_TIME = 4000;
const int IgnitableComponent::BASE_AVERAGE_BURN_TIME = 8000;
const int IgnitableComponent::EXTRA_AVERAGE_BURN_TIME = 5000;
const float IgnitableComponent::EXTRA_BURN_TIME_RADIUS = 150.0f;
const float IgnitableComponent::SPREAD_RADIUS = 120.0f;

static_assert(IgnitableComponent::BASE_AVERAGE_BURN_TIME > IgnitableComponent::MIN_BURN_TIME,
"Average burn time needs to be greater than minimum burn time.");

IgnitableComponent::IgnitableComponent(Entity& entity, bool alwaysOnFire, ThinkingComponent& r_ThinkingComponent)
: IgnitableComponentBase(entity, alwaysOnFire, r_ThinkingComponent)
, onFire(alwaysOnFire)
, igniteTime(alwaysOnFire ? level.time : 0)
, immuneUntil(0)
, fireStarter(nullptr) {
, spreadAt(INT_MAX)
, fireStarter(nullptr)
, randomGenerator(rand()) // TODO: Have one PRNG for all of sgame.
, normalDistribution(0.0f, (float)BASE_AVERAGE_BURN_TIME) {
REGISTER_THINKER(DamageSelf, ThinkingComponent::SCHEDULER_AVERAGE, 100);
REGISTER_THINKER(DamageArea, ThinkingComponent::SCHEDULER_AVERAGE, 100);
REGISTER_THINKER(ConsiderStop, ThinkingComponent::SCHEDULER_AVERAGE, STOP_CHECK_TIME);
REGISTER_THINKER(ConsiderSpread, ThinkingComponent::SCHEDULER_AVERAGE, SPREAD_CHECK_TIME);
REGISTER_THINKER(ConsiderStop, ThinkingComponent::SCHEDULER_AVERAGE, 500);
REGISTER_THINKER(ConsiderSpread, ThinkingComponent::SCHEDULER_AVERAGE, 500);
}

void IgnitableComponent::HandlePrepareNetCode() {
Expand All @@ -68,24 +73,43 @@ void IgnitableComponent::HandleIgnite(gentity_t* fireStarter) {
return;
}

// Refresh ignite time even if already burning.
igniteTime = level.time;

// Start burning on initial ignition.
if (!onFire) {
onFire = true;
this->fireStarter = fireStarter;

fireLogger.Debug("Ignited.");
fireLogger.Notice("Ignited.");
} else {
if (alwaysOnFire && !this->fireStarter) {
// HACK: Igniting an alwaysOnFire entity will initialize the fire starter.
this->fireStarter = fireStarter;

fireLogger.Debug("Firestarter initialized.");
fireLogger.Debug("Firestarter set.");
} else {
fireLogger.Debug("Re-Ignited.");
fireLogger.Debug("Re-ignited.");
}
}

// Refresh ignite time even if already burning.
igniteTime = level.time;

// The spread delay follows a normal distribution: More likely to spread early than late.
int spreadTarget = level.time + (int)std::abs(normalDistribution(randomGenerator));

// Allow re-ignition to update the spread delay to a lower value.
if (spreadTarget < spreadAt) {
fireLogger.DoNoticeCode([&]{
int newDelay = spreadTarget - level.time;
if (spreadAt == INT_MAX) {
fireLogger.Notice("Spread delay set to %.1fs.", newDelay * 0.001f);
} else {
int oldDelay = spreadAt - level.time;
fireLogger.Notice("Spread delay updated from %.1fs to %.1fs.",
oldDelay * 0.001f, newDelay * 0.001f);
}
});
spreadAt = spreadTarget;
}
}

void IgnitableComponent::HandleExtinguish(int immunityTime) {
Expand Down Expand Up @@ -127,56 +151,82 @@ void IgnitableComponent::ConsiderStop(int timeDelta) {

// Don't stop freshly (re-)ignited fires.
if (igniteTime + MIN_BURN_TIME > level.time) {
fireLogger.Debug("(Re-)Ignited %i ms ago, skipping stop check.", level.time - igniteTime);
fireLogger.DoDebugCode([&]{
int elapsed = level.time - igniteTime;
int remaining = MIN_BURN_TIME - elapsed;
fireLogger.Debug("Burning for %.1fs, skipping stop check for another %.1fs.",
(float)elapsed/1000.0f, (float)remaining/1000.0f);
});

return;
}

float burnStopChance = STOP_CHANCE;
float averagePostMinBurnTime = BASE_AVERAGE_BURN_TIME - MIN_BURN_TIME;

// Lower burn stop chance if there are other burning entities nearby.
// Increase average burn time dynamically for burning entities in range.
ForEntities<IgnitableComponent>([&](Entity &other, IgnitableComponent &ignitable){
if (&other == &entity) return;
if (!ignitable.onFire) return;
if (G_Distance(other.oldEnt, entity.oldEnt) > STOP_RADIUS) return;

float frac = G_Distance(entity.oldEnt, other.oldEnt) / STOP_RADIUS;
float mod = frac * 1.0f + (1.0f - frac) * STOP_CHANCE;
// TODO: Use LocationComponent.
float distance = G_Distance(other.oldEnt, entity.oldEnt);

if (distance > EXTRA_BURN_TIME_RADIUS) return;

float distanceFrac = distance / EXTRA_BURN_TIME_RADIUS;
float distanceMod = 1.0f - distanceFrac;

burnStopChance *= mod;
averagePostMinBurnTime += EXTRA_AVERAGE_BURN_TIME * distanceMod;
});

// The burn stop chance follows an exponential distribution.
float lambda = 1.0f / averagePostMinBurnTime;
float burnStopChance = 1.0f - std::exp(-1.0f * lambda * (float)timeDelta);

float averageTotalBurnTime = averagePostMinBurnTime + (float)MIN_BURN_TIME;

// Attempt to stop burning.
if (random() < burnStopChance) {
fireLogger.Debug("Stopped burning (chance was %.0f%%)", burnStopChance * 100.0f);
fireLogger.Notice("Stopped burning after %.1fs, target average lifetime was %.1fs.",
(float)(level.time - igniteTime) / 1000.0f, averageTotalBurnTime / 1000.0f);

entity.Extinguish(0);
return;
} else {
fireLogger.Debug("Didn't stop burning (chance was %.0f%%)", burnStopChance * 100.0f);
fireLogger.Debug("Burning for %.1fs, target average lifetime is %.1fs.",
(float)(level.time - igniteTime) / 1000.0f, averageTotalBurnTime / 1000.0f);
}
}

void IgnitableComponent::ConsiderSpread(int timeDelta) {
if (!onFire) return;
if (level.time < spreadAt) return;

fireLogger.Notice("Trying to spread.");

ForEntities<IgnitableComponent>([&](Entity &other, IgnitableComponent &ignitable){
if (&other == &entity) return;

// Don't re-ignite.
if (ignitable.onFire) return;

// TODO: Use LocationComponent.
float chance = 1.0f - G_Distance(entity.oldEnt, other.oldEnt) / SPREAD_RADIUS;
float distance = G_Distance(other.oldEnt, entity.oldEnt);

if (distance > SPREAD_RADIUS) return;

if (chance <= 0.0f) return; // distance > spread radius
float distanceFrac = distance / SPREAD_RADIUS;
float distanceMod = 1.0f - distanceFrac;
float spreadChance = distanceMod;

if (random() < chance) {
if (random() < spreadChance) {
if (G_LineOfSight(entity.oldEnt, other.oldEnt) && other.Ignite(fireStarter)) {
fireLogger.Debug("(Re-)Ignited a neighbour (chance was %.0f%%)", chance * 100.0f);
} else {
fireLogger.Debug("Tried to ignite a non-ignitable or non-LOS neighbour (chance was %.0f%%)",
chance * 100.0f);
fireLogger.Notice("Ignited a neighbour, chance to do so was %.0f%%.",
spreadChance*100.0f);
}
} else {
fireLogger.Debug("Didn't try to ignite a neighbour (chance was %.0f%%)", chance * 100.0f);
}
});

// Don't spread again until re-ignited.
spreadAt = INT_MAX;
}
34 changes: 25 additions & 9 deletions src/sgame/components/IgnitableComponent.h
Expand Up @@ -28,19 +28,31 @@ along with Unvanquished Source Code. If not, see <http://www.gnu.org/licenses/>
#include "../backend/CBSEBackend.h"
#include "../backend/CBSEComponents.h"

#include <random>

class IgnitableComponent: public IgnitableComponentBase {
public:
const static float SELF_DAMAGE;
const static float SPLASH_DAMAGE;
const static float SPLASH_DAMAGE_RADIUS;

/** The minimum time any fire will burn. */
const static int MIN_BURN_TIME;
const static int STOP_CHECK_TIME;
const static float STOP_CHANCE;
/** Every burning entity in this radius results in a stop chance reduction factor with
* a size that increases linearly from STOP_CHANCE to 1 depending on entity distance. */
const static float STOP_RADIUS;
const static int SPREAD_CHECK_TIME;
/** Fire spread chance decreases linearly from 1.0 to 0.0 over this distance. */

/** The average time an independent fire will burn. Includes the minimum burn period. */
const static int BASE_AVERAGE_BURN_TIME;

/** The maximum additional average burn time per burning neighbour in range. */
const static int EXTRA_AVERAGE_BURN_TIME;

/** The radius used to check for burning neighbours in range for the extra burn time. */
const static float EXTRA_BURN_TIME_RADIUS;

/** During the average lifetime of a fire, this is for each neighbour in range the
* cummulated maximum chance to spread to it. */
const static float LIFETIME_SPREAD_CHANCE;

/** The radius in which fire can spread. */
const static float SPREAD_RADIUS;

// ///////////////////// //
Expand Down Expand Up @@ -85,9 +97,13 @@ class IgnitableComponent: public IgnitableComponentBase {

private:
bool onFire;
int igniteTime;
int immuneUntil; /**< Fire immunity time after being extinguished. */
int igniteTime; /**< Time of (re-)ignition. */
int immuneUntil; /**< Fire immunity time after being extinguished. */
int spreadAt; /**< Will try to spread to neighbours at this time. */
gentity_t* fireStarter; /**< Client who orginally started the fire. */

std::default_random_engine randomGenerator;
std::normal_distribution<float> normalDistribution;
};

#endif // IGNITABLE_COMPONENT_H_

0 comments on commit 69cb0ff

Please sign in to comment.