I'm attempting to make a game in C++ where pieces will move around a gameboard.
In our team, we're trying to be good and use as much modern methodology as we can, given we've gotten rusty and now have a compiler that can use C++17 - previously we were limited for cross-compatibility, library, and legacy reasons to a mix of C++03 and C++11, without a lot of the good stuff C++11 brought with it, including smart pointers. But this rustiness is causing us to hit some issues with polymorphism. We are looking towards C++20, but we're not there yet, so while solutions involving it are okay as a future extension, we really need a C++17 method.
By "move around a gameboard", I mean that cards move into different places in play, or into a deck/discard, or tokens put onto those cards while in play. We're modelling the gameboard in memory using a selection of containers representing the different places in the game, such as the decks, "in play", a market, etc. Different zones house different cards with differing sets of capabilities, some shared with other types, others not.
Consider the following (cut back) class diagram:
GameInstance
- PlayerAvatarInstance
- CardInstance
- MonsterCardInstance
- SpellCardInstance
- TokenInstance
AbilityHealthInterface (pure virtual)
- PlayerAvatarInstance
- MonsterCardInstance
We're currently using std::unique_ptr to represent each instance of GameInstance, putting them in different collections to represent different areas in the gameboard model, using std::move to move them from one list to another.
The Gameboard has the following areas for each player:
std::vector<std::unique_ptr<CardInstance>>std::vector<std::unique_ptr<CardInstance>>std::array<std::unique_ptr<CardInstance>,5>std::unique_ptr<PlayerAvatarInstance>std::array<std::unique_ptr<MonsterCardInstance>,5>std::vector<std::unique_ptr<SpellCardInstance>>This use of std::unique_ptr helps the model integrity too. The area on the gameboard the item is in owns that item. The idea is to enforce that if a card moves from one collection representing an area of the gameboard to another, it needs to use std::move, to make sure that duplication of an instance in the model is hard/impossible and thus preventing bugs/mistakes. If we were using std::shared_ptr or raw pointers they could easily be copied into the destination without being removed from the source.
On game start, we construct the decks, creating each card as their relevant type, calling emplace_back and feeding in a call to std::make_unique<MonsterCardInstance>() or std::make_unique<SpellCardInstance>() as appropriate. The market is filled from the deck for the player to buy. No issues here, the deck and market are the same CardInstance type.
However, issues have arisen on the part I'm trying to code, which happens to be our first attempt to use the GameInstance objects polymorphically, or try to deduce their type. We're all a bit confused it's not allowing use to use it as we'd expect.
The game action I'm trying to code is as follows:
When a player buys a card from the market, it then needs to be moved to the relevant place: the field if it's a monster, or the active area if it's a spell.
To achieve this, I want to cast from a std::unique_ptr<CardInstance> to a std::unique_ptr<MonsterCardInstance> like I would if I were using raw pointers and dynamic_cast. So I can determine the constructed type, potentially leaning on some method of detecting a bad cast (like I'd get nullptr from then move the instance into the collection it needs to move to in order to represent the spot on the game board. The issue is, I can't find an std::unique_ptr equivalent of std::dynamic_pointer_cast which I found existed for std::shared_ptr
bool BuyCard(size_t idx)
{
//find a free spot in the field and attempt to move the card there if it's a monster
auto newPos = std::find_if(Field.begin(), Field.end(), [&mktref, &index, iTargets](std::unique_ptr<MonsterCardInstance>& fldref ) {
if (fldref == nullptr)
{
//unique_pointer_cast is some equivalent for dynamic_cast in this instance
fldref = unique_pointer_cast<MonsterCardInstance>(std::move(mktref));
return fldref == nullptr; //assume unique_pointer_cast would return null if cast impossible
}
index++;
return false;
});
if (newpos != Field.end())
{
return true; //buy monster worked
}
else
{
//wasn't a monster, put the card to the back of the active cards as we can assume it was a spell
Active.emplace_back(unique_pointer_cast<SpellCardInstance>(std::move(mktref));
if (Active.back() != nullptr) //lets hope the buy spell worked
{
//now it's been cast, I can access SpellCardInstance methods
Active.back()->ActivateSpell();
return true;
}
}
return false;
}
So how do I cast-and-move a std::unique_ptr instance from one place where the object is a std::unique_ptr<Base>, to std::unique_ptr<Derived>? What about the reverse, a down-casting from std::unique_ptr<Derived> to std::unique_ptr<Base>? Assume that in this case I'd already know the cast would work.
Furthermore, how do I detect the declared-as type when they're encapsulated in std::unique_ptr so I can act on them differently? This includes both illustrated use-cases: ascertaining type for a cast-and-move, and accessing member functions a derived class may have, but the base doesn't.
Similar issues arise if I try to have a function make use of the functionality that different areas share through a common parent, specifically as it pertains to use as a parameter passed as a reference. It's important to note I don't want to move the cards when doing this, but potentially perform the same operation on them, so this could also be a lambda within the same function being enacted over different areas.
MonsterCardInstance and PlayerAvatarInstance share functionality through AbilityHealthInstance. If I was using raw pointers, I'd pass either object into a parameter that would accept an AbilityHealthInstance pointer. I can't seem do that when I'm looking at a std::unique_ptr<MonsterCardInstance> or std::unique_ptr<PlayerAvatarInstance> trying to pass into a std::unique_ptr<AbilityHealthInstance>& parameter.
bool DamageCard(unique_ptr<AbilityHealthInstance>& target, size_t damage)
{
target->TakeDamage(damage)
if (target->isDead())
{
target->onDeath()
}
}
//won't allow the following
DamageCard(PlayerAvatar, 5);
DamageCard(Field[3], 5);
// Or use in a lambda, which is a more common occurrence given I'm trying to use <algorithm> functionality as much as I can
void DamageAll(size_t damage)
{
auto doDamage=[&](std::unique_ptr<AbilityHealthInstance>& target){
target->TakeDamage(damage)
if (target->isDead())
{
target->onDeath()
}
};
std::for_each(Field.begin(), Field.end(), doDamage);
doDamage(PlayerAvatar)
}
//so instead it needs to be written as such, duplicating some code
void DamageAll(size_t damage)
{
auto doDamage=[&](std::unique_ptr<MonsterCardInstance>& target){
target->TakeDamage(damage)
if (target->isDead())
{
target->onDeath();
}
};
std::for_each(Field.begin(), Field.end(), doDamage);
PlayerAvatar->TakeDamage(damage)
if (PlayerAvatar->isDead())
{
PlayerAvatar->onDeath();
}
}
In all these cases, we are aware of std::unique_ptr::Get() potentially being a solution, but even that doesn't help much in the case of using lambdas in the <algorithm> functions. This another point of confusion for us: we'd expect to just use the object we have as if we were using a raw pointer, not needing to get a raw pointer out of it in order to use it fully. This feels like a real enough use-case for polymorphism that we expected std::unique_ptr to support natively.
What is it we're getting wrong with our use of smart pointers here? As I'm assuming these stumbling blocks mean we're either missing a trick or misunderstanding their use-case.
But the compiler won't let us cast from a unique_ptr to a unique_ptr like I would if I were using raw pointers and dynamic_cast
Most likely you don't need to transfer ownership of the object while you do something with the downcast pointer, so you can simply use raw pointers for that.
Derived* derived = dynamic_cast<Derived*>(base_ptr.get());
Functions that do not transfer ownership should accept raw pointers and not smart pointers as arguments.
If, against all odds, you do need to downcast and transfer ownership at the same time, you can do that with your own function:
template <typename P, typename Q>
std::unique_ptr<P>
cast_and_move_pointer(std::unique_ptr<Q>& src)
{
std::unique_ptr<P> res(dynamic_cast<P*> (src.get()));
if (res)
{
src.release();
}
return res;
};
The semantics of this is not very intuitive though (the source pointer is zeroed out if the cast was successful, and left intact otherwise), which is probably why the standard doesn't have something like that. You also need to decide what to do with the the resulting downcast pointer when you are done with it. You cannot just throw it away because you will lose the pointed-to object. You probably need to return it back to src.
std::unique_ptr<Derived> derived_ptr = cast_and_move_pointer(base_ptr);
if (derived_ptr) {
derived_ptr->do_something();
base_ptr = std::move(derived_ptr);
}
You can wrap this functionality (downcast, do something, assign back to the original) into yet another function.
The best approach though is to never downcast a pointer. Do everything with virtual functions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With