I'm not that great with threads, parallel programming so I won't comment about them... However, as for design decisions; I've taken your code and wrote my own version. I wouldn't say that the code that I'm about to present to you as an example is 100% bug proof. It does currently run and compile producing some interesting results! This has more to do with class hierarchies, the relationships between them, and their differences. I used a CRTP structure. I gave the Character classes the ability to assign function pointers for attack
and defending
which can be created by the user to be able to customize how they want a specific character or enemy type to perform an attack or to defend. I did not program any "game logic" but I have randomly generated a player where the user can choose between 3 types (no need to inherit here from the player class), and to randomly generate 3 different kinds of enemies (which are inherited)... Here's my version of the code that you can use for a reference and build off of. As for writing the game logic (that's for you to build and design), as for threading and parallel programming, it's beyond what I can explain. I can write some code that does it, but explaining it is not my strong suit! The code is quite long so I split it into 2 files due to some of the overloaded operators that I had to write for easier printing. Make sure to read the comments in the code too... it shows where I had made some design decisions, and where I modified the input values.
main.cpp
#include "Game.h"
int main() {
try {
Game game;
game.run();
}
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Here's a potential output since the number of enemies is randomly generated...
Output
Welcome to Dungeon Raid!
Choose your character type
1 - Human:
2 - Dwarven:
3 - Elven:
3
Main Player Info:
Elven
level:45
health: 5493.489746
attack: 919.298096
defense: 970.807129
response: 91.92981
delay: 97.08071
recover: 746.64215
Orc_000
level:71
health: 9015.84277
attack: 2291.32764
defense: 2364.90454
response: 229.13277
delay: 236.49046
recover: 1225.37927
Orc_001
level:58
health: 7145.38623
attack: 1581.78455
defense: 1630.08899
response: 158.17845
delay: 163.00890
recover: 971.15802
Orc_002
level:36
health: 5356.60059
attack: 723.04858
defense: 653.49048
response: 72.30486
delay: 65.34905
recover: 728.03699
Troll_000
level:29
health: 4248.76318
attack: 458.06143
defense: 453.84021
response: 45.80614
delay: 45.38402
recover: 577.46637
Troll_001
level:92
health: 13566.97852
attack: 4404.49219
defense: 4765.45508
response: 440.44922
delay: 476.54550
recover: 1843.94226
Orc_003
level:74
health: 9432.72852
attack: 2752.44165
defense: 2504.81201
response: 275.24417
delay: 250.48120
recover: 1282.03979
Orc_004
level:29
health: 4301.33301
attack: 426.52374
defense: 492.74667
response: 42.65237
delay: 49.27467
recover: 584.61139
Troll_002
level:100
health: 14677.85352
attack: 5369.20947
defense: 5856.85938
response: 536.92096
delay: 585.68597
recover: 1994.92578
Troll_003
level:47
health: 6805.82422
attack: 1253.68689
defense: 1255.42249
response: 125.36869
delay: 125.54225
recover: 925.00677
...and now for the actual classes to make it all work.
Game.h
#pragma once
// error handling
#include <exception>
// numerics, algorithms, properties, limits
#include <algorithm>
#include <cstdint>
#include <limits>
#include <numeric>
#include <random>
#include <type_traits>
// string and stream libraries
#include <fstream>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <string>
// containers
#include <array>
//#include <vector>
#include <concurrent_vector.h>
#include <concurrent_priority_queue.h>
// memory, threads, etc.
#include <memory>
#include <mutex>
#include <thread>
// When adding a new type: must update operators
enum class PlayerType {
HUMAN = 1,
DWARVEN,
ELVEN
};
std::istream& operator>>(std::istream& in, PlayerType& type);
std::ostream& operator<<(std::ostream& in, const PlayerType& type);
// When adding a new type: must update operators
enum class EnemyType {
GOBLIN = 1,
ORC,
TROLL
};
std::istream& operator>>(std::istream& in, EnemyType& type);
std::ostream& operator<<(std::ostream& out, const EnemyType& type);
struct CharacterInfo {
std::uint32_t level_;
float health_;
float attack_power_;
float defense_;
float time_response_; // range [0,1] // same as speed or how fast they can attack
float time_delay_; // range [0,1] // time delay before next attack
float recovery_rate_; // range [0,1] // how fast they can recover, regain health, etc...
CharacterInfo();
CharacterInfo(std::uint32_t level, float health, float attackPower, float defense,
float timeResponse, float timeDelay, float recoverRate);
CharacterInfo(const CharacterInfo& other);
CharacterInfo& operator=(const CharacterInfo& other);
};
std::istream& operator>>(std::istream& in, CharacterInfo& info);
std::ostream& operator<<(std::ostream& out, const CharacterInfo& info);
class Character;
typedef float(*AttackFunc)(Character* attacker, Character* defender, float time_response);
typedef void(*DefendOrBlockFunc)(Character* defender, Character* attacker, float time_response);
template<class T>
class EntityID {
protected:
explicit EntityID(std::string& id) {
static int i = 0;
std::stringstream strValue;
strValue << std::setw(3) << std::setfill('0') << std::to_string(i++);
id.append("_" + strValue.str());
}
virtual ~EntityID() = default;
};
class Character {
protected:
std::string id_ = "";
AttackFunc attack_;
CharacterInfo info_;
DefendOrBlockFunc defend_;
explicit Character(CharacterInfo& info, const std::string& id = "")
: info_{ info }, id_{std::move(id)} {}
public:
virtual ~Character() = default;
Character* operator()() { return this; }
CharacterInfo* info() { return &info_; }
std::string& id() { return id_; }
float health() const { return info_.health_; }
float attackPower() const { return info_.attack_power_; }
float defense() const { return info_.defense_; }
float response() const { return info_.time_response_; }
float delay() const { return info_.time_delay_; }
float recovery() const { return info_.recovery_rate_; }
void assignAttack(AttackFunc func) { attack_ = func; }
void assignBlock(DefendOrBlockFunc func) { defend_ = func; }
inline virtual void healthModifier(const Character& other) {
auto power = other.attackPower();
this->info_.health_ -= (power - this->info_.defense_);
}
float attack(Character* defender, float time_response) { return attack_(this, defender, time_response); }
void defend(Character* attacker, float time_response) { return defend_(this, attacker, time_response); }
};
template<typename Ty>
const std::string nameOfCharacterType(Ty type) {
std::stringstream name;
name << type;
return name.str();
}
class Player final : public Character, EntityID<Player> {
PlayerType player_type_;
public:
Player(CharacterInfo& info, PlayerType type, const std::string& name = "Player" )
: Character(info, std::move(name)), EntityID(id()), player_type_{ type }
{}
virtual ~Player() = default;
Player* operator()() { return this; }
inline virtual void healthModifier(const Character& other) override {
// modify as desired... leave blank for default
}
PlayerType type() const { return player_type_; }
};
class Enemy : public Character {
EnemyType enemy_type_;
protected:
Enemy(CharacterInfo& info,EnemyType type, const std::string& name = "Enemy")
: Character(info, std::move(name)), enemy_type_{ type } {}
public:
virtual ~Enemy() = default;
inline virtual void healthModifier(const Character& other) override {
// modify as desired... leave blank for default
}
Enemy* operator()() { return this; }
EnemyType type() const { return enemy_type_; }
};
class Goblin final : public Enemy, EntityID<Goblin> { // remove final if you want to derive from Goblin
public:
Goblin(CharacterInfo& info, EnemyType type, const std::string& name = "Goblin")
: Enemy(info, type, std::move(name)), EntityID(id()) {}
virtual ~Goblin() = default;
Goblin* operator()() { return this; }
virtual void healthModifier(const Character& other) override {
// modify as desired... leave blank for default
}
};
class Orc final : public Enemy, EntityID<Orc> { // remove final if you want to derive from Orc
public:
Orc(CharacterInfo& info, EnemyType type, const std::string& name = "Orc")
: Enemy(info, type, std::move(name)), EntityID(id()) {}
virtual ~Orc() = default;
Orc* operator()() { return this; }
virtual void healthModifier(const Character& other) override {
// modify as desired... leave blank for default
}
};
class Troll final : public Enemy, EntityID<Troll> { // remove final if you want to derive from Troll
public:
Troll(CharacterInfo& info, EnemyType type, const std::string& name = "Troll")
: Enemy(info, type, std::move(name)), EntityID(id()) {}
virtual ~Troll() = default;
Troll* operator()() { return this; }
virtual void healthModifier(const Character& other) override {
// modify as desired... leave blank for default
}
};
std::ostream& operator<<(std::ostream& out, Player& player);
std::ostream& operator<<(std::ostream& out, Orc& orc);
std::ostream& operator<<(std::ostream& out, Troll& troll);
// define your attack and defense functions for the function pointers here! Or create a lambda within the initialize function.
class Game {
std::unique_ptr<Player> player_;
std::vector<std::unique_ptr<Enemy>> enemies_;
std::random_device rd;
std::mt19937 gen{ rd() };
public:
Game() {
initialize();
}
void initialize() {
std::cout << "Welcome to Dungeon Raid!\n";
createPlayer();
generateRandomEnemies();
// here is where you would assign the attack and defence function pointers:
// player_->assignAttack();
// player_->assignBlock();
/*for (auto& e : enemies_) {
e->assignAttack();
e->assignBlock();
}*/
}
void run() {
// main loop, user input, game logic here
// for testing purposes, let's just print out our player and enemy info as lists:
std::cout << "\nMain Player Info:\n" << player_->type() << '\n' << *player_->info() << '\n';
for (auto& e : enemies_) {
std::cout << e->id() << '\n' << *e->info() << std::endl;
}
}
private:
void createPlayer() {
PlayerType type;
CharacterInfo playerInfo;
retrievePlayerInfo(type, playerInfo);
this->player_.reset(new Player{ playerInfo, type, nameOfCharacterType(type) });
}
void showPlayerChoice(PlayerType& type) {
std::cout << "Choose your character type\n";
std::cout << "1 - Human:\n"
<< "2 - Dwarven:\n"
<< "3 - Elven:\n";
std::string str;
std::getline(std::con, str);
std::uint32_t val = std::stoi(str); // can improve this to fix invalid input types...
type = static_cast<PlayerType>(val);
}
void retrievePlayerInfo(PlayerType& type, CharacterInfo& playerInfo) {
bool properType = false;
do {
if (!properType)
showPlayerChoice(type);
switch (type) {
case PlayerType::HUMAN: {
playerInfo = generateRandomStats(); // CharacterInfo{ 1, 10, 4, 3, 0.7f, 0.5f, 0.4f };
properType = true; break;
}
case PlayerType::DWARVEN: {
playerInfo = generateRandomStats(); // CharacterInfo{ 1, 12, 4, 4, 0.5f, 0.3f, 0.6f };
properType = true; break;
}
case PlayerType::ELVEN: {
playerInfo = generateRandomStats(); // CharacterInfo{ 1, 9, 3, 3, 0.8f, 0.2f, 0.7f };
properType = false; break;
}
default: {
properType = false; break;
}
}
} while (!properType);
}
void generateRandomEnemies() {
std::uniform_int_distribution<std::uint32_t> enemyCountDist{ 5, 20 }; // 5 to 20 enemies
std::uint32_t enemyCount = enemyCountDist(gen);
enemies_.resize(enemyCount);
std::uniform_int_distribution<std::uint32_t> enemyTypeDist{ 1, 3 }; // 1 = Goblin, 2 = Orc, 3 = Troll
EnemyType type;
CharacterInfo enemyInfo;
for (unsigned i = 0; i < enemyCount; i++) {
type = static_cast<EnemyType>( enemyTypeDist(gen) );
switch (type) {
case EnemyType::GOBLIN: {
enemyInfo = generateRandomStats(); // CharacterInfo{ 1, 5, 2, 3, 0.9f, 0.2f, 0.9f };
this->enemies_[i].reset(new Goblin{ enemyInfo, type });
}
case EnemyType::ORC: {
enemyInfo = generateRandomStats(); // CharacterInfo{ 1, 7, 5, 8, 0.3f, 0.4f, 0.6f };
this->enemies_[i].reset(new Orc{ enemyInfo, type });
break;
}
case EnemyType::TROLL: {
enemyInfo = generateRandomStats(); // CharacterInfo{ 1, 14, 5, 8, 0.3f, 0.4f, 0.6f };
this->enemies_[i].reset(new Troll{ enemyInfo, type });
break;
}
}
}
}
CharacterInfo generateRandomStats() {
// Generate a Random level in the range of [1,20] for the player
std::uniform_int_distribution<std::uint32_t> randomLevelDist(1, 100);
std::uint32_t randomLevel = randomLevelDist(gen);
// Character states will be based on the curve of the level
// Generate Random Stats: Level 1 base health = 100
// Health Range = ((Base Health * Multiplyer) + (Base Health * Level)) / BaseHealth
const float baseHealth = 10.0f;
auto baseMinMultiplyer = 1.2f;
auto baseMaxMultiplyer = 1.5f;
auto baseLevelHealth = (baseHealth * randomLevel);
auto lowerRange = baseHealth * baseMinMultiplyer * baseLevelHealth;
auto upperRange = baseHealth * baseMaxMultiplyer * baseLevelHealth;
std::uniform_real_distribution<float> dist(lowerRange, upperRange);
auto randomHealth = dist(gen);
// Attack & Defense Range = 50% of health
auto healthPercentage = randomHealth * 0.5f;
lowerRange /= randomLevel;
upperRange /= randomLevel;
std::uniform_real_distribution<float> randomAttackDefenceDist(healthPercentage / upperRange, healthPercentage / lowerRange);
auto randomAttack = randomAttackDefenceDist(gen) * randomLevel;
auto randomDefense = randomAttackDefenceDist(gen) * randomLevel;
// Time Response and Delay is based off of attack and defense where recovery is based off of health
auto randomResponse = randomAttack * 0.1f;
auto randomDelay = randomDefense * 0.1f;
auto randomRecovery = randomHealth * 0.271828f * 0.5f; // 0.271828 approximate e/10
// Create our Info
return CharacterInfo{ randomLevel, randomHealth, randomAttack, randomDefense, randomResponse, randomDelay, randomRecovery };
}
};
Game.cpp
#include "Game.h"
std::istream& operator>>(std::istream& in, PlayerType& type) {
std::uint32_t val{ 0 };
in >> val;
type = static_cast<PlayerType>(val);
return in;
}
std::ostream& operator<<(std::ostream& out, const PlayerType& type) {
std::string str;
switch (type) {
case PlayerType::HUMAN:
str = "Human";
break;
case PlayerType::DWARVEN:
str = "Dwarven";
break;
case PlayerType::ELVEN:
str = "Elven";
break;
default:
str = "Unknown";
break;
}
return out << str;
}
std::istream& operator>>(std::istream& in, EnemyType& type) {
std::uint32_t val{ 0 };
in >> val;
type = static_cast<EnemyType>(type);
return in;
}
std::ostream& operator<<(std::ostream& out, const EnemyType& type) {
std::string str;
switch (type) {
case EnemyType::GOBLIN:
str = "Goblin";
break;
case EnemyType::ORC:
str = "Orc";
break;
case EnemyType::TROLL:
str = "Troll";
break;
default:
str = "Unknown";
break;
}
return out;
}
CharacterInfo::CharacterInfo() :
level_{ 0 },
health_{ 0 },
attack_power_{ 0 },
defense_{ 0 },
time_response_{ 0.0f },
time_delay_{ 0.0f },
recovery_rate_{ 0.0f }
{}
CharacterInfo::CharacterInfo( std::uint32_t level, float health,
float attackPower, float defense,
float timeResponse, float timeDelay, float recoveryRate) :
level_{level},
health_{health},
attack_power_{attackPower},
defense_{defense},
time_response_{timeResponse},
time_delay_{timeDelay},
recovery_rate_{recoveryRate}
{}
CharacterInfo::CharacterInfo(const CharacterInfo& other) {
this->level_ = other.level_;
this->health_ = other.health_;
this->attack_power_ = other.attack_power_;
this->defense_ = other.defense_;
this->time_response_ = other.time_response_;
this->time_delay_ = other.time_delay_;
this->recovery_rate_ = other.recovery_rate_;
}
CharacterInfo& CharacterInfo::operator=(const CharacterInfo& other) {
this->level_ = other.level_;
this->health_ = other.health_;
this->attack_power_ = other.attack_power_;
this->defense_ = other.defense_;
this->time_response_ = other.time_response_;
this->time_delay_ = other.time_delay_;
this->recovery_rate_ = other.recovery_rate_;
return *this;
}
std::istream& operator>>(std::istream& in, CharacterInfo& info) {
in >> info.level_;
in >> info.health_;
in >> info.attack_power_;
in >> info.defense_;
in >> info.time_response_;
in >> info.time_delay_;
in >> info.recovery_rate_;
return in;
}
std::ostream& operator<<(std::ostream& out, const CharacterInfo& info) {
out << "level:" << info.level_ << '\n';
out.setf(std::ios::floatfield, std::ios::fixed);
out << "health: " << std::setw(3) << std::setfill('0') << info.health_ << '\n'
<< "attack: " << std::setw(3) << std::setfill('0') << info.attack_power_ << '\n'
<< "defense: " << std::setw(3) << std::setfill('0') << info.defense_ << '\n'
<< "response: " << std::setprecision(5) << std::setfill('0') << info.time_response_ << '\n'
<< "delay: " << std::setprecision(5) << std::setfill('0') << info.time_delay_ << '\n'
<< "recover: " << std::setprecision(5) << std::setfill('0') << info.recovery_rate_ << std::endl;
return out;
}
std::ostream& operator<<(std::ostream& out, Player& player) {
out << "ID: " << player.id() << '\n'
<< player.info() << std::endl;
return out;
}
std::ostream& operator<<(std::ostream& out, Orc& orc) {
out << "ID: " << orc.id() << '\n'
<< orc.info() << std::endl;
return out;
}
std::ostream& operator<<(std::ostream& out, Troll& troll) {
out << "ID: " << troll.id() << '\n'
<< troll.info() << std::endl;
return out;
}
If you look at the generated output, you can see that Goblin
, Orc
, Troll
are their own class, but through inheritance, they are all enemy objects and enemy objects are a form of a Character. Even the Player is a Character. However, Character
and Enemy
themselves are abstract types. The CRTP
that is used helps in the aide of generating unique IDs for them in the form of an std::string
. Every time a new Orc
is generated the id value is incremented based on the number of orcs, but this doesn't affect the number id for the Trolls or Goblins, however, we can easily store all of these into a single vector. Using unique_ptr
's helps with cleaning up memory. Now it's just a matter of making sure that your threads and access to read/write within your containers are synchronized and concurrent. Intel has a nice threading library, you can just search for tbb
as it's a part of their IDEs, but they do have an open-source - free version of the tbb
library via apache's
licenses for download. At the time of this post, here is their active link: Intel:TBB. This should be able to help you with your threading and parallel programming concerns.