You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

unique_ptr与shared_ptr设计疑问:卡牌游戏智能指针选型问题

解决卡牌游戏中多模块访问同一张卡牌的设计问题

嘿,这个场景我做卡牌游戏原型的时候刚好踩过坑!核心问题其实是你混淆了所有权和访问权——你一开始用unique_ptr是完全正确的,因为卡牌从Deck到Player的流转都是明确的所有权转移,但现在需要Board和GameLogic同时接触同一张卡,不代表要共享所有权呀!下面给你几个符合智能指针设计理念的解决方案:

方案1:明确单一所有权,用引用/原始指针提供访问权

这是最推荐的方案,完全贴合unique_ptr的设计初衷——让一个模块持有卡牌的唯一所有权,其他模块只通过引用或const原始指针来访问,不持有所有权。

具体来说,卡牌的所有权流转应该是这样的:

  • Deck → Player:用unique_ptr转移所有权,Player持有手牌的vector<unique_ptr<Card>>
  • Player打出卡牌 → Board:Player把unique_ptr转移给Board,Board持有场上卡牌的vector<unique_ptr<Card>>
  • GameLogic不需要持有卡牌,而是通过Board提供的接口来访问场上的卡牌

代码示例:

class Board {
private:
    vector<unique_ptr<Card>> played_cards;
public:
    // 接收玩家打出的卡牌,接管所有权
    void place_card(unique_ptr<Card> card) {
        played_cards.push_back(move(card));
    }

    // 给GameLogic提供只读访问(如果需要修改可以提供非const版本)
    const Card& get_played_card(size_t index) const {
        // 注意要先做边界检查,避免越界
        if (index >= played_cards.size()) {
            throw out_of_range("Invalid card index");
        }
        return *played_cards[index];
    }

    // 当卡牌离场时,销毁所有权
    void remove_card(size_t index) {
        if (index < played_cards.size()) {
            played_cards.erase(played_cards.begin() + index);
        }
    }
};

class GameLogic {
private:
    Board& game_board; // GameLogic持有Board的引用,确保生命周期一致
public:
    GameLogic(Board& board) : game_board(board) {}

    void resolve_card_effect() {
        // 访问场上的第一张卡牌,处理效果
        const Card& current_card = game_board.get_played_card(0);
        if (current_card.get_type() == CardType::ATTACK) {
            // 执行攻击逻辑
        }
    }
};

这个方案的优势:

  • 所有权完全清晰,每个卡牌始终只有一个所有者,避免内存泄漏或悬垂指针
  • GameLogic只负责逻辑处理,不持有任何实体资源,符合单一职责原则
  • 完全不需要shared_ptr,保持代码简洁优雅

方案2:仅在必要时使用shared_ptr(不推荐除非真的需要共享所有权)

如果你的场景中,卡牌确实需要被多个模块同时拥有(比如某些特殊机制:一张卡同时存在于玩家手牌和场上?这种情况很少见),可以用shared_ptr,但一定要明确共享的边界。

比如,当卡牌被打出时,Player保留一个weak_ptr,Board持有shared_ptr,GameLogic通过Board的shared_ptr访问。这样Player可以检查卡牌是否还存在,但不影响所有权的管理:

class Player {
private:
    vector<weak_ptr<Card>> pending_cards; // 仅用于追踪已打出但未结算的卡牌
public:
    void play_card(shared_ptr<Card> card, Board& board) {
        board.place_shared_card(card);
        pending_cards.push_back(card);
    }
};

class Board {
private:
    vector<shared_ptr<Card>> played_cards;
public:
    void place_shared_card(shared_ptr<Card> card) {
        played_cards.push_back(card);
    }
};

但再次强调:这个方案只适合真正需要共享所有权的特殊场景,大部分卡牌游戏的流转逻辑都不需要,用方案1足够。

方案3:分离卡牌的属性与状态(进阶优化)

如果你的卡牌有大量固定属性(比如名称、攻击力、效果描述),而只有少量可变状态(比如是否被激活、剩余耐久),可以把这两部分分离:

  • 用shared_ptr<const CardData>共享不可变的属性(所有同类型卡牌复用同一个CardData)
  • 可变状态存在CardInstance中,由Board持有unique_ptr<CardInstance>

这样既节省内存,又能保持所有权清晰,适合卡牌种类多的大型游戏。

总结

回到你的问题,你觉得用shared_ptr不优雅是对的——因为你的场景根本不需要共享所有权!优先选择方案1,明确Board是打出后卡牌的所有者,GameLogic通过Board访问卡牌,这样既符合智能指针的设计原则,又能让代码结构更清晰。

内容的提问来源于stack exchange,提问作者user2146414

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:15:49