- What You'll Learn
- What is MultiMower?
- Prerequisites
- Downloading the Source Code
- Setting Up the Dry Engine
- Compiling MultiMower
- Running the Game
- Understanding the Codebase
- Version Compatibility
- Modding Tutorials
- Project Statistics
- Troubleshooting on Fedora
- Resources and Community
- Quick Start Summary
- Conclusion
What You'll Learn
By building and modding MultiMower, you'll gain experience with:
- C++ Game Development: Modern C++11 with game-specific patterns
- Physics Programming: Bullet Physics integration for vehicles and projectiles
- 3D Graphics: OpenGL with custom shaders and post-processing
- Game Architecture: Component systems, scene graphs, and input handling
- Asset Pipeline: Working with 3D models, textures, and audio
- Cross-Platform Development: CMake, qmake, make, and Linux development tools
This tutorial is perfect for intermediate programmers who want to understand game engine architecture and learn practical modding techniques.
What is MultiMower?
MultiMower is a physics-based 3D game where players control armed robotic lawn mowers. Built with the Dry Engine, it's currently in early development but provides an excellent foundation for learning game modding and physics programming.
Current Features: - Single mower type with basic weaponry (machine gun and missiles) - Multiple stationary computer-controlled mowers that randomly fire missiles in random directions - 3D arena with grass terrain and collision boundaries - Real-time physics simulation for movement and projectiles - Local player control with keyboard and mouse
Technical Foundation: - OpenGL 3D rendering with custom shaders - Bullet Physics integration for realistic movement - Component-based entity system - Sound system with SDL2 audio - Cross-platform C++ codebase
Development Status: MultiMower is an artistic technical demonstration in its current state. The original version provides a foundation with basic movement and projectile mechanics that we can enhance through modding. Our tutorial expands it into a full-featured tank combat experience.
Prerequisites
Before we begin, ensure you have the following installed on your Linux system:
For Ubuntu/Debian:
sudo apt-get update
sudo apt-get install -y git make cmake build-essential qtbase5-dev qt5-qmake \
libx11-dev libxrandr-dev libasound2-dev libegl1-mesa-dev \
libwayland-dev wayland-protocols
For Fedora:
sudo dnf install git make cmake gcc gcc-c++ qt5-qtbase-devel \
libX11-devel libXrandr-devel alsa-lib-devel mesa-libEGL-devel \
wayland-devel wayland-protocols-devel
Downloading the Source Code
First, let's clone the MultiMower repository and checkout the specific commit used in this tutorial:
cd ~/git
git clone git@gitlab.com:luckeyproductions/games/MultiMower.git
cd MultiMower
git checkout 60b5dcd6597fe6c7a480505f822a70e0a1469cda
Setting Up the Dry Engine
MultiMower uses the Dry Engine. Here's how to build it from source on Fedora:
Building Dry from Source on Fedora
- Clone the Dry repository and checkout the specific commit used in this tutorial:
cd ~/git
git clone git@gitlab.com:luckeyproductions/dry.git
cd dry
git checkout bc78ed2c3b2c52f21ccb639ace09c016bce8536d
- Install dependencies (the repository includes a helper script):
# This script automatically detects your distro and installs required packages
./script/installreq.sh
- Build Dry using the provided build script:
# For a minimal build (recommended for game development)
./quick.sh
# Or for a full build with samples and tools (recommended):
mkdir build && cd build
cmake .. -DDRY_64BIT=1 -DCMAKE_BUILD_TYPE=Release -DVIDEO_WAYLAND=OFF
make -j$(nproc)
Note: We disable Wayland (-DVIDEO_WAYLAND=OFF) to avoid linking issues. X11 support works perfectly for gaming.
- Set the DRY_HOME environment variable:
# Set DRY_HOME to your Dry build directory (adjust path for your username)
export DRY_HOME=$HOME/git/dry/build
# Make it permanent (add to ~/.bashrc)
echo "export DRY_HOME=$HOME/git/dry/build" >> ~/.bashrc
source ~/.bashrc
Building Dry with Debug Symbols
If you need debug symbols for development:
cd ~/git/dry
mkdir build-debug && cd build-debug
cmake .. -DDRY_64BIT=1 -DCMAKE_BUILD_TYPE=Debug -DVIDEO_WAYLAND=OFF
make -j$(nproc)
# Use this debug build instead
export DRY_HOME=$HOME/git/dry/build-debug
Compiling MultiMower
Now we're ready to build the game in its own directory (much more sensible than building inside the engine):
# Navigate to your MultiMower directory and create a build folder
cd ~/git/MultiMower
mkdir build && cd build
# CRITICAL: Set DRY_HOME before running qmake
export DRY_HOME=$HOME/git/dry/build
# Generate the Makefile (qmake will find Dry via DRY_HOME)
qmake ../MultiMower.pro
# Compile the game with parallel jobs for faster builds
make -j$(nproc)
# Copy the game resources to the build directory
cp -r ../Resources .
Build Notes:
- Build Location: The executable is created in ~/git/MultiMower/build/ (not in the Dry engine directory!)
- CRITICAL: You MUST set DRY_HOME=$HOME/git/dry/build before running qmake, or compilation will fail with "Dry/Dry.h: No such file or directory"
- User-Specific Paths: Replace $HOME with your actual home directory (e.g., /home/yourusername, /Users/yourusername, etc.)
- Warnings Expected: You'll see many compiler warnings about unused parameters and deprecated copy constructors from the Dry engine - these are normal and don't affect functionality
- Build Time: Compilation takes about 30-60 seconds on modern hardware
- File Size: The final executable is approximately 15MB
- Dependencies: Links against libDry.a from your $DRY_HOME, plus pthread, dl, and OpenGL
If everything went well, you should now have a multimower executable in ~/git/MultiMower/build/.
Running the Game
To run MultiMower, navigate to the build directory:
# The game builds in its own build directory
cd ~/git/MultiMower/build
# Run the game
./multimower
Important: The game must be run from the directory containing the Resources folder, or it won't find its assets.
Game Controls:
Player Controls (After Our Fixes): - Movement: WASD keys (W=forward, S=backward, A=turn left, D=turn right) - Aim: Mouse to look around and aim - Machine Gun: Left mouse button (fires bullets) - Missiles: Right mouse button (fires homing missiles)
Note: In the original version, WASD movement is broken and needs our modding fixes to work properly. The mouse aiming works out of the box.
Expected Output:
When you first run the game, you should see:
- A 3D area with tanks surrounding
- Robot mowers that can be controlled by players
- Physics-based movement and projectiles
- Visual effects like explosions & sparks
- Working audio: Launch sounds, explosion effects, and other game audio
Success Indicators: - Log shows: Set audio mode 44100 Hz stereo interpolated - No "Failed to initialise SDL subsystem" errors - Sound effects play when firing weapons with mouse clicks
Understanding the Codebase
Before modding, let's explore the project structure:
MultiMower/
├── src/ # C++ source files
│ ├── mastercontrol.cpp # Main game loop
│ ├── mower.cpp # Mower implementation
│ ├── inputmaster.cpp # Input handling
│ └── ...
├── Resources/ # Game assets
│ ├── Models/ # 3D models
│ ├── Textures/ # Texture files
│ ├── Shaders/ # GLSL shaders
│ └── ...
└── blends/ # Blender source files
Key classes to understand:
- MasterControl: Manages the game state and scene
- Mower: The player-controlled mower entity
- InputMaster: Handles keyboard and joystick input
- SpawnMaster: Manages entity spawning
Version Compatibility
This tutorial was tested and written for specific commits to ensure reproducibility:
- Dry Engine: commit bc78ed2c3b2c52f21ccb639ace09c016bce8536d
- MultiMower: commit 60b5dcd6597fe6c7a480505f822a70e0a1469cda
If you want to follow this tutorial exactly, check out these specific commits:
# Check out the exact Dry version used in this tutorial
cd ~/git/dry
git checkout bc78ed2c3b2c52f21ccb639ace09c016bce8536d
# Check out the exact MultiMower version used in this tutorial
cd ~/git/MultiMower
git checkout 60b5dcd6597fe6c7a480505f822a70e0a1469cda
Modding Tutorials
Here are three comprehensive mods that transform MultiMower into a proper tank combat game with realistic movement, damage systems, and spectacular explosions.
Part 1: Tank Movement Controls with Realistic Acceleration
Goal: Enhance the movement system with realistic tank-style acceleration and responsive controls.
Solution: Implement proper tank-style movement with realistic acceleration curves.
Step 1: Add acceleration variables to src/mower.h:
// 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:
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
{
}
Step 3: Update the method signature in src/mower.h:
private:
void HandleInput(float timeStep); // Add timeStep parameter
Step 4: Replace the HandleInput() method in src/mower.cpp:
void Mower::HandleInput(float timeStep)
{
RigidBody* body = node_->GetComponent<RigidBody>();
if (!body) return;
// Get inputs
float forwardInput = move_.z_; // W/S keys (-1 to 1)
float rotationInput = move_.x_; // A/D keys
// Calculate target speed based on input
float targetSpeed = forwardInput * maxSpeed_; // Can be negative for reverse
// Gradual acceleration/deceleration (0-60 in 8 seconds)
if (Abs(targetSpeed - currentSpeed_) > 0.1f)
{
if (targetSpeed > currentSpeed_)
{
currentSpeed_ += acceleration_ * timeStep;
if (currentSpeed_ > targetSpeed) currentSpeed_ = targetSpeed;
}
else
{
currentSpeed_ -= acceleration_ * timeStep;
if (currentSpeed_ < targetSpeed) currentSpeed_ = targetSpeed;
}
}
else
{
currentSpeed_ = targetSpeed; // Close enough, snap to target
}
// Get current facing direction and set velocity
Vector3 forward = node_->GetWorldDirection();
Vector3 desiredVelocity = forward * currentSpeed_;
body->SetLinearVelocity(desiredVelocity);
// Handle rotation
if (Abs(rotationInput) > 0.01f)
{
Vector3 torque = Vector3::UP * rotationInput * 15.0f;
body->ApplyTorque(torque);
}
// Angular damping for smoother rotation
Vector3 angularVel = body->GetAngularVelocity();
body->ApplyTorque(-angularVel * 8.0f);
// Fire bullet (existing code)
if (INPUT->GetMouseButtonDown(MOUSEB_LEFT) && sinceBullet >= bulletInterval)
{
bullets_.Push(Projectile{ gun_ });
sinceBullet = 0.f;
}
// Fire missile (existing code)
if (INPUT->GetMouseButtonPress(MOUSEB_RIGHT) && sinceMissile >= missileInterval)
{
FireMissile();
}
}
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():
if (GetPlayer())
{
HandleInput(timeStep); // Pass timeStep parameter
}
Result: Tanks now accelerate realistically from 0-60 in 8 seconds, reach high speeds, and feel like real heavy vehicles!
Part 2: Health and Damage System with Random Damage
Goal: Add hit points, collision detection, and random weapon damage for balanced combat.
Step 1: Add health variables to src/mower.h:
// Add these private members:
private:
// Health system
float health_;
float maxHealth_;
bool isDestroyed_;
Step 2: Initialize health in src/mower.cpp constructor:
Mower::Mower(Context* context): Controllable(context),
// existing initializers...
acceleration_{ 7.5f }, // 0-60 in 8 seconds
health_{ 100.0f },
maxHealth_{ 100.0f },
isDestroyed_{ false }
{
}
Step 3: Add damage methods to src/mower.h:
private:
void TakeDamage(float damage);
void DestroyMower();
float GetHealthPercentage() const;
Step 4: Implement damage methods in src/mower.cpp:
void Mower::TakeDamage(float damage)
{
if (isDestroyed_) return;
health_ -= damage;
DRY_LOGINFOF("Mower took %.1f damage, health now %.1f", damage, health_);
if (health_ <= 0.0f)
{
DestroyMower();
}
}
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_;
}
Step 5: Add collision detection in Mower::Update() method (in the bullet loop):
// Replace the bullet update loop with collision detection
for (unsigned b{ 0 }; b < bullets_.Size(); )
{
Projectile& bullet{ bullets_.At(b)};
bullet.age_ += timeStep;
Vector3 bulletPos = bullet.path_.Solve(bullet.age_);
// 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)
{
Mower* otherMower = mowerNode->GetComponent<Mower>();
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
{
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:
// In the missile update loop, add collision detection
if (missile.age_ < 4.f/3.f)
{
missile.age_ += timeStep;
Sparkle(missile.path_.Solve(missile.age_), .1f);
Vector3 missilePos = missile.path_.Solve(missile.age_);
// Check for missile hits on other mowers
bool missileHit = false;
PODVector<Node*> mowerNodes;
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
for (Node* mowerNode : mowerNodes)
{
Mower* otherMower = mowerNode->GetComponent<Mower>();
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
{
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:
// Add to RenderDebug() method in mower.cpp
// Render health bar above mower
if (!isDestroyed_)
{
Vector3 healthBarPos = node_->GetWorldPosition() + Vector3::UP * 3.0f;
float healthPercent = GetHealthPercentage();
Color healthColor = Color::GREEN.Lerp(Color::RED, 1.0f - healthPercent);
// Health bar background (dark)
debug->AddLine(healthBarPos - Vector3::RIGHT,
healthBarPos + Vector3::RIGHT,
Color::BLACK);
// Health bar (colored based on health)
debug->AddLine(healthBarPos - Vector3::RIGHT,
healthBarPos - Vector3::RIGHT + Vector3::RIGHT * 2.0f * healthPercent,
healthColor);
}
Weapon Damage Values: - Bullets: Random 1-4 damage per hit (takes 25-100 bullets to destroy) - 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):
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:
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
Goal: Create a complete end-game experience with massive explosions, death camera effects, electrical sparking, and victory celebrations.
Step 1: Add new variables to src/mower.h:
// 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:
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:
void Mower::DestroyMower()
{
if (isDestroyed_) return;
isDestroyed_ = true;
health_ = 0.0f;
currentSpeed_ = 0.0f; // Stop acceleration
DRY_LOGINFO("Mower destroyed! MASSIVE EXPLOSION!");
Vector3 explosionPos = node_->GetWorldPosition();
// HUGE explosion effect - like multiple rockets going off!
for (int i = 0; i < 200; ++i) // 200 sparkles instead of 30
{
Sparkle(explosionPos + Vector3{RandomOffCenter(5.0f), Random(3.0f), RandomOffCenter(5.0f)},
30.0f + Random(20.0f)); // Varied speed and position
}
// Create multiple trail effects radiating outward (like rocket exhaust)
for (int i = 0; i < 50; ++i)
{
Vector3 trailStart = explosionPos + Vector3{RandomOffCenter(2.0f), Random(1.0f), RandomOffCenter(2.0f)};
Trail(trailStart);
}
// Multiple explosion sounds for massive effect
PlaySample(RES(Sound, "Samples/Explode.wav"), 1.5f); // Main explosion
// Add delayed secondary explosions (ammo cook-off effect)
for (int i = 0; i < 8; ++i)
{
// Create delayed sparkle bursts
for (int j = 0; j < 25; ++j)
{
Vector3 delayedPos = explosionPos + Vector3{RandomOffCenter(8.0f), Random(4.0f), RandomOffCenter(8.0f)};
Sparkle(delayedPos, 25.0f);
}
}
// Create blast lights for dramatic effect
for (int i = 0; i < 5; ++i)
{
Node* lightNode = GetScene()->CreateChild("BlastLight");
lightNode->CreateComponent<BlastLight>();
Vector3 lightPos = explosionPos + Vector3{RandomOffCenter(3.0f), Random(2.0f) + 1.0f, RandomOffCenter(3.0f)};
lightNode->SetPosition(lightPos);
}
// Disable physics so it stops moving
RigidBody* body = node_->GetComponent<RigidBody>();
if (body)
{
body->SetEnabled(false);
}
// 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");
}
}
Explosion Features: - 200 sparkles scattered over a large area (vs 30 in original) - 50 rocket exhaust trails radiating outward - 8 secondary explosion bursts simulating ammo cook-off - 5 dramatic blast lights for cinematic effect - Louder explosion sound (1.5x volume) - Varied particle speeds and positions for realism
Step 4: Add continuous sparking in Update() method:
// 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
{
sparkTimer_ = 0.0f; // Reset timer
// Create multiple sparks around the destroyed mower
Vector3 mowerPos = node_->GetWorldPosition();
for (int i = 0; i < 3; ++i)
{
Vector3 sparkPos = mowerPos + Vector3{RandomOffCenter(2.0f), Random(1.0f), RandomOffCenter(2.0f)};
Sparkle(sparkPos, 3.0f); // Electrical sparking effect
}
// Occasional larger sizzle effect
if (Random(5) == 0) // 20% chance
{
for (int i = 0; i < 8; ++i)
{
Vector3 sizzlePos = mowerPos + Vector3{RandomOffCenter(1.5f), Random(0.8f), RandomOffCenter(1.5f)};
Sparkle(sizzlePos, 5.0f); // Bigger electrical discharge
}
}
}
Step 4: Add dynamic camera zoom-out in PostUpdate() method:
void Mower::PostUpdate(float timeStep)
{
if (!GetPlayer())
return;
Jib* jib{ GetPlayer()->GetJib() };
// 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
float speedFactor = 1.0f - (deathCameraDistance_ / maxDistance); // 1.0 to 0.0
speedFactor = Max(0.1f, speedFactor); // Don't go below 0.1
// Fast zoom initially (60 units/sec), slowing down as we get further
deathCameraDistance_ += 60.0f * timeStep * speedFactor;
deathCameraDistance_ = Min(deathCameraDistance_, maxDistance); // Cap at max distance
// 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 or victorious
}
// ... existing PostUpdate code continues here ...
}
Visual Effects: - Grayscale Material: Mower turns gray (0.4 RGB) showing battle damage - Continuous Sparking: 3 electrical sparks every 0.1 seconds around the wreck - Sizzle Bursts: 20% chance of 8 larger electrical discharges - Dynamic Camera Movement: Fast zoom initially (60 units/sec), slowing to a halt at 50 units distance - No Weapon Firing: Both player and AI mowers stop firing when destroyed - Persistent Effect: Sparking continues forever, showing damaged electronics - No Self-Damage: Neither bullets nor missiles can damage the mower that fired them
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 5: Add victory condition system. First add the method declaration to src/mower.h:
void CheckWinCondition();
Step 6: Add victory check in src/mower.cpp Update method:
void Mower::Update(float timeStep)
{
// Existing update code...
// Check win condition - if player exists, check if all AI tanks are destroyed
if (GetPlayer())
{
CheckWinCondition();
}
// Rest of existing update code...
}
Step 7: Implement the victory detection system:
void Mower::CheckWinCondition()
{
// Only check win condition if this is the player mower
if (!GetPlayer() || isDestroyed_) return;
// Get all mowers in the scene
PODVector<Node*> mowerNodes;
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
int aliveTanks = 0;
int totalAITanks = 0;
for (Node* mowerNode : mowerNodes)
{
Mower* mower = mowerNode->GetComponent<Mower>();
if (mower && !mower->GetPlayer()) // AI mower
{
totalAITanks++;
if (!mower->isDestroyed_)
{
aliveTanks++;
}
}
}
// Check if all AI tanks are destroyed
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 - exciting but controlled (only fires once!)
for (int i = 0; i < 50; ++i)
{
Vector3 fireworkPos = playerPos + Vector3{RandomOffCenter(6.0f), Random(4.0f) + 2.0f, RandomOffCenter(6.0f)};
Sparkle(fireworkPos, 25.0f + Random(15.0f));
}
// Victory trail burst
for (int i = 0; i < 20; ++i)
{
Vector3 trailStart = playerPos + Vector3{RandomOffCenter(4.0f), Random(2.0f) + 1.0f, RandomOffCenter(4.0f)};
Trail(trailStart);
}
// 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);
// Log stats
DRY_LOGINFOF("Victory achieved! Defeated %d enemy tanks!", totalAITanks);
}
}
Victory Effects: - 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)
How It Works:
- Continuous Checking: Every frame, the player mower scans all tanks in the scene
- AI Detection: Counts tanks that don't have a player component (AI-controlled)
- Status Verification: Checks if each AI tank is destroyed using isDestroyed_ flag
- Victory Trigger: When aliveTanks == 0 and totalAITanks > 0, victory fires
- Celebration: Immediate massive fireworks display centered on player position
- Performance: Efficient - only player mower checks, stops when victory achieved
Testing Victory:
# Run the game and destroy all enemy tanks
./multimower
# Victory triggers automatically when last enemy dies
# Watch for log message: "=== VICTORY! ALL ENEMY TANKS DESTROYED! ==="
# Enjoy the massive fireworks celebration!
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
After implementing all three parts, you'll have:
- Realistic Tank Movement: 0-60 acceleration in 8 seconds, high top speeds, smooth control
- Balanced Combat: Random damage (1-4 bullets, 10-25 missiles), health bars, collision detection
- Complete End-Game Experience: Massive explosions, death camera effects, electrical sparking, and victory celebrations
Total Changes: - Files Modified: src/mower.h, src/mower.cpp, src/inputmaster.cpp, src/mastercontrol.cpp - Lines Added: +392 lines of code, -16 lines removed - Net Addition: +376 lines of code - New Features: Acceleration system, health system, collision detection, massive explosions, death effects, victory celebrations - Gameplay Impact: Transforms MultiMower into a complete cinematic tank combat experience
Testing Your Mods:
# Compile with your changes
make clean && make -j$(nproc)
# Run the enhanced game
./multimower
# Test the features:
# - W/S for realistic acceleration/deceleration
# - A/D for tank turning
# - Left click for machine gun (1-4 damage)
# - Right click for missiles (10-25 damage)
# - Destroy enemy tanks for massive explosions!
# - Destroy all enemy tanks for victory fireworks!
Project Statistics
Here are the actual numbers behind this modding tutorial, showing the scope and impact of our changes:
Engine and Game Size
Dry Engine (commit bc78ed2c): - Source Files: 4,330 files (.cpp, .h, .c) - Lines of Code: 199,687 total lines - Repository Size: 1.1 GB (includes samples, tools, third-party libraries) - Static Library: 44 MB (libDry.a) - Build Time: ~5-8 minutes on modern hardware
MultiMower Game (commit 60b5dcd): - Source Files: 18 files (.cpp, .h) - Lines of Code: 2,346 total lines - Repository Size: 31 MB (includes assets, models, textures) - Game Resources: 5.3 MB (3D models, textures, sounds) - Final Executable: 15 MB (statically linked with Dry) - Build Time: ~30-60 seconds
Our Modding Impact
Code Changes Made: - Files Modified: 4 source files - Lines Added: +392 lines of code - Lines Removed: -16 lines of code - Net Addition: +376 lines of code
Breakdown by File: - src/mower.cpp: +359 lines, -13 lines (main implementation) - src/mower.h: +21 lines, -1 line (new variables and methods) - src/inputmaster.cpp: +9 lines, -1 line (debug logging) - src/mastercontrol.cpp: +3 lines, -1 line (debug logging)
Feature Implementation by Parts: - Part 1 - Tank Movement: ~80 lines (acceleration system, realistic physics) - Part 2 - Health & Damage: ~120 lines (collision detection, health bars, damage system) - Part 3 - Death & Victory: ~160 lines (explosions, camera effects, sparking, victory system) - Supporting Code: ~16 lines (debugging, initialization, cleanup)
Scale Perspective
Code Efficiency: - Our 376 lines represent 0.16% of the total MultiMower codebase - Yet they transform the game from broken demo to complete tank combat with victory conditions - Shows the power of focused, strategic modifications
Engine vs Game: - Dry Engine: 85x larger than MultiMower (199K vs 2.3K lines) - MultiMower: 35x smaller repository than Dry (31MB vs 1.1GB) - Game executable includes the entire engine statically linked
Development Effort: - Engine Development: Years of work, complex graphics/physics systems - Game Development: Weeks of work, focused gameplay mechanics - Our Modding: Hours of work, targeted feature additions
Build Performance: - Dry Engine: Takes 5-8 minutes to compile from scratch - MultiMower: Takes 30-60 seconds with incremental changes - Our Patches: Compile in 5-10 seconds after initial build
Real-World Impact
Before Our Mods: - Broken movement (WASD didn't work) - No combat system (weapons fire but no damage) - Basic explosions (30 particles, minimal effect) - Tech demo quality
After Our Mods: - Realistic tank physics (0-60 acceleration) - Balanced combat system (random damage, health bars) - Cinematic explosions (200+ particles, multiple effects) - Playable game quality
Modding Accessibility: - Small Changes, Big Impact: 376 lines for complete gameplay overhaul - Well-Structured Code: Clear separation of concerns makes modding easier - Engine Stability: Dry engine handles our changes without crashes - Rapid Iteration: Quick compile times enable fast experimentation
This demonstrates how a well-designed engine like Dry enables rapid game development and modding. Our relatively small code additions (0.16% of the codebase) fundamentally transformed the gameplay experience, showing the power of targeted improvements in the right places.
Troubleshooting on Fedora
Common Build Issues
- "Dry/Dry.h: No such file or directory": This happens when DRY_HOME isn't set before running qmake:
# Check if DRY_HOME is set correctly
echo $DRY_HOME
# Should print: /home/yourusername/git/dry/build (with your actual username)
# If empty or wrong, fix it:
export DRY_HOME=$HOME/git/dry/build
# Remove old Makefile and regenerate
rm -f Makefile
qmake ../MultiMower.pro
make -j$(nproc)
- Build directory confusion: Always build in MultiMower's own build directory, not in the Dry engine:
# Correct approach - build in game directory
cd ~/git/MultiMower/build
# Check your executable is where you expect
ls -la multimower
- Resources not found at runtime: The game needs the Resources folder:
# Copy resources to the build directory
cd ~/git/MultiMower/build
cp -r ../Resources .
- Library not found errors: Ensure all dependencies are installed:
# Re-run the dependency installer
cd ~/git/dry
./script/installreq.sh
- Qt6 vs Qt5 conflicts: The build uses Qt6 by default on newer Fedora:
# If you need to force Qt5 (though Qt6 works fine)
/usr/lib64/qt5/bin/qmake MultiMower.pro
- Compiler warnings: Expect many warnings - they're from the Dry engine and don't affect functionality:
- Unused parameter warnings
- Deprecated copy constructor warnings
- Type-punned pointer warnings
- These are normal and the game will work fine
- Wayland linking errors: If you see undefined reference to 'wl_proxy_*' errors:
# Rebuild Dry without Wayland (X11 is preferred for gaming anyway)
cd ~/git/dry
rm -rf build && mkdir build && cd build
cmake .. -DDRY_64BIT=1 -DCMAKE_BUILD_TYPE=Release -DVIDEO_WAYLAND=OFF
make -j$(nproc)
- Audio issues: If you get "No available audio device" errors:
# The issue is usually that Dry was built before audio packages were installed
# Rebuild Dry after ensuring audio development packages are present
cd ~/git/dry
rm -rf build && mkdir build && cd build
cmake .. -DDRY_64BIT=1 -DCMAKE_BUILD_TYPE=Release -DVIDEO_WAYLAND=OFF
make -j$(nproc)
Real-World Build Experience
When I tested this process on Fedora, here's what actually happened:
- Final executable: 15MB located in ~/git/MultiMower/build/multimower
- Warnings: About 50+ compiler warnings (all safe to ignore)
- Memory usage: Game links against a 45MB libDry.a static library
- Success rate: 100% when following the exact steps above
- Common issues: Wayland linking errors (fixed by disabling Wayland), audio initialization failures (fixed by rebuilding Dry with audio packages installed)
- Audio confirmation: Working audio with Launch.wav and Explode.wav sound effects
- Controls working: WASD movement (after code fix)
Resources and Community
- MultiMower Repository: https://gitlab.com/luckeyproductions/games/MultiMower
- Dry Engine: https://gitlab.com/luckeyproductions/dry
- Dry Engine Documentation: Available in the Dry repository
Quick Start Summary
For experienced developers, here's the TL;DR version:
# Install dependencies on Fedora
sudo dnf install git make cmake gcc gcc-c++ qt5-qtbase-devel \
libX11-devel libXrandr-devel alsa-lib-devel mesa-libEGL-devel \
wayland-devel wayland-protocols-devel
# Build Dry engine
cd ~/git
git clone https://gitlab.com/luckeyproductions/dry.git
cd dry && mkdir build && cd build
cmake .. -DDRY_64BIT=1 -DCMAKE_BUILD_TYPE=Release -DVIDEO_WAYLAND=OFF
make -j$(nproc)
# Build MultiMower (CRITICAL: set DRY_HOME first!)
export DRY_HOME=$HOME/git/dry/build
cd ~/git
git clone https://gitlab.com/luckeyproductions/games/MultiMower.git
cd MultiMower && mkdir build && cd build
qmake ../MultiMower.pro && make -j$(nproc)
# Run the game
cp -r ../Resources .
./multimower
Conclusion
MultiMower provides an excellent platform for learning game development and modding. The clean codebase and use of the Dry Engine make it accessible for beginners while offering enough depth for advanced modifications. Whether you're adding new mower types, creating custom game modes, or just tweaking the physics, there's plenty of room for creativity.
The build process is straightforward on Fedora when you follow these tested steps, and the Dry engine's helper scripts make dependency management painless.
Happy mowing & modding!