CocosCreator: 实现斗地主滑动选择多张牌

如题,要实现这个功能,首先我们需要先了解下的CocosCreator中的触摸事件传递的机制。

触摸事件冒泡

触摸事件支持节点树的事件冒泡,以下图为例:

在图中的场景里,假设 A 节点拥有一个子节点 B,B 拥有一个子节点 C。开发者对 A、B、C 都监听了触摸事件(以下的举例都默认节点监听了触摸事件)。

当鼠标或手指在 C 节点区域内按下时,事件将首先在 C 节点触发,C 节点监听器接收到事件。接着 C 节点会将事件向其父节点传递这个事件,B 节点的监听器将会接收到事件。同理 B 节点会将事件传递给 A 父节点。这就是最基本的事件冒泡过程。需要强调的是,在触摸事件冒泡的过程中不会有触摸检测,这意味着即使触点不在 A B 节点区域内,A B 节点也会通过触摸事件冒泡的机制接收到这个事件。

触摸事件的冒泡过程与普通事件的冒泡过程并没有区别。所以,调用 event.stopPropagation() 可以主动停止冒泡过程。

同级节点间的触点归属问题

假设上图中 B、C 为同级节点,C 节点部分覆盖在 B 节点之上。这时候如果 C 节点接收到触摸事件后,就宣布了触点归属于 C 节点,这意味着同级节点的 B 就不会再接收到触摸事件了,即使触点同时也在 B 节点内。同级节点间,触点归属于处于顶层的节点。

此时如果 C 节点还存在父节点,则还可以通过事件冒泡的机制传递触摸事件给父节点。

这一点就是我们所需要注意的,由于牌组中的牌都处于同一层,因此不能直接监听每一张牌的触摸事件,这样触摸点归属一张牌之后,其他牌就无法收到事件传递,因此不能监听每张牌的触摸事件,需要一个触摸层,用来接受触摸事件,通过判断触摸点是否落在卡牌上。

原理

如上图,由于每张牌都要按照一个固定位移差排列着,因此检查触摸范围时,只有最后一张卡牌的触摸范围是整张牌范围,其他牌的触摸范围只要检查显示的部分。

不管触摸点起始位置时在触摸终点的左边还是右边,只要判断卡牌的显示部分处于起始点和终点之间就是被选中的状态

实现

1,创建一个卡牌视图类,用来控制卡牌被选中,挂起和复位效果,代码如下:

typescript 复制代码
export default class CardView {    
    ...
    /** 原始位置 */
    private _orginPos: cc.Vec3 = cc.v3();

    /** 是否被选中 */
    private _isSelect: boolean = false;
    public get isSelect (): boolean {
        return this._isSelect;
    }

    public set isSelect (isSelect: boolean) {
        this._isSelect = isSelect;
        this.selectStateImg.active = isSelect;
    }

    /** 是否挂起 */
    private _isUp: boolean = false;
    public isUp (): boolean {
        return this._isUp;
    }
    
    /** 牌之间的间隔 */
    private _interval: number = 0;
    public set interval (interval: number) {
        this._interval = interval;
    }

    public get interval (): number {
        return this._interval;
    }
    
    /** 被选中后,挂起的牌复位,原位的牌挂起 */
    public checkUp () {
        if (this._isUp) {
            this.unselectAction();
        } else {
            this.selectAction();
        }
    }

    /** 被选中后挂起动画 */
    public selectAction () {
        if (this._isUp) {
            return;
        }
        cc.tween(this.node)
            .to(0.1, { position: cc.v3(this.node.x, this._orginPos.y + 20, 0) }, { easing: 'sineIn'})
            .call(() => {
                this._isUp = true;
            })
            .start();
    }

    /** 复位动画 */
    public unselectAction () {
        if (!this._isUp) {
            return;
        }
        cc.tween(this.node)
            .to(0.1, { position: cc.v3(this.node.x, this._orginPos.y, 0) }, { easing: 'sineIn'})
            .call(() => {
                this._isUp = false;
            })
            .start();
    }
}

2, 创建触摸层, 用来监听触摸事件

