Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/weaponConfigReadme.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ This file provides information about the weapon to be used in the experiment. De
|`autoFire` |`bool` | Whether or not the weapon fires when the left mouse is held, or requires release between fire |
|`damagePerSecond` |damage/s | The damage done by the weapon per second, when `firePeriod` > 0 the damage per round is set by `damagePerSecond`*`firePeriod`, when `firePeriod` is 0 and `autoFire` is `True` damage is computed based on time hitting the target. |
|`hitScan` |`bool` | Whether or not the weapon acts as an instantaneous hitscan (true) vs propagated projectile (false) |
|`pelletsPerShot` |`int` | Number of pellets to fire each time the weapon fires (each pellet does the `damagePerSecond` * `firePeriod` damage) |
|`fireSpreadDegrees` |`float` | The constant angular (horizontal and vertical) spread of bullets fired from the weapon in degrees. Clamps to 0 to 120 degrees. |
|`fireSpreadShape` |`String` | The distributional shape to draw the fire spread from (can be `"uniform"` or `"gaussian"`). Invalid fire types will result in no spread. When using a `"gaussian"` distribution shape `fireSpreadDegrees` is the width of the ±3σ interval. |

Expand Down
13 changes: 8 additions & 5 deletions source/FPSciApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -918,8 +918,8 @@ void FPSciApp::onSimulation(RealTime rdt, SimTime sdt, SimTime idt) {
float hitDist = finf();
int hitIdx = -1;

shared_ptr<TargetEntity> target = weapon->fire(sess->hittableTargets(), hitIdx, hitDist, info, dontHit, false); // Fire the weapon
if (isNull(target)) // Miss case
Array<shared_ptr<TargetEntity>> hitTargets = weapon->fire(sess->hittableTargets(), hitIdx, hitDist, info, dontHit, false); // Fire the weapon
if (hitTargets.size() == 0) // Miss case
{
// Play scene hit sound
if (!weapon->config()->isContinuous() && notNull(m_sceneHitSound)) {
Expand Down Expand Up @@ -954,6 +954,7 @@ void FPSciApp::onSimulation(RealTime rdt, SimTime sdt, SimTime idt) {
m_explosionRemainingTimes[i] -= sdt;
if (m_explosionRemainingTimes[i] <= 0) {
scene()->remove(explosion);
weapon->removeFromDontHit(explosion); // Remove this explosion from the weapon's dont hit list
m_explosions.fastRemove(i);
m_explosionRemainingTimes.fastRemove(i);
i--;
Expand Down Expand Up @@ -1237,7 +1238,7 @@ void FPSciApp::setScopeView(bool scoped) {
player->turnScale = currentTurnScale(); // Scale sensitivity based on the field of view change here
}

void FPSciApp::hitTarget(shared_ptr<TargetEntity> target) {
bool FPSciApp::hitTarget(shared_ptr<TargetEntity> target) {
// Damage the target
float damage = m_currentWeaponDamage;
target->doDamage(damage);
Expand Down Expand Up @@ -1285,6 +1286,7 @@ void FPSciApp::hitTarget(shared_ptr<TargetEntity> target) {
m_explosionIdx %= m_maxExplosions;
scene()->insert(newExplosion);
m_explosions.push(newExplosion);
weapon->addToDontHit(newExplosion); // Don't hit explosions (update the weapon to know about this explosion)
m_explosionRemainingTimes.push(experimentConfig.getTargetConfigById(target->id())->destroyDecalDuration); // Schedule end of explosion
target->playDestroySound();

Expand All @@ -1311,6 +1313,8 @@ void FPSciApp::hitTarget(shared_ptr<TargetEntity> target) {
// Update the target color based on it's health
updateTargetColor(target);
}

return destroyedTarget; // Return whether we destroyed this target or not
}

void FPSciApp::updateTargetColor(const shared_ptr<TargetEntity>& target) {
Expand Down Expand Up @@ -1391,12 +1395,11 @@ void FPSciApp::onUserInput(UserInput* ui) {
for (GKey dummyShoot : keyMap.map["dummyShoot"]) {
if (ui->keyPressed(dummyShoot) && (sess->currentState == PresentationState::referenceTarget) && !m_userSettingsWindow->visible()) {
Array<shared_ptr<Entity>> dontHit;
dontHit.append(m_explosions);
dontHit.append(sess->unhittableTargets());
Model::HitInfo info;
float hitDist = finf();
int hitIdx = -1;
shared_ptr<TargetEntity> target = weapon->fire(sess->hittableTargets(), hitIdx, hitDist, info, dontHit, true); // Fire the weapon
Array<shared_ptr<TargetEntity>> hitTargets = weapon->fire(sess->hittableTargets(), hitIdx, hitDist, info, dontHit, true); // Fire the weapon
if (trialConfig->audio.refTargetPlayFireSound && !trialConfig->weapon.loopAudio()) { // Only play shot sounds for non-looped weapon audio (continuous/automatic fire not allowed)
weapon->playSound(true, false); // Play audio here for reference target
}
Expand Down
2 changes: 1 addition & 1 deletion source/FPSciApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class FPSciApp : public GApp {
/** Set the scoped view (and also adjust the turn scale), use setScopeView(!weapon->scoped()) to toggle scope */
void setScopeView(bool scoped = true);

void hitTarget(shared_ptr<TargetEntity> target);
bool hitTarget(shared_ptr<TargetEntity> target);
void missEvent();

public:
Expand Down
175 changes: 96 additions & 79 deletions source/Weapon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ WeaponConfig::WeaponConfig(const Any& any) {
reader.getIfPresent("hitDecalTimeoutS", hitDecalTimeoutS);
reader.getIfPresent("hitDecalColorMult", hitDecalColorMult);

reader.getIfPresent("pelletsPerShot", pelletsPerShot);
reader.getIfPresent("fireSpreadDegrees", fireSpreadDegrees);
reader.getIfPresent("fireSpreadShape", fireSpreadShape);

Expand Down Expand Up @@ -341,106 +342,122 @@ void Weapon::clearDecals(bool clearHitDecal) {
}
}

shared_ptr<TargetEntity> Weapon::fire(
Array<shared_ptr<TargetEntity>> Weapon::fire(
const Array<shared_ptr<TargetEntity>>& targets,
int& targetIdx,
float& hitDist,
Model::HitInfo& hitInfo,
Array<shared_ptr<Entity>>& dontHit,
Array<shared_ptr<Entity>> dontHit,
bool dummyShot)
{
Ray ray = m_camera->frame().lookRay(); // Use the camera lookray for hit detection
float spread = m_config->fireSpreadDegrees * 2.f * pif() / 360.f;
int pellets = m_config->pelletsPerShot;

// ignore bullet spread on dummy targets
if (dummyShot) { spread = 0.f; }
// Ignore bullet spread and multiple pellets on dummy targets
if (dummyShot) {
spread = 0.f;
pellets = 1;
}
else { m_ammo -= 1; }

// Apply random rotation (for fire spread)
Matrix3 rotMat = Matrix3::fromEulerAnglesXYZ(0.f,0.f,0.f);
if (m_config->fireSpreadShape == "uniform") {
rotMat = Matrix3::fromEulerAnglesXYZ(m_rand.uniform(-spread / 2, spread / 2), m_rand.uniform(-spread / 2, spread / 2), 0);
}
else if (m_config->fireSpreadShape == "gaussian") {
rotMat = Matrix3::fromEulerAnglesXYZ(m_rand.gaussian(0, spread / 3), m_rand.gaussian(0, spread / 3), 0);
}
Vector3 dir = Vector3(0.f, 0.f, -1.f) * rotMat;
ray.set(ray.origin(), m_camera->frame().rotation * dir);

// Check for closest hit (in scene, otherwise this ray hits the skybox)
float closest = finf();
Array<shared_ptr<Entity>> dontHitItems = dontHit;
dontHitItems.append(m_currentMissDecals);
dontHitItems.append(targets);
dontHitItems.append(m_projectiles);
m_scene->intersect(ray, closest, false, dontHitItems, hitInfo);
if (closest < finf()) { hitDist = closest; }

// Create the bullet (if we need to draw it or are using non-hitscan behavior)
if (m_config->renderBullets || !m_config->hitScan) {
// Create the bullet start frame from the weapon frame plus muzzle offset
CFrame bulletStartFrame = m_camera->frame();

// Apply bullet offset w/ camera rotation here
bulletStartFrame.translation += ray.direction() * m_config->bulletOffset;

// Angle the bullet start frame towards the aim point
Point3 aimPoint = m_camera->frame().translation + ray.direction() * 1000.0f;
// If we hit the scene w/ this ray, angle it towards that collision point
if (closest < finf()) {
aimPoint = hitInfo.point;
Array<shared_ptr<TargetEntity>> hitTargets;
Array<shared_ptr<TargetEntity>> remainingTargets = targets;

// Iterate for each pellet in the shot
for (int i = 0; i < pellets; i++) {
// Apply random rotation (for fire spread)
Matrix3 rotMat = Matrix3::fromEulerAnglesXYZ(0.f, 0.f, 0.f);
if (m_config->fireSpreadShape == "uniform") {
rotMat = Matrix3::fromEulerAnglesXYZ(m_rand.uniform(-spread / 2, spread / 2), m_rand.uniform(-spread / 2, spread / 2), 0);
}
bulletStartFrame.lookAt(aimPoint);

// Non-laser weapon, draw a projectile
if (!m_config->isContinuous()) {
const shared_ptr<VisibleEntity>& bullet = VisibleEntity::create(format("bullet%03d", ++m_lastBulletId), m_scene.get(), m_bulletModel, bulletStartFrame);
bullet->setShouldBeSaved(false);
bullet->setCanCauseCollisions(false);
bullet->setCastsShadows(false);
bullet->setVisible(m_config->renderBullets);

const shared_ptr<Projectile> projectile = Projectile::create(bullet, m_config->bulletSpeed, !m_config->hitScan, m_config->bulletGravity, fmin((closest + 1.0f) / m_config->bulletSpeed, 10.0f));
m_projectiles.push(projectile);
m_scene->insert(projectile);
else if (m_config->fireSpreadShape == "gaussian") {
rotMat = Matrix3::fromEulerAnglesXYZ(m_rand.gaussian(0, spread / 3), m_rand.gaussian(0, spread / 3), 0);
}
// Laser weapon (very hacky for now...)
else {
// Need to do something better than this, draws for 2 frames and also doesn't work w/ the start frame aligned w/ the camera (see the backfaces)
//shared_ptr<CylinderShape> beam = std::make_shared<CylinderShape>(CylinderShape(Cylinder(bulletStartFrame.translation, aimPoint, 0.02f)));
//debugDraw(beam, FLT_EPSILON, Color4(0.2f, 0.8f, 0.0f, 1.0f), Color4::clear());
Vector3 dir = Vector3(0.f, 0.f, -1.f) * rotMat;
ray.set(ray.origin(), m_camera->frame().rotation * dir);

// Check for closest hit (in scene, otherwise this ray hits the skybox)
float closest = finf();
Array<shared_ptr<Entity>> dontHitItems = dontHit;
dontHitItems.append(m_hitDecal);
dontHitItems.append(m_currentMissDecals);
dontHitItems.append(m_projectiles);
dontHitItems.append(targets);
dontHitItems.append(m_dontHit); // Add in stored don't hit items (explosions generated by app)
m_scene->intersect(ray, closest, false, dontHitItems, hitInfo); // Prime the closest value w/ the scene intersection point (miss from scene hit)
if (closest < finf()) { hitDist = closest; }

// Create the bullet (if we need to draw it or are using non-hitscan behavior)
if (m_config->renderBullets || !m_config->hitScan) {
// Create the bullet start frame from the weapon frame plus muzzle offset
CFrame bulletStartFrame = m_camera->frame();

// Apply bullet offset w/ camera rotation here
bulletStartFrame.translation += ray.direction() * m_config->bulletOffset;

// Angle the bullet start frame towards the aim point
Point3 aimPoint = m_camera->frame().translation + ray.direction() * 1000.0f;
// If we hit the scene w/ this ray, angle it towards that collision point
if (closest < finf()) {
aimPoint = hitInfo.point;
}
bulletStartFrame.lookAt(aimPoint);

// Non-laser weapon, draw a projectile
if (!m_config->isContinuous()) {
const shared_ptr<VisibleEntity>& bullet = VisibleEntity::create(format("bullet%03d", ++m_lastBulletId), m_scene.get(), m_bulletModel, bulletStartFrame);
bullet->setShouldBeSaved(false);
bullet->setCanCauseCollisions(false);
bullet->setCastsShadows(false);
bullet->setVisible(m_config->renderBullets);

const shared_ptr<Projectile> projectile = Projectile::create(bullet, m_config->bulletSpeed, !m_config->hitScan, m_config->bulletGravity, fmin((closest + 1.0f) / m_config->bulletSpeed, 10.0f));
m_projectiles.push(projectile);
m_scene->insert(projectile);
}
// Laser weapon (very hacky for now...)
else {
// Need to do something better than this, draws for 2 frames and also doesn't work w/ the start frame aligned w/ the camera (see the backfaces)
//shared_ptr<CylinderShape> beam = std::make_shared<CylinderShape>(CylinderShape(Cylinder(bulletStartFrame.translation, aimPoint, 0.02f)));
//debugDraw(beam, FLT_EPSILON, Color4(0.2f, 0.8f, 0.0f, 1.0f), Color4::clear());
}
}
}

// Hit scan specific logic here (immediately do hit/miss determination)
shared_ptr<TargetEntity> target = nullptr;
if(m_config->hitScan){
// Check whether we hit any targets
int closestIndex = -1;
for (int t = 0; t < targets.size(); ++t) {
if (targets[t]->intersect(ray, closest, hitInfo)) {
closestIndex = t;
// Hit scan specific logic here (immediately do hit/miss determination)
if (m_config->hitScan) {
// Check whether we hit any targets
int closestIndex = -1;
for (int t = 0; t < remainingTargets.size(); t++) {
if (remainingTargets[t]->intersect(ray, closest, hitInfo)) {
closestIndex = t;
}
}
if (closestIndex >= 0) {
const shared_ptr<TargetEntity> hitTarget = remainingTargets[closestIndex];
// Hit logic
hitTargets.append(hitTarget); // Assign the target pointer here (not null indicates the hit)
targetIdx = closestIndex; // Write back the index of the target

// If we did, we are in hitscan mode, apply the damage and manage the target here
if (m_hitCallback(hitTarget)) {
// We destroyed this target, remove it from our remaining targets
remainingTargets.remove(remainingTargets.findIndex(hitTarget));
}
// Offset position slightly along shot direction to avoid Z-fighting the target
drawDecal(hitInfo.point + 0.01f * -ray.direction(), ray.direction(), true);
}
else {
m_missCallback();
// Offset position slightly along normal to avoid Z-fighting the wall
drawDecal(hitInfo.point + 0.01f * hitInfo.normal, hitInfo.normal);
}
}
if (closestIndex >= 0) {
// Hit logic
target = targets[closestIndex]; // Assign the target pointer here (not null indicates the hit)
targetIdx = closestIndex; // Write back the index of the target

m_hitCallback(target); // If we did, we are in hitscan mode, apply the damage and manage the target here
// Offset position slightly along shot direction to avoid Z-fighting the target
drawDecal(hitInfo.point + 0.01f * -ray.direction(), ray.direction(), true);
}
else {
m_missCallback();
// Offset position slightly along normal to avoid Z-fighting the wall
drawDecal(hitInfo.point + 0.01f * hitInfo.normal, hitInfo.normal);
}
}

END_PROFILER_EVENT();

return target;
return hitTargets;
}

void Weapon::playSound(bool shotFired, bool shootButtonUp) {
Expand Down
19 changes: 15 additions & 4 deletions source/Weapon.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class WeaponConfig {
float hitDecalTimeoutS = 0.1f; ///< Duration to show the hit decal for (in seconds)
float hitDecalColorMult = 2.0f; ///< "Encoding" field (aka color multiplier) for hit decal

int pelletsPerShot = 1; ///< Number of pellets to generate per shot
float fireSpreadDegrees = 0; ///< The spread of the fire
String fireSpreadShape = "uniform"; ///< The shape of the fire spread distribution
float damageRollOffAim = 0; ///< Damage roll off w/ aim
Expand Down Expand Up @@ -136,6 +137,7 @@ class Weapon : Entity {
shared_ptr<AudioChannel> m_fireAudio; ///< Audio channel for fire sound
WeaponConfig* m_config; ///< Weapon configuration

Array<shared_ptr<Entity>> m_dontHit; ///< Objects that shouldn't be hit (currently app-generated explosions)
Array<shared_ptr<Projectile>> m_projectiles; ///< Arrray of drawn projectiles

int m_lastBulletId = 0; ///< Bullet ID (auto incremented)
Expand All @@ -148,7 +150,7 @@ class Weapon : Entity {
shared_ptr<Scene> m_scene; ///< Scene for weapon
shared_ptr<Camera> m_camera; ///< Camera for weapon

std::function<void(shared_ptr<TargetEntity>)> m_hitCallback; ///< This is set to FPSciApp::hitTarget
std::function<bool(shared_ptr<TargetEntity>)> m_hitCallback; ///< This is set to FPSciApp::hitTarget
std::function<void(void)> m_missCallback; ///< This is set to FPSciApp::missEvent

int m_lastDecalID = 0;
Expand Down Expand Up @@ -196,17 +198,26 @@ class Weapon : Entity {
int shotsTaken() const { return m_config->maxAmmo - m_ammo; }
void reload() { m_ammo = m_config->maxAmmo; }

void addToDontHit(shared_ptr<Entity> e) { m_dontHit.append(e); }
bool removeFromDontHit(shared_ptr<Entity> e) {
if (m_dontHit.contains(e)) {
m_dontHit.remove(m_dontHit.findIndex(e));
return true;
}
return false;
}

/**
targets is the list of targets to try to hit
Ignore anything in the dontHit list
dummyShot controls whether it's a shot at the test target (is this true?)
targetIdx, hitDist and hitInfo are all returned along with the targetEntity that was hit
*/
shared_ptr<TargetEntity> fire(const Array<shared_ptr<TargetEntity>>& targets,
Array<shared_ptr<TargetEntity>> fire(const Array<shared_ptr<TargetEntity>>& targets,
int& targetIdx,
float& hitDist,
Model::HitInfo& hitInfo,
Array<shared_ptr<Entity>>& dontHit,
Array<shared_ptr<Entity>> dontHit,
bool dummyShot);

// Records provided lastFireTime
Expand Down Expand Up @@ -236,7 +247,7 @@ class Weapon : Entity {
// Plays the sound based on the weapon fire mode
void playSound(bool shotFired, bool shootButtonUp);

void setHitCallback(std::function<void(shared_ptr<TargetEntity>)> callback) { m_hitCallback = callback; }
void setHitCallback(std::function<bool(shared_ptr<TargetEntity>)> callback) { m_hitCallback = callback; }
void setMissCallback(std::function<void(void)> callback) { m_missCallback = callback; }

void setConfig(WeaponConfig* config) { m_config = config; }
Expand Down