modified: content/2025-07-13-building-and-modding-multimower-with-dry-engine.rst

This commit is contained in:
Russell Ballestrini 2025-07-14 11:43:30 -04:00
parent 870ef65ff9
commit 48e94f60b7

View file

@ -288,15 +288,12 @@ Part 1: Tank Movement Controls with Realistic Acceleration
.. code-block:: cpp
class Mower : public Controllable
{
// Add these private members after existing variables:
// Add these private members after existing variables:
private:
// Acceleration system
float currentSpeed_;
float maxSpeed_;
float acceleration_;
};
**Step 2**: Initialize acceleration in ``src/mower.cpp`` constructor:
@ -304,9 +301,10 @@ Part 1: Tank Movement Controls with Realistic Acceleration
Mower::Mower(Context* context): Controllable(context),
// existing initializers...
lastFiredRight_{ false },
currentSpeed_{ 0.0f },
maxSpeed_{ 60.0f }, // 60 units/sec top speed
acceleration_{ 7.5f } // 0-60 in 8 seconds
acceleration_{ 7.5f } // 0-60 in 8 seconds
{
}
@ -323,9 +321,6 @@ Part 1: Tank Movement Controls with Realistic Acceleration
void Mower::HandleInput(float timeStep)
{
// Don't handle input if destroyed
if (isDestroyed_) return;
RigidBody* body = node_->GetComponent<RigidBody>();
if (!body) return;
@ -385,6 +380,8 @@ Part 1: Tank Movement Controls with Realistic Acceleration
}
}
**Note**: This is the initial version from Part 1. The health system checks (``isDestroyed_``) will be added in Part 2.
**Step 5**: Update the call site in ``Mower::Update()``:
.. code-block:: cpp
@ -405,15 +402,12 @@ Part 2: Health and Damage System with Random Damage
.. code-block:: cpp
class Mower : public Controllable
{
// Add these private members:
// Add these private members:
private:
// Health system
float health_;
float maxHealth_;
bool isDestroyed_;
};
**Step 2**: Initialize health in ``src/mower.cpp`` constructor:
@ -421,6 +415,7 @@ Part 2: Health and Damage System with Random Damage
Mower::Mower(Context* context): Controllable(context),
// existing initializers...
acceleration_{ 7.5f }, // 0-60 in 8 seconds
health_{ 100.0f },
maxHealth_{ 100.0f },
isDestroyed_{ false }
@ -453,6 +448,31 @@ Part 2: Health and Damage System with Random Damage
}
}
void Mower::DestroyMower()
{
if (isDestroyed_) return;
isDestroyed_ = true;
health_ = 0.0f;
currentSpeed_ = 0.0f; // Stop acceleration
DRY_LOGINFO("Mower destroyed!");
// Disable physics so it stops moving
RigidBody* body = node_->GetComponent<RigidBody>();
if (body)
{
body->SetEnabled(false);
}
// Hide the mower model
AnimatedModel* model = node_->GetComponent<AnimatedModel>();
if (model)
{
model->SetEnabled(false);
}
}
float Mower::GetHealthPercentage() const
{
return health_ / maxHealth_;
@ -462,63 +482,99 @@ Part 2: Health and Damage System with Random Damage
.. code-block:: cpp
// Check all mowers in the scene for bullet hits
PODVector<Node*> mowerNodes;
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
for (Node* mowerNode : mowerNodes)
// Replace the bullet update loop with collision detection
for (unsigned b{ 0 }; b < bullets_.Size(); )
{
Mower* otherMower = mowerNode->GetComponent<Mower>();
Projectile& bullet{ bullets_.At(b)};
bullet.age_ += timeStep;
Vector3 bulletPos = bullet.path_.Solve(bullet.age_);
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
// Check for hits on other mowers
bool bulletHit = false;
// Check all mowers in the scene
PODVector<Node*> mowerNodes;
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
for (Node* mowerNode : mowerNodes)
{
float distance = (bulletPos - otherMower->node_->GetWorldPosition()).Length();
if (distance < 5.0f) // Hit radius
Mower* otherMower = mowerNode->GetComponent<Mower>();
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
{
float damage = 1.0f + Random(4); // Random 1-4 damage
DRY_LOGINFOF("Bullet hit! Distance: %f, Damage: %.0f", distance, damage);
otherMower->TakeDamage(damage);
bullets_.EraseSwap(b);
bulletHit = true;
break;
float distance = (bulletPos - otherMower->node_->GetWorldPosition()).Length();
if (distance < 5.0f) // Hit radius
{
float damage = 1.0f + Random(4); // Random 1-4 damage
DRY_LOGINFOF("Bullet hit! Distance: %f, Damage: %.0f", distance, damage);
otherMower->TakeDamage(damage);
bullets_.EraseSwap(b);
bulletHit = true;
break;
}
}
}
if (!bulletHit && (bullet.age_ > 0.17f || bulletPos.y_ < .075f))
{
for (int s{ 0 }; s < 23; ++s)
Sparkle(bulletPos, 5.f);
bullets_.EraseSwap(b);
}
else if (!bulletHit)
{
++b;
}
}
**Step 6**: Add missile collision detection in the missile update loop:
.. code-block:: cpp
// Check for missile hits on other mowers
bool missileHit = false;
PODVector<Node*> mowerNodes;
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
for (Node* mowerNode : mowerNodes)
// In the missile update loop, add collision detection
if (missile.age_ < 4.f/3.f)
{
Mower* otherMower = mowerNode->GetComponent<Mower>();
missile.age_ += timeStep;
Sparkle(missile.path_.Solve(missile.age_), .1f);
// No self-damage for any mower - missiles only hit other mowers
bool allowHit = otherMower && otherMower != this && !otherMower->isDestroyed_;
Vector3 missilePos = missile.path_.Solve(missile.age_);
if (allowHit)
// Check for missile hits on other mowers
bool missileHit = false;
PODVector<Node*> mowerNodes;
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
for (Node* mowerNode : mowerNodes)
{
float distance = (missilePos - otherMower->node_->GetWorldPosition()).Length();
if (distance < 8.0f) // Larger explosion radius
Mower* otherMower = mowerNode->GetComponent<Mower>();
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
{
float damage = 10.0f + Random(16); // Random 10-25 damage
DRY_LOGINFOF("Missile hit! Distance: %f, Damage: %.0f", distance, damage);
otherMower->TakeDamage(damage);
missileHit = true;
break;
float distance = (missilePos - otherMower->node_->GetWorldPosition()).Length();
if (distance < 8.0f) // Larger explosion radius
{
float damage = 10.0f + Random(16); // Random 10-25 damage
DRY_LOGINFOF("Missile hit! Distance: %f, Damage: %.0f", distance, damage);
otherMower->TakeDamage(damage);
missileHit = true;
break;
}
}
}
if (missileHit || missile.age_ > 4.f/3.f || missilePos.y_ < .25f)
{
// Create explosion effects and remove missile
// ... existing explosion code ...
}
}
**Step 7**: Add health bar visualization in ``RenderDebug()`` method:
.. code-block:: cpp
// Add to RenderDebug() method in mower.cpp
// Render health bar above mower
if (!isDestroyed_)
{
@ -542,6 +598,42 @@ Part 2: Health and Damage System with Random Damage
- **Missiles**: Random 10-25 damage per hit (takes 4-10 missiles to destroy)
- **Total mower health**: 100 HP
**Step 8**: Update ``HandleInput()`` to prevent firing when dead (modify the method from Part 1):
.. code-block:: cpp
void Mower::HandleInput(float timeStep)
{
// Don't handle input if destroyed
if (isDestroyed_) return;
// ... existing movement code from Part 1 ...
// Fire bullet (only if not destroyed)
if (!isDestroyed_ && INPUT->GetMouseButtonDown(MOUSEB_LEFT) && sinceBullet >= bulletInterval)
{
bullets_.Push(Projectile{ gun_ });
sinceBullet = 0.f;
}
// Fire missile (only if not destroyed)
if (!isDestroyed_ && INPUT->GetMouseButtonPress(MOUSEB_RIGHT) && sinceMissile >= missileInterval)
{
FireMissile();
}
}
**Step 9**: Update AI firing in ``Update()`` method:
.. code-block:: cpp
else
{
// AI mowers only fire if not destroyed
if (!isDestroyed_ && Random(420) == 0)
FireMissile();
}
**Result**: Balanced combat with visible health bars and satisfying random damage!
Part 3: Death and Victory Effects
@ -549,7 +641,35 @@ Part 3: Death and Victory Effects
**Goal**: Create a complete end-game experience with massive explosions, death camera effects, electrical sparking, and victory celebrations.
Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
**Step 1**: Add new variables to ``src/mower.h``:
.. code-block:: cpp
// Add these private members:
private:
// Death camera
float deathCameraDistance_;
// Continuous sparking effect for destroyed mowers
float sparkTimer_;
// Victory flag to prevent multiple celebrations
bool victoryTriggered_;
**Step 2**: Initialize new variables in ``src/mower.cpp`` constructor:
.. code-block:: cpp
Mower::Mower(Context* context): Controllable(context),
// existing initializers...
isDestroyed_{ false },
deathCameraDistance_{ 10.0f }, // Initial camera distance on death
sparkTimer_{ 0.0f }, // Timer for continuous sparking
victoryTriggered_{ false } // Flag to prevent multiple victory celebrations
{
}
**Step 3**: Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
.. code-block:: cpp
@ -609,11 +729,21 @@ Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
body->SetEnabled(false);
}
// Simple visual change - just disable the model instead of trying to clone material
// Transform into a grayscale wreck instead of hiding
AnimatedModel* model = node_->GetComponent<AnimatedModel>();
if (model)
{
model->SetEnabled(false);
// Create grayscale material to show battle damage
SharedPtr<Material> grayscaleMaterial = model->GetMaterial(0)->Clone();
// Convert to grayscale - desaturate while keeping original brightness
grayscaleMaterial->SetShaderParameter("MatDiffColor", Color(0.4f, 0.4f, 0.4f)); // Grayscale
grayscaleMaterial->SetShaderParameter("MatSpecColor", Color(0.1f, 0.1f, 0.1f)); // Dull reflection
grayscaleMaterial->SetShaderParameter("MatEmissiveColor", Color(0.0f, 0.0f, 0.0f)); // No glow
model->SetMaterial(grayscaleMaterial);
DRY_LOGINFO("Applied grayscale material to destroyed mower");
}
}
@ -625,81 +755,14 @@ Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
- **Louder explosion sound** (1.5x volume)
- **Varied particle speeds** and positions for realism
**Step 2**: Add tracking variables to ``src/mower.h``:
.. code-block:: cpp
// Death camera
float deathCameraDistance_;
// Continuous sparking effect for destroyed mowers
float sparkTimer_;
**Step 2**: Initialize in constructor in ``src/mower.cpp``:
.. code-block:: cpp
Mower::Mower(Context* context): Controllable(context),
// existing initializers...
deathCameraDistance_{ 10.0f }, // Initial camera distance on death
sparkTimer_{ 0.0f } // Timer for continuous sparking
**Step 3**: Prevent firing when dead in ``HandleInput()`` and AI update:
.. code-block:: cpp
// Fire bullet (only if not destroyed)
if (!isDestroyed_ && INPUT->GetMouseButtonDown(MOUSEB_LEFT) && sinceBullet >= bulletInterval)
{
bullets_.Push(Projectile{ gun_ });
sinceBullet = 0.f;
}
// Fire missile (only if not destroyed)
if (!isDestroyed_ && INPUT->GetMouseButtonPress(MOUSEB_RIGHT) && sinceMissile >= missileInterval)
{
FireMissile();
}
And for AI mowers:
.. code-block:: cpp
else
{
// AI mowers only fire if not destroyed
if (!isDestroyed_ && Random(420) == 0)
FireMissile();
}
**Step 4**: Replace the model hiding code in ``DestroyMower()``:
.. code-block:: cpp
// Transform into a grayscale wreck instead of hiding
AnimatedModel* model = node_->GetComponent<AnimatedModel>();
if (model)
{
// Create grayscale material to show battle damage
SharedPtr<Material> grayscaleMaterial = model->GetMaterial(0)->Clone();
// Convert to grayscale - desaturate while keeping original brightness
grayscaleMaterial->SetShaderParameter("MatDiffColor", Color(0.4f, 0.4f, 0.4f)); // Grayscale
grayscaleMaterial->SetShaderParameter("MatSpecColor", Color(0.1f, 0.1f, 0.1f)); // Dull reflection
grayscaleMaterial->SetShaderParameter("MatEmissiveColor", Color(0.0f, 0.0f, 0.0f)); // No glow
model->SetMaterial(grayscaleMaterial);
DRY_LOGINFO("Applied grayscale material to destroyed mower");
}
**Step 5**: Add continuous sparking in ``Update()`` method:
**Step 4**: Add continuous sparking in ``Update()`` method:
.. code-block:: cpp
// Add to Update() method after existing timer updates
// Update spark timer for destroyed mowers
sparkTimer_ += timeStep;
// Continuous sparking effect for destroyed mowers
if (isDestroyed_ && sparkTimer_ >= 0.1f) // Spark every 0.1 seconds
{
@ -724,7 +787,8 @@ And for AI mowers:
}
}
**Step 6**: Add dynamic camera zoom-out in ``PostUpdate()`` when player is destroyed:
**Step 4**: Add dynamic camera zoom-out in ``PostUpdate()`` method:
.. code-block:: cpp
@ -735,8 +799,8 @@ And for AI mowers:
Jib* jib{ GetPlayer()->GetJib() };
// Handle death camera zoom
if (isDestroyed_ && jib && jib->GetCamera())
// Handle death camera zoom (both for death and victory)
if ((isDestroyed_ || victoryTriggered_) && jib && jib->GetCamera())
{
// Dynamic zoom speed - fast initially, then slow to a halt
float maxDistance = 50.0f; // Maximum zoom distance
@ -747,16 +811,16 @@ And for AI mowers:
deathCameraDistance_ += 60.0f * timeStep * speedFactor;
deathCameraDistance_ = Min(deathCameraDistance_, maxDistance); // Cap at max distance
// Set camera to orbit destroyed mower
// Set camera to orbit the mower (destroyed or victorious)
Node* cameraNode = jib->GetCamera()->GetNode();
Vector3 offset = Vector3::BACK * deathCameraDistance_ + Vector3::UP * (deathCameraDistance_ * 0.4f);
cameraNode->SetPosition(node_->GetWorldPosition() + offset);
cameraNode->LookAt(node_->GetWorldPosition(), Vector3::UP);
return; // Skip normal camera update when dead
return; // Skip normal camera update when dead or victorious
}
// Existing PostUpdate code continues here...
// ... existing PostUpdate code continues here ...
}
**Visual Effects**:
@ -770,13 +834,13 @@ And for AI mowers:
**Result**: When you die, all firing stops, the camera dramatically pulls back, and your mower becomes a gray wreck with continuous electrical sparking effects - like damaged electronics shorting out!
**Step 7**: Add victory condition system. First add the method declaration to ``src/mower.h``:
**Step 5**: Add victory condition system. First add the method declaration to ``src/mower.h``:
.. code-block:: cpp
void CheckWinCondition();
**Step 8**: Add victory check in ``src/mower.cpp`` Update method:
**Step 6**: Add victory check in ``src/mower.cpp`` Update method:
.. code-block:: cpp
@ -793,7 +857,7 @@ And for AI mowers:
// Rest of existing update code...
}
**Step 9**: Implement the victory detection system:
**Step 7**: Implement the victory detection system:
.. code-block:: cpp
@ -823,32 +887,38 @@ And for AI mowers:
}
// Check if all AI tanks are destroyed
if (totalAITanks > 0 && aliveTanks == 0)
if (totalAITanks > 0 && aliveTanks == 0 && !victoryTriggered_)
{
victoryTriggered_ = true; // Set flag to prevent multiple celebrations
// Player wins! Display victory message
DRY_LOGINFO("=== VICTORY! ALL ENEMY TANKS DESTROYED! ===");
// Create a big celebration explosion at player position
Vector3 playerPos = node_->GetWorldPosition();
// Victory celebration - minimal and clean
for (int i = 0; i < 15; ++i)
// Victory celebration - exciting but controlled (only fires once!)
for (int i = 0; i < 50; ++i)
{
Vector3 fireworkPos = playerPos + Vector3{RandomOffCenter(3.0f), Random(2.0f) + 1.0f, RandomOffCenter(3.0f)};
Sparkle(fireworkPos, 15.0f + Random(10.0f));
Vector3 fireworkPos = playerPos + Vector3{RandomOffCenter(6.0f), Random(4.0f) + 2.0f, RandomOffCenter(6.0f)};
Sparkle(fireworkPos, 25.0f + Random(15.0f));
}
// Small victory trail burst
for (int i = 0; i < 8; ++i)
// Victory trail burst
for (int i = 0; i < 20; ++i)
{
Vector3 trailStart = playerPos + Vector3{RandomOffCenter(2.0f), Random(1.5f), RandomOffCenter(2.0f)};
Vector3 trailStart = playerPos + Vector3{RandomOffCenter(4.0f), Random(2.0f) + 1.0f, RandomOffCenter(4.0f)};
Trail(trailStart);
}
// Single celebration light
Node* lightNode = GetScene()->CreateChild("VictoryLight");
lightNode->CreateComponent<BlastLight>();
lightNode->SetPosition(playerPos + Vector3::UP * 2.0f);
// Multiple celebration lights
for (int i = 0; i < 3; ++i)
{
Node* lightNode = GetScene()->CreateChild("VictoryLight");
lightNode->CreateComponent<BlastLight>();
Vector3 lightPos = playerPos + Vector3{RandomOffCenter(3.0f), Random(2.0f) + 3.0f, RandomOffCenter(3.0f)};
lightNode->SetPosition(lightPos);
}
// Victory sound
PlaySample(RES(Sound, "Samples/Explode.wav"), 2.0f);
@ -859,9 +929,9 @@ And for AI mowers:
}
**Victory Effects**:
- **Victory Sparkles**: 15 sparkles in a small area around the player (minimal and clean)
- **Victory Trails**: 8 colored trail effects radiating outward from player position
- **Celebration Light**: Single blast light above the player
- **Victory Sparkles**: 50 sparkles in a large area around the player (exciting celebration)
- **Victory Trails**: 20 colored trail effects radiating outward from player position
- **Celebration Lights**: 3 blast lights scattered around the player
- **Audio Feedback**: Victory explosion sound at double volume
- **Console Message**: Clear victory announcement with enemy count
- **Single Trigger**: Victory only fires once when condition is first met (prevents multiple celebrations per frame)
@ -886,7 +956,7 @@ And for AI mowers:
# Watch for log message: "=== VICTORY! ALL ENEMY TANKS DESTROYED! ==="
# Enjoy the massive fireworks celebration!
**Result**: When you destroy the last enemy tank, a clean and satisfying victory celebration appears around your mower with minimal sparkles, trails, a light, and victory sound - making you feel like a true tank commander without visual chaos!
**Result**: When you destroy the last enemy tank, a massive and exciting victory celebration appears around your mower with 50 sparkles, 20 trails, 3 lights, and victory sound - making you feel like a true tank commander with a spectacular fireworks display!
Complete Modding Results
~~~~~~~~~~~~~~~~~~~~~~~~