typescript 复制代码
xport default class CardPanelView extends EventComponent {

    @inject("playerPanel1", cc.Node)
    playerPanel1: cc.Node = null;

    /** 根视图 */
    private _gameView: UIView = null;
    public set gameView (view: UIView) {
        this._gameView = view;
    }

    /** 创建的牌组 */
    private _cardViews: cc.Node[] = [];

    /** 触摸点起始位置 */
    private _startPos: cc.Vec2 = null;
    /** 触摸到牌外之前的坐标 */
    private _moveEndPos: cc.Vec2 = null;

    /** 触摸起始位置是否触摸到牌 */
    private _isTouchedInCard: boolean = false;

    onLoad(): void {
        super.onLoad();
    }

    /** 监听触摸事件 */
    addEvents(): void {
        this.onN(this.node, cc.Node.EventType.TOUCH_START, this._touchStart);
        this.onN(this.node, cc.Node.EventType.TOUCH_MOVE, this._touchMove);
        this.onN(this.node, cc.Node.EventType.TOUCH_END, this._touchEnd);
    }

    init() {
        this.initCards();
    }

    /** 初始化牌组 */
    initCards () {
        const cards = [0x1A, 0x1A, 0x1B, 0x1B, 0x1E, 0x1E, 0x1E, 0x1F, 0x1F, 0x01, 0x02];
        createPrefab({
            url: DDZ_PREFAB_URL[0],
            view: this._gameView,
            complete: (node) => {
                for (let i = 0; i < cards.length; i++) {
                    const node1 = cc.instantiate(node);
                    this.node.addChild(node1);
                    node1.x = setCardPositionX(cards.length-1, i, node1.width, 60);
                    node1.y = this.playerPanel1.y;
                    const cardView1 = node1.addComponent(CardView); 
                    cardView1.gameView = this._gameView;
                    cardView1.interval = 60;
                    cardView1.value = cards[i];

                    this._cardViews.push(node1);
                }
            }
        });
    }

    private _touchStart (ev : cc.Event.EventTouch) {
        this._startPos = ev.getLocation();
        this._moveEndPos = this._startPos;
        this._isTouchedInCard = this._isTouchedCard(this._startPos);
    }

    private _touchMove (ev : cc.Event.EventTouch) {
        /** 起始触摸点在牌组范围外,不进行触摸检测 */
        if (!this._isTouchedInCard) return;

        /** 触摸点移动到牌组范围外,就不继续进行新的碰撞检测 */
        const currentPos = ev.getLocation();
        const isCurTouchedCard = this._isTouchedCard(currentPos);
        if (!isCurTouchedCard) {
            return;
        }

        this._moveEndPos = currentPos;


        /** 碰撞检测,检测到牌显示部分在起始和终点范围之间,则为选中状态,否则为选中 */
        this._cardViews.forEach((card, index, arr) => {
            const cardRect = card.getBoundingBoxToWorld();
            const cardView = card.getComponent(CardView);
            cardRect.width = index === arr.length - 1 ? cardRect.width : cardView.interval;
            if (this._isCardInSelectionRange(cardRect, this._startPos, this._moveEndPos)) {
                cardView.isSelect = true;
            } else {
                cardView.isSelect = false;
            }
        });
    }

    private _touchEnd (ev : cc.Event.EventTouch) {
        const currentPos = ev.getLocation();

        // 选中牌意外的区域,所有牌复位
        const _isEndTouchedInCard = this._isTouchedCard(currentPos);
        if (!this._isTouchedInCard && !_isEndTouchedInCard) {
            this._cardViews.forEach((card, index, arr) => {
                const cardView = card.getComponent(CardView);
                if (cardView.isSelect)
                    cardView.isSelect = false;
                if (cardView.isUp) {
                    cardView.unselectAction();
                }
            });
            return;
        }
        if (!this._isTouchedInCard) return;

        // 选中牌区域,挂起的牌复位,原始位置的牌挂起
        this._cardViews.forEach((card, index, arr) => {
            const cardRect = card.getBoundingBoxToWorld();
            const cardView = card.getComponent(CardView);
            cardRect.width = index === arr.length - 1 ? cardRect.width : cardView.interval;
            
            if (this._isCardInSelectionRange(cardRect, this._startPos, this._moveEndPos)) {
                cardView.checkUp();
            }

            if (cardView.isSelect)
                cardView.isSelect = false;
        });
    }

    /** 判断牌显示区域是否在两个点范围内 */
    private _isCardInSelectionRange(cardRect: cc.Rect, startPos: cc.Vec2, currentPos: cc.Vec2): boolean {
        const minX = Math.min(startPos.x, currentPos.x);
        const maxX = Math.max(startPos.x, currentPos.x);
        const minY = Math.min(startPos.y, currentPos.y);
        const maxY = Math.max(startPos.y, currentPos.y);

        if (cardRect.x + cardRect.width < minX || cardRect.x > maxX || cardRect.y + cardRect.height < minY || cardRect.y > maxY) {
            return false;
        }

        return true;
    }

    /** 判断是否在牌组内 */
    private _isTouchedCard (targetPos: cc.Vec2) {
        let isTouchedCard = false;
        this._cardViews.forEach((card, index, arr) => {
            const cardRect = card.getBoundingBoxToWorld();
            const cardView = card.getComponent(CardView);
            cardRect.width = index === arr.length - 1 ? cardRect.width : cardView.interval;
            
            if (this._isCardInSelectionRange(cardRect, targetPos, targetPos)) {
                isTouchedCard = true;
            }
        });
        return isTouchedCard;
    }
}

代码就不多解释了,看注释。 这样就基本可以实现多选卡牌的功能,效果如下:

相关推荐
烛阴16 天前
用 MCP 调教 AI 代理:让 Cocos Creator 3.8.8 核心逻辑一键全自动生成
typescript·cocos creator
烛阴18 天前
Cocos Creator 3.x 装饰器实战:让你的代码优雅 10 倍
typescript·cocos creator
winlife_19 天前
把 Cocos Creator 编辑器接入 AI:Funplay MCP for Cocos 介绍
人工智能·编辑器·ai编程·cocos creator·游戏开发·claude·mcp
LcGero2 个月前
TypeScript 快速上手:泛型与工具类型
typescript·cocos creator·游戏开发
LcGero2 个月前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机
LcGero2 个月前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲
LcGero2 个月前
Cocos Creator平台适配层框架设计
cocos creator·平台·框架设计
LcGero2 个月前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
LcGero2 个月前
TypeScript 快速上手:前言
typescript·cocos creator·游戏开发
Setsuna_F_Seiei2 个月前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发