From 24e6598c85b909aaf59caead37c8b6394f6d3c68 Mon Sep 17 00:00:00 2001 From: Ben Boudaoud Date: Thu, 19 Jan 2023 14:55:27 -0500 Subject: [PATCH 1/3] First-pass multishot weapon implementation --- docs/weaponConfigReadme.md | 1 + source/FPSciApp.cpp | 6 +- source/Weapon.cpp | 153 +++++++++++++++++++------------------ source/Weapon.h | 3 +- 4 files changed, 84 insertions(+), 79 deletions(-) diff --git a/docs/weaponConfigReadme.md b/docs/weaponConfigReadme.md index bf8ece09..1f3d8da7 100644 --- a/docs/weaponConfigReadme.md +++ b/docs/weaponConfigReadme.md @@ -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. | diff --git a/source/FPSciApp.cpp b/source/FPSciApp.cpp index 9ab09db1..16191746 100644 --- a/source/FPSciApp.cpp +++ b/source/FPSciApp.cpp @@ -918,8 +918,8 @@ void FPSciApp::onSimulation(RealTime rdt, SimTime sdt, SimTime idt) { float hitDist = finf(); int hitIdx = -1; - shared_ptr target = weapon->fire(sess->hittableTargets(), hitIdx, hitDist, info, dontHit, false); // Fire the weapon - if (isNull(target)) // Miss case + Array> 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)) { @@ -1396,7 +1396,7 @@ void FPSciApp::onUserInput(UserInput* ui) { Model::HitInfo info; float hitDist = finf(); int hitIdx = -1; - shared_ptr target = weapon->fire(sess->hittableTargets(), hitIdx, hitDist, info, dontHit, true); // Fire the weapon + Array> 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 } diff --git a/source/Weapon.cpp b/source/Weapon.cpp index 29b0cde2..89e4e25b 100644 --- a/source/Weapon.cpp +++ b/source/Weapon.cpp @@ -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); @@ -341,7 +342,7 @@ void Weapon::clearDecals(bool clearHitDecal) { } } -shared_ptr Weapon::fire( +Array> Weapon::fire( const Array>& targets, int& targetIdx, float& hitDist, @@ -356,91 +357,93 @@ shared_ptr Weapon::fire( if (dummyShot) { spread = 0.f; } else { m_ammo -= 1; } + Array> hitTargets; // 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> 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; + for (int i = 0; i < m_config->pelletsPerShot; i++) { + 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& 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::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 beam = std::make_shared(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> 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; + } + bulletStartFrame.lookAt(aimPoint); + + // Non-laser weapon, draw a projectile + if (!m_config->isContinuous()) { + const shared_ptr& 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::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 beam = std::make_shared(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 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 < targets.size(); ++t) { + if (targets[t]->intersect(ray, closest, hitInfo)) { + closestIndex = t; + } + } + if (closestIndex >= 0) { + // Hit logic + hitTargets.append(targets[closestIndex]); // Assign the target pointer here (not null indicates the hit) + targetIdx = closestIndex; // Write back the index of the target + + m_hitCallback(targets[closestIndex]); // 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); } - } - 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) { diff --git a/source/Weapon.h b/source/Weapon.h index 286d657a..d26129cd 100644 --- a/source/Weapon.h +++ b/source/Weapon.h @@ -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 @@ -202,7 +203,7 @@ class Weapon : Entity { 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 fire(const Array>& targets, + Array> fire(const Array>& targets, int& targetIdx, float& hitDist, Model::HitInfo& hitInfo, From 24f276f5f297cd91bfd552a8e14bb0205ec003ff Mon Sep 17 00:00:00 2001 From: Ben Boudaoud Date: Thu, 19 Jan 2023 17:11:40 -0500 Subject: [PATCH 2/3] Fix for multi-pellet decals (decals on explosions) --- source/FPSciApp.cpp | 7 +++++-- source/FPSciApp.h | 2 +- source/Weapon.cpp | 26 ++++++++++++++++++-------- source/Weapon.h | 16 +++++++++++++--- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/source/FPSciApp.cpp b/source/FPSciApp.cpp index 16191746..e1a76796 100644 --- a/source/FPSciApp.cpp +++ b/source/FPSciApp.cpp @@ -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--; @@ -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 target) { +bool FPSciApp::hitTarget(shared_ptr target) { // Damage the target float damage = m_currentWeaponDamage; target->doDamage(damage); @@ -1285,6 +1286,7 @@ void FPSciApp::hitTarget(shared_ptr 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(); @@ -1311,6 +1313,8 @@ void FPSciApp::hitTarget(shared_ptr 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& target) { @@ -1391,7 +1395,6 @@ void FPSciApp::onUserInput(UserInput* ui) { for (GKey dummyShoot : keyMap.map["dummyShoot"]) { if (ui->keyPressed(dummyShoot) && (sess->currentState == PresentationState::referenceTarget) && !m_userSettingsWindow->visible()) { Array> dontHit; - dontHit.append(m_explosions); dontHit.append(sess->unhittableTargets()); Model::HitInfo info; float hitDist = finf(); diff --git a/source/FPSciApp.h b/source/FPSciApp.h index e498f6eb..56775ba1 100644 --- a/source/FPSciApp.h +++ b/source/FPSciApp.h @@ -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 target); + bool hitTarget(shared_ptr target); void missEvent(); public: diff --git a/source/Weapon.cpp b/source/Weapon.cpp index 89e4e25b..83eb0b43 100644 --- a/source/Weapon.cpp +++ b/source/Weapon.cpp @@ -347,7 +347,7 @@ Array> Weapon::fire( int& targetIdx, float& hitDist, Model::HitInfo& hitInfo, - Array>& dontHit, + Array> dontHit, bool dummyShot) { Ray ray = m_camera->frame().lookRay(); // Use the camera lookray for hit detection @@ -358,8 +358,11 @@ Array> Weapon::fire( else { m_ammo -= 1; } Array> hitTargets; - // Apply random rotation (for fire spread) + Array> remainingTargets = targets; + + // Iterate for each pellet in the shot for (int i = 0; i < m_config->pelletsPerShot; 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); @@ -373,10 +376,12 @@ Array> Weapon::fire( // Check for closest hit (in scene, otherwise this ray hits the skybox) float closest = finf(); Array> dontHitItems = dontHit; + dontHitItems.append(m_hitDecal); dontHitItems.append(m_currentMissDecals); - dontHitItems.append(targets); dontHitItems.append(m_projectiles); - m_scene->intersect(ray, closest, false, dontHitItems, hitInfo); + 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) @@ -419,17 +424,22 @@ Array> Weapon::fire( 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)) { + for (int t = 0; t < remainingTargets.size(); t++) { + if (remainingTargets[t]->intersect(ray, closest, hitInfo)) { closestIndex = t; } } if (closestIndex >= 0) { + const shared_ptr hitTarget = remainingTargets[closestIndex]; // Hit logic - hitTargets.append(targets[closestIndex]); // Assign the target pointer here (not null indicates the hit) + hitTargets.append(hitTarget); // Assign the target pointer here (not null indicates the hit) targetIdx = closestIndex; // Write back the index of the target - m_hitCallback(targets[closestIndex]); // If we did, we are in hitscan mode, apply the damage and manage the target here + // 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); } diff --git a/source/Weapon.h b/source/Weapon.h index d26129cd..4844fe70 100644 --- a/source/Weapon.h +++ b/source/Weapon.h @@ -137,6 +137,7 @@ class Weapon : Entity { shared_ptr m_fireAudio; ///< Audio channel for fire sound WeaponConfig* m_config; ///< Weapon configuration + Array> m_dontHit; ///< Objects that shouldn't be hit (currently app-generated explosions) Array> m_projectiles; ///< Arrray of drawn projectiles int m_lastBulletId = 0; ///< Bullet ID (auto incremented) @@ -149,7 +150,7 @@ class Weapon : Entity { shared_ptr m_scene; ///< Scene for weapon shared_ptr m_camera; ///< Camera for weapon - std::function)> m_hitCallback; ///< This is set to FPSciApp::hitTarget + std::function)> m_hitCallback; ///< This is set to FPSciApp::hitTarget std::function m_missCallback; ///< This is set to FPSciApp::missEvent int m_lastDecalID = 0; @@ -197,6 +198,15 @@ class Weapon : Entity { int shotsTaken() const { return m_config->maxAmmo - m_ammo; } void reload() { m_ammo = m_config->maxAmmo; } + void addToDontHit(shared_ptr e) { m_dontHit.append(e); } + bool removeFromDontHit(shared_ptr 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 @@ -207,7 +217,7 @@ class Weapon : Entity { int& targetIdx, float& hitDist, Model::HitInfo& hitInfo, - Array>& dontHit, + Array> dontHit, bool dummyShot); // Records provided lastFireTime @@ -237,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)> callback) { m_hitCallback = callback; } + void setHitCallback(std::function)> callback) { m_hitCallback = callback; } void setMissCallback(std::function callback) { m_missCallback = callback; } void setConfig(WeaponConfig* config) { m_config = config; } From f784e031eb403ce13b3cf2809a18492576209f92 Mon Sep 17 00:00:00 2001 From: Ben Boudaoud Date: Mon, 23 Jan 2023 15:41:50 -0500 Subject: [PATCH 3/3] Make reference target fire use a single pellet --- source/Weapon.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/source/Weapon.cpp b/source/Weapon.cpp index 83eb0b43..b33658ee 100644 --- a/source/Weapon.cpp +++ b/source/Weapon.cpp @@ -352,16 +352,20 @@ Array> Weapon::fire( { 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; } Array> hitTargets; Array> remainingTargets = targets; // Iterate for each pellet in the shot - for (int i = 0; i < m_config->pelletsPerShot; i++) { + 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") {