最近,我在一个系统的项目上做了很多工作,这个系统代表了一个游戏中的库存和物品使用情况,其中一个重要的特性就是它应该在多个项目中被重用。我想我做得很好,尽可能少地保持所需的界面。我已经到了一个阶段,我认为这个项目已经完成了,但是我想得到关于我仍然可以改进代码的反馈。它真的很长,所以我不确定我是否应该把所有的东西都贴在这里。我将在这里张贴主目录头,其余的代码可以在github上找到。如果我需要复制这里的所有代码,请告诉我。
Inventory.h
#pragma once
#include "IItem.h"
#include "ItemDispatcher.h"
#include
#include
#include
#include
#include
#include
#include
template
class Inventory
{
private:
class Traits //class to simulate namespace inside class
{
public:
/*HasUseMethod type trait*/
template>
struct HasUseMethodHelper : std::false_type
{
};
template
struct HasUseMethodHelper<_Ty, std::void_t().use(std::declval&>()))>> : std::true_type
{
};
template
struct HasUseMethodT : HasUseMethodHelper<_Ty>::type
{
};
template using HasUseMethod = typename HasUseMethodT<_Ty>::type;
template static constexpr bool HasUseMethodV = HasUseMethod<_Ty>::value;
/*HasEquippabmeMethod type trait*/
template>
struct HasEquippableMethodHelper : std::false_type
{
};
template
struct HasEquippableMethodHelper<_Ty,
std::void_t().equippable()), bool >)>> : std::true_type
{
};
template
struct HasEquippableMethodT : HasEquippableMethodHelper<_Ty>::type
{
};
template using HasEquippableMethod = typename HasEquippableMethodT<_Ty>::type;
template static constexpr bool HasEquippableMethodV = HasEquippableMethod<_Ty>::value;
/*HasIsEquipped type trait*/
template>
struct HasIsEquippedMethodHelper : std::false_type
{
};
template
struct HasIsEquippedMethodHelper<_Ty,
std::void_t().is_equipped()), bool >)>> : std::true_type
{
};
template
struct HasIsEquippedMethodT : HasIsEquippedMethodHelper<_Ty>::type
{
};
template using HasIsEquippedMethod = typename HasIsEquippedMethodT<_Ty>::type;
template static constexpr bool HasIsEquippedMethodV = HasIsEquippedMethod<_Ty>::value;
/*HasSetEquip type trait*/
template>
struct HasSetEquipMethodHelper : std::false_type
{
};
template
struct HasSetEquipMethodHelper<_Ty,
std::void_t().set_equip(std::declval())), void >)>>
: std::true_type
{
};
template
struct HasSetEquipMethodT : HasSetEquipMethodHelper<_Ty>::type
{
};
template using HasSetEquipMethod = typename HasSetEquipMethodT<_Ty>::type;
template static constexpr bool HasSetEquipMethodV = HasSetEquipMethod<_Ty>::value;
/*HasUnequipMethod type trait*/
template>
struct HasUnequipMethodHelper : std::false_type
{
};
template
struct HasUnequipMethodHelper<_Ty,
std::void_t().unequip(std::declval())), void >)>>
: std::true_type
{
};
template
struct HasUnequipMethodT : HasUnequipMethodHelper<_Ty>::type
{
};
template using HasUnequipMethod = typename HasUnequipMethodT<_Ty>::type;
template static constexpr bool HasUnequipMethodV = HasUnequipMethod<_Ty>::value;
/*HasReusableMethod type trait*/
template>
struct HasReusableMethodHelper : std::false_type
{
};
template
struct HasReusableMethodHelper<_Ty,
std::void_t().reusable()), bool >)>>
: std::true_type
{
};
template
struct HasReusableMethodT : HasReusableMethodHelper<_Ty>::type
{
};
template using HasReusableMethod = typename HasReusableMethodT<_Ty>::type;
template static constexpr bool HasReusableMethodV = HasReusableMethod<_Ty>::value;
template>
struct HasStackableMethodHelper : std::false_type
{
};
template
struct HasStackableMethodHelper<_Ty,
std::void_t().stackable()), bool>)>>
: std::true_type
{
};
template
struct HasStackableMethodT : HasStackableMethodHelper<_Ty>::type
{
};
template using HasStackableMethod = typename HasStackableMethodT<_Ty>::type;
template static constexpr bool HasStackableMethodV = HasStackableMethod<_Ty>::value;
template
struct IsValidItemT
{
static constexpr bool value =
HasEquippableMethodV<_Ty>
&& HasUseMethodV<_Ty>
&& HasIsEquippedMethodV<_Ty>
&& HasSetEquipMethodV<_Ty>
&& HasEquippableMethodV<_Ty>
&& HasReusableMethodV<_Ty>
&& HasStackableMethodV<_Ty>;
};
template using IsValidItem = typename IsValidItemT<_Ty>::type;
template static constexpr bool IsValidItemV = IsValidItemT<_Ty>::value;
};
public:
static_assert(Traits::IsValidItemV, "Item type is invalid. It should provide methods listed in documentation");
class Exception
{
private:
std::string msg;
public:
explicit inline Exception(std::string_view error) : msg {error} {}
inline std::string_view what() { return msg; }
};
using game_object_type = GameObjTy;
using item_type = ItemTy;
using item_pointer = std::unique_ptr;
using game_object_pointer = game_object_type*;
using inventory_type = std::unordered_map>;
using iterator = typename inventory_type::iterator;
using const_iterator = typename inventory_type::const_iterator;
using size_type = typename inventory_type::size_type;
explicit Inventory(game_object_pointer owner);
Inventory(Inventory const& other) = delete;
Inventory(Inventory&& other);
Inventory& operator=(Inventory const& other) = delete;
Inventory& operator=(Inventory&& other);
inventory_type const& contents() const;
inventory_type& contents();
/*Adds a new item, stacked on top of an item with the same ID. The id parameter will be used to access the item*/
template
void addItem(std::string_view id);
/*constructs a new item and adds it to the inventory. The id parameter will be used to access the item*/
template
void emplaceItem(std::string_view id, Args... args);
void useItem(std::string_view name, game_object_pointer target = nullptr);
/*The iterators invalidate when a new Item is added to the inventory*/
iterator getItem(std::string_view name);
/*The iterators invalidate when a new Item is added to the inventory*/
const_iterator getItem(std::string_view name) const;
void removeItem(std::string_view name);
iterator begin();
iterator end();
const_iterator cbegin() const;
const_iterator cend() const;
size_type max_size() const;
size_type size() const;
/*Merges inventory A with this inventory. Leaves other empty, unless this inventory is full, in which case the leftover
elements will be deleted*/ //#TODO: leftover elements are left in old inventory?
template
void merge(Inventory& other);
template
bool merge_fits(Inventory const& other);
/*Transfers item with name parameter into the inventory specified in destination, unless destination does not have enough
*space left*/
template
void transfer(Inventory& destination, std::string_view name);
bool empty() const;
bool full() const;
void setOwner(game_object_pointer owner);
game_object_pointer getOwner() const;
void clear();
unsigned int getItemCount(std::string_view id) const;
private:
size_type m_size = 0;
inventory_type m_items { MAX_SIZE };
game_object_pointer m_owner { nullptr };
//these functions are private so you cannot accidentally pass an invalid iterator to one of these, causing undefined behavior
void useItem(iterator pos, game_object_pointer target = nullptr);
void removeItem(iterator pos);
inline Exception InventoryFullException() const { return Exception {"Inventory is full"}; }
inline Exception InvalidItemTypeException() const { return Exception {"Item type must be derived from Inventory::ItemTy, which defaults to IItem"}; }
inline Exception InvalidItemException() const { return Exception { "Invalid item name" }; }
inline Exception InvalidStackException() const { return Exception {"Tried to stack a non-stackable item"}; }
inline Exception InvalidIDException() const { return Exception {"ID not found in inventory"}; }
};
template
Inventory::Inventory(game_object_pointer owner) : m_owner(owner)
{
}
template
Inventory::Inventory(Inventory&& other) : m_owner(std::move(other.m_owner)), m_items(std::move(other.m_items)), m_size(other.m_size)
{
other.m_owner = nullptr;
other.m_items = inventory_type {};
other.m_size = 0;
}
template
Inventory& Inventory::operator=(Inventory&& other)
{
// #WARNING: Self assignment check is missing
m_owner = other.m_owner;
m_items = std::move(other.m_items);
m_size = other.m_size;
other.m_owner = nullptr;
other.m_items = inventory_type {};
other.m_size = 0;
return *this;
}
template
typename Inventory::inventory_type const& Inventory::contents() const
{
return m_items;
}
template
typename Inventory::inventory_type& Inventory::contents()
{
return m_items;
}
template
typename Inventory::iterator Inventory::begin()
{
return m_items.begin();
}
template
typename Inventory::iterator Inventory::end()
{
return m_items.end();
}
template
typename Inventory::const_iterator Inventory::cbegin() const
{
return m_items.cbegin();
}
template
typename Inventory::const_iterator Inventory::cend() const
{
return m_items.cend();
}
template
typename Inventory::iterator Inventory::getItem(std::string_view name)
{
return m_items.find(name.data());
}
template
typename Inventory::const_iterator Inventory::getItem(std::string_view name) const
{
return m_items.find(name.data());
}
template
void Inventory::useItem(std::string_view name, game_object_pointer target)
{
useItem(getItem(name), target);
}
template
template
void Inventory::addItem(std::string_view id)
{
if constexpr (!std::is_base_of_v)
throw InvalidItemTypeException();
if (size() >= MAX_SIZE)
{
throw InventoryFullException();
}
if (m_items.find(id.data()) != m_items.end()) //if we already own this item, increment the count
{
if (!m_items[id.data()].first->stackable())
throw InvalidStackException();
m_items[id.data()].second += 1; //increment count
m_size += 1;
}
else
{
throw InvalidIDException();
}
}
template
template
void Inventory::emplaceItem(std::string_view id, Args... args)
{
if constexpr (!std::is_base_of_v)
throw InvalidItemTypeException();
if (size() >= MAX_SIZE)
{
throw InventoryFullException();
}
m_items[id.data()] = std::make_pair(std::make_unique(std::forward(args)...), 1);
m_size += 1;
}
template
void Inventory::useItem(iterator pos, game_object_pointer target)
{
if (pos == m_items.end()) throw InvalidItemException();
//use the item
ItemDispatcher dispatcher { target };
auto& it = *pos;
auto& itemPair = it.second;
auto& item = itemPair.first;
if (item->equippable())
{
dispatcher.setTarget(m_owner);
if (!item->is_equipped())
{
item->set_equip(true);
item->use(dispatcher);
}
else
{
item->set_equip(false);
item->use(dispatcher);
}
return;
}
else
{
dispatcher.setTarget(target);
item->use(dispatcher); //dispatcher.target == target, see construction above
}
if (!item->reusable())
{
if (!item->stackable())
removeItem(pos);
else
{
if (itemPair.second > 1) itemPair.second -= 1; //decrement count if we have more than 1
else removeItem(pos);
}
m_size -= 1;
}
}
template
void Inventory::removeItem(iterator pos)
{
m_items.erase(pos);
}
template
void Inventory::removeItem(std::string_view name)
{
removeItem(getItem(name));
}
template
void Inventory::setOwner(game_object_pointer owner)
{
m_owner = owner;
}
template
typename Inventory::game_object_pointer Inventory::getOwner() const
{
return m_owner;
}
template
typename Inventory::size_type Inventory::max_size() const
{
return MAX_SIZE;
}
template
typename Inventory::size_type Inventory::size() const
{
return m_size;
}
template
template
void Inventory::merge(Inventory& other)
{
if (!merge_fits(other))
throw InventoryFullException();
for (auto& it = other.begin(); it != other.end(); std::advance(it, 1))
{
this->m_items[it->first] = std::move(it->second);
}
other.clear();
}
template
void Inventory::clear()
{
m_size = 0;
m_items.clear();
}
template
template
bool Inventory::merge_fits(Inventory const& other)
{
return !(full() || other.size() + this->size() >= max_size());
}
template
template
void Inventory::transfer(Inventory& destination, std::string_view name)
{
if (destination.full())
return;
auto& it = getItem(name);
auto& item = (*it).second;
destination.contents()[name.data()] = std::move(item);
m_items.erase(it);
m_size -= 1;
}
template
bool Inventory::empty() const
{
return size() <= 0;
}
template
bool Inventory::full() const
{
return max_size() <= size();
}
template
unsigned int Inventory::getItemCount(std::string_view id) const
{
if (m_items.find(id.data()) == m_items.end()) throw InvalidItemException();
return m_items.at(id.data()).second;
}我还将添加一个main.cpp测试文件,以演示如何使用它。
main.cpp
#include "DamagePotion.h"
#include "Inventory.h"
#include "HealPotion.h"
#include "Sword.h"
#include "ItemUtil.h"
std::ostream& operator<<(std::ostream& out, ItemID const& id)
{
if (id == ItemID::DAMAGE_POTION)
{
out << "ID_DAMAGE_POTION";
}
else if (id == ItemID::DEFAULT_ITEM) out << "ID_DEFAULT_ITEM";
else if (id == ItemID::HEAL_POTION) out << "ID_HEAL_POTION";
else if (id == ItemID::SWORD) out << "ID_SWORD";
return out;
}
//Replace temp::GameObject class with the GameObject class used by your game
class Player : public temp::GameObject
{
public:
Player() : temp::GameObject(200)
{
try
{
m_inventory.emplaceItem("Damage Potion I", 50);
m_inventory.emplaceItem("Heal Potion I", 70);
m_inventory.emplaceItem("Sword I", 20);
std::cout << "Inventory contents after adding base items:\n";
for (auto const& it : m_inventory.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
m_inventory.useItem("Damage Potion I", this);
m_inventory.useItem("Heal Potion I", this);
m_inventory.useItem("Sword I");
std::cout << "Inventory contents after using base items:\n";
for (auto const& it : m_inventory.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
m_inventory.useItem("Sword I"); // will unequip Sword I
std::cout << "Inventory contents after unequipping Sword I:\n";
for (auto const& it : m_inventory.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
chest.emplaceItem("CDmgPot", 100);
chest.emplaceItem("CHealPot", 200);
std::cout << "Chest contents after adding base items:\n";
for (auto const& it : chest.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
m_inventory.merge(chest);
std::cout << "Chest contents after merging with inventory:\n";
for (auto const& it : chest.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
std::cout << "Inventory contents after merging with chest:\n";
for (auto const& it : m_inventory.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
chest.emplaceItem("CSword", 50);
std::cout << "Chest contents after adding CSword:\n";
for (auto const& it : chest.contents()) std::cout << it.second.first ->id() << "\n";
std::cout << "\n";
chest.transfer(m_inventory, "CSword");
std::cout << "Inventory contents after transferring CSword from chest:\n";
for (auto const& it : m_inventory.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
chest.emplaceItem("CSword", 20);
std::cout << "Chest contents after adding a CSword:\n";
for (auto const& it : chest.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
chest.removeItem("CSword");
std::cout << "Chest contents after removing a CSword:\n";
for (auto const& it :chest.contents()) std::cout << it.second.first->id() << "\n";
std::cout << "\n";
}
catch (std::runtime_error e)
{
std::cerr << e.what();
}
}
private:
Inventory<200> m_inventory { this };
Inventory<5> chest { this };
};
struct InvalidItem
{
};
struct InvalidEquippable
{
void equippable()
{
}
};
class TestClass : public temp::GameObject
{
public:
TestClass() : temp::GameObject(50)
{
try
{
Inventory<100> inv { this };
inv.emplaceItem::type>("P", 20);
inv.addItem("P");
std::cout << ItemName(ItemID::HEAL_POTION) << ' ' << ItemId() << '\n';
std::cout << inv.getItemCount("P") << '\n';
inv.useItem("P", this);
inv.useItem("P", this);
std::cout << getHealth() << '\n';
std::cout << inv.getItemCount("P") << '\n';
}
catch (Inventory<100>::Exception e)
{
std::cout << e.what() << "\n";
}
}
};
int main()
{
Player p;
// IItem* base_ptr;
// Inventory<200> {nullptr};
//Fails to compile due to traits
// Inventory<100, temp::GameObject, InvalidItem> {nullptr};
// Inventory<100, temp::GameObject, InvalidEquippable> {nullptr};
// base_ptr = new DamagePotion(20);
// temp::GameObject target { 100 };
/*
std::cout << "Using Item: \n";
std::cout << "Name:\t" << base_ptr->name() << "\n";
std::cout << "ID:\t" << base_ptr->id() << "\n";
base_ptr->use(&target);
*/
std::cin.get();
}注意:我刚读到一篇元文章,上面说它不允许发布GitHub链接。我不知道在这种情况下我应该做什么,因为我主要是希望库存头审查。github中的文件只适用于真正需要更多信息的时候。
编辑:根据评论中的要求,以下是一些进一步的解释:
架构
当我创建这个项目时,考虑到其他项目的可移植性,因此作为一种库,该体系结构设计得尽可能简单。所有项都将从基类IItem类或任何其他支持库存::特征类中的函数的类继承。库存负责管理每个项目的资源,并创建它们。因此,当添加您调用的项时
inventory.emplaceItem("Potion", 50); //the 50 is passed to SomePotion's constructorMAX_SIZE
MAX_SIZE是一个模板参数,因为在一开始就有一个不吉利的设计选择。因为我不想为项目做大量的分配,所以我想要固定大小。这可能是完全不相关的,但现在令人讨厌的删除。
Item使用率
由于清单负责使用项目(通过调用useItem("name", target_ptr);),所以我需要某种方法。我选择了访问者模式,我用ItemDispatcher类实现了这个模式。
Data结构
这些项目存储在一个std::unordered_map, unsigned>>;中,我将详细分析我为什么选择这样做。首先,我希望能以某种方式获取这些物品。迭代器很笨拙,因为当一个项目被添加到库存中时,迭代器就会失效。我仍然保留了begin()和end()函数,以便在循环中迭代库存。因此,我选择了一个未完成的映射,其中包含了字符串索引,这样您就可以为每个项目提供自己的字符串ID。这些项作为一对存储,因为我希望能够在可能的情况下堆叠项目(Item.stackable() == true)。
可重用性
关于多个项目中的可重用性。我的意思是,如果我,在某一时刻决定制作一个RPG游戏,而角色需要一个清单,我可以使用这个精确的库存类而不作任何修改。如果我以后想要制作另一个完全不同的游戏,它也具有库存,或者其他存储物品的方式,我可以再次使用它。
技术
属性类用作SFINAE类型特征的包装器,用于检测最终的自定义项类型是否支持所有必需的函数,例如:
Inventory<200, MyCoolGameObject, MyCoolItem> inventory;特质是一门课因为我做不到
class X
{
namespace Y
{
};
};发布于 2018-05-28 00:22:58
https://codereview.stackexchange.com/questions/194561
复制相似问题