Cocos Creator事件系统实战:从零构建五子棋游戏

1. 项目概述与核心价值

最近在社区看到不少朋友对Cocos Creator的交互逻辑实现,特别是事件处理这块感到头疼,总觉得概念懂了,一上手就乱。正好,我之前用Cocos Creator 3.x完整实现了一个带简单AI的人机对战五子棋,整个过程对节点事件的应用算是“扒了一层皮”,踩了不少坑,也总结出一些非常实用的套路。这个项目不像那些复杂的RPG或动作游戏,它规则明确、交互聚焦,是绝佳的、用于吃透Cocos Creator事件系统与基础游戏逻辑的练手项目。你不仅能学会如何让棋盘响应点击、如何高亮落子,更能掌握如何构建一个清晰的游戏状态机,并为其注入一个“会思考”的对手——哪怕这个AI最初只是随机落子。

很多人学引擎容易陷入两个极端:要么只盯着官方文档的API看,感觉都会了但无从下手;要么直接克隆一个复杂项目,被各种模块绕晕。这个五子棋项目恰恰卡在中间,它要求你必须理解“节点树”、“事件监听与派发”、“组件间通信”这些核心概念,才能让游戏跑起来。通过它,你会真切地感受到,在Cocos Creator里,一切交互的起点都是“节点”,而“事件”是驱动节点活起来的血液。我们不止要实现功能,更要弄明白为什么这样设计代码结构更清晰、为什么用这种方式监听事件性能更好。接下来,我就把这个项目的实战经验,尤其是“节点事件篇”的精华,毫无保留地拆解给你看。

2. 项目整体设计与核心思路拆解

2.1 为什么选择五子棋作为实战项目?

五子棋规则简单,但实现它所需的技术栈却非常典型,几乎涵盖了2D棋盘类游戏的所有基础要素:场景搭建(棋盘、棋子)、用户输入处理(鼠标点击)、游戏逻辑(胜负判定)、AI逻辑以及状态管理。选择它,意味着你可以集中火力攻克Cocos Creator的核心机制,而不是被美术资源、复杂动画所干扰。我们的核心目标是: 利用Cocos Creator的节点与组件系统,构建一个响应灵敏、逻辑清晰、且具备可扩展AI接口的游戏框架。

这个项目的设计思路遵循“模型-视图-控制器”的简易变体,但更贴合Cocos Creator的“组件化”思想:

  1. 数据模型 (Model) :一个纯JavaScript类(如 GameModel ),负责维护棋盘数据(15x15的二维数组)、当前行棋方、游戏状态(进行中、结束)。它不依赖任何引擎API,只关心数据逻辑。
  2. 视图 (View) :由Cocos Creator场景中的节点树构成。包括背景、棋盘网格线、棋子精灵(Prefab)、当前玩家提示UI等。视图的职责是根据模型的数据进行渲染。
  3. 控制器 (Controller) :由挂载在各个节点上的Component脚本担任。它们监听用户输入(节点事件),调用模型的方法更新数据,再根据模型的变化通知视图更新。 节点事件是连接视图与控制器的桥梁

2.2 技术选型与节点事件的核心地位

在这个项目中,我们几乎会用到Cocos Creator事件系统所有常见的用法:

  • input.on 与节点事件监听 :处理鼠标点击棋盘格子。
  • 事件派发 ( EventTarget ) :用于模型状态改变时,通知多个视图组件更新(例如,棋子落下后,需要更新棋盘渲染和判断胜负)。
  • 触摸事件与UI事件 :虽然五子棋主要用鼠标,但理解触摸事件对于移动端适配至关重要。
  • 自定义事件 :定义如 GameEventType.ON_PIECE_PLACED 这样的事件,实现模块间解耦。

为什么强调节点事件?因为Cocos Creator中,所有交互的捕获点都在节点上。你不能直接去“点击一个数组”,你必须点击代表那个棋盘格子的节点(或一个覆盖在其上的透明按钮)。理解这一点,就理解了Cocos Creator交互开发的入门钥匙。

注意 :在项目初期,务必在 项目设置 -> 功能裁剪 中,确保 Canvas EventSystem 相关模块没有被裁剪掉,否则所有输入事件都会失效。这是一个常见的打包后事件无响应的“坑”。

3. 核心细节解析与实操要点

3.1 棋盘与棋子节点的创建与管理

棋盘通常不直接用15x15个Sprite节点来拼,那样节点数太多(225个),对性能和管理都是挑战。更常见的做法是:

  1. 一个棋盘背景节点 :包含一张棋盘纹理图片。
  2. 一个棋盘交互层节点 :这是一个空节点,我们为其添加一个 BlockInput 组件(一个自定义脚本),用于统一处理点击事件。或者,也可以使用一个全屏大小的 Button 组件,但通常更灵活的是使用 Graphics 组件绘制网格线,并结合事件监听。
  3. 棋子Prefab :创建一个精灵节点作为预制体,包含黑棋和白棋两种状态的Sprite。Prefab是动态生成棋子的蓝图。

关键技巧:坐标转换 这是第一个难点。当你点击屏幕时,获得的是屏幕坐标。你需要将其转换到棋盘交互层节点的本地坐标系中,再根据格子尺寸(如每个格子40像素)计算出对应的行列索引。

// 在 BlockInput 组件的 onLoad 或 start 中监听触摸事件
this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);

private onTouchEnd(event: EventTouch) {
    // 1. 获取触摸点的世界坐标
    const touchPos = event.getUILocation();
    // 2. 将世界坐标转换为当前节点(棋盘交互层)的本地坐标
    const localPos = this.node.getComponent(UITransform)!.convertToNodeSpaceAR(v3(touchPos.x, touchPos.y, 0));
    // 3. 根据本地坐标和格子大小计算行列
    const gridSize = 40;
    const row = Math.floor(localPos.y / gridSize + 7.5); // 假设棋盘中心为(0,0),共15行
    const col = Math.floor(localPos.x / gridSize + 7.5);
    
    // 确保行列在有效范围内
    if (row >= 0 && row < 15 && col >= 0 && col < 15) {
        // 触发落子逻辑
        this.onGridClicked(row, col);
    }
}

这里用到了 UITransform 组件的 convertToNodeSpaceAR 方法,它是处理UI坐标转换的核心。务必理解世界坐标、本地坐标、锚点这些概念,否则点击位置永远对不上。

3.2 游戏模型(GameModel)的设计

模型是游戏的大脑,它应该是“纯净”的,不依赖于Cocos Creator的运行时环境。这样便于单独测试AI算法。

// GameModel.ts
export enum PieceType {
    Empty = 0,
    Black = 1,
    White = 2
}

export enum GameState {
    Playing,
    BlackWin,
    WhiteWin,
    Draw
}

export class GameModel {
    private _board: number[][] = [];
    private _currentPlayer: PieceType = PieceType.Black;
    private _gameState: GameState = GameState.Playing;
    // 事件派发器,用于通知外部状态变更
    public eventTarget: EventTarget = new EventTarget();

    constructor() {
        this.resetBoard();
    }

    public resetBoard() {
        this._board = Array(15).fill(0).map(() => Array(15).fill(PieceType.Empty));
        this._currentPlayer = PieceType.Black;
        this._gameState = GameState.Playing;
        this.eventTarget.emit('onGameReset');
    }

    public placePiece(row: number, col: number): boolean {
        if (this._gameState !== GameState.Playing || this._board[row][col] !== PieceType.Empty) {
            return false;
        }
        this._board[row][col] = this._currentPlayer;
        // 派发棋子落下事件,携带行列和棋子类型信息
        this.eventTarget.emit('onPiecePlaced', {row, col, type: this._currentPlayer});
        
        if (this.checkWin(row, col)) {
            this._gameState = this._currentPlayer === PieceType.Black ? GameState.BlackWin : GameState.WhiteWin;
            this.eventTarget.emit('onGameOver', { winner: this._currentPlayer });
        } else if (this.isBoardFull()) {
            this._gameState = GameState.Draw;
            this.eventTarget.emit('onGameOver', { winner: PieceType.Empty });
        } else {
            this.switchPlayer();
        }
        return true;
    }

    private checkWin(row: number, col: number): boolean {
        const directions = [
            [1, 0],  // 水平
            [0, 1],  // 垂直
            [1, 1],  // 右下
            [1, -1] // 右上
        ];
        const type = this._board[row][col];
        for (const [dx, dy] of directions) {
            let count = 1;
            // 正向检查
            for (let i = 1; i < 5; i++) {
                const newRow = row + dx * i;
                const newCol = col + dy * i;
                if (this._board[newRow]?.[newCol] === type) count++; else break;
            }
            // 反向检查
            for (let i = 1; i < 5; i++) {
                const newRow = row - dx * i;
                const newCol = col - dy * i;
                if (this._board[newRow]?.[newCol] === type) count++; else break;
            }
            if (count >= 5) return true;
        }
        return false;
    }
    // ... 其他方法,如 switchPlayer, isBoardFull, getBoard 等
}

模型通过 EventTarget 派发事件,这是实现与视图解耦的关键。控制器(组件)监听这些事件,然后去更新节点状态,而不是由模型直接操作节点。

3.3 节点事件的监听与派发实战

在Cocos Creator中,事件处理主要有三种方式,在这个项目里都会用到:

1. 内置事件监听(如触摸、鼠标) 如上文所述,在 BlockInput 组件中,使用 this.node.on(Node.EventType.TOUCH_END, ...) 来监听棋盘点击。

2. 自定义事件监听(监听模型事件) 在负责渲染棋盘的 BoardManager 组件中,需要监听模型的事件来更新视图。

// BoardManager.ts
import { GameModel } from './GameModel';

export class BoardManager extends Component {
    @property(Prefab)
    public piecePrefab: Prefab = null!;

    private _pieceMap: Node[][] = []; // 存储已创建的棋子节点
    private _model: GameModel = null!;

    start() {
        this._model = new GameModel();
        this.initBoardView();
        // 监听模型的自定义事件
        this._model.eventTarget.on('onPiecePlaced', this.onPiecePlaced, this);
        this._model.eventTarget.on('onGameReset', this.onGameReset, this);
    }

    private onPiecePlaced(data: {row: number, col: number, type: PieceType}) {
        // 在指定位置实例化一个棋子Prefab
        const pieceNode = instantiate(this.piecePrefab);
        this.node.addChild(pieceNode);
        // 设置棋子的位置、精灵帧(根据type)
        const spriteComp = pieceNode.getComponent(Sprite);
        spriteComp.spriteFrame = data.type === PieceType.Black ? this.blackSpriteFrame : this.whiteSpriteFrame;
        // 计算世界坐标并设置
        const worldPos = this.convertGridToWorld(data.row, data.col);
        pieceNode.setPosition(worldPos);
        
        // 存入映射,方便管理(如重置游戏时销毁)
        if (!this._pieceMap[data.row]) this._pieceMap[data.row] = [];
        this._pieceMap[data.row][data.col] = pieceNode;
    }

    private onGameReset() {
        // 销毁所有棋子节点
        for (let row of this._pieceMap) {
            if (row) {
                for (let pieceNode of row) {
                    if (pieceNode && pieceNode.isValid) {
                        pieceNode.destroy();
                    }
                }
            }
        }
        this._pieceMap = [];
    }
}

3. 使用全局事件系统(可选,用于更松散的耦合) 对于一些跨场景、跨组件的通知,比如更新UI顶部的状态文字,可以使用Cocos Creator内置的 director.getScene().emit 或自己维护一个全局事件管理器。但在本例中,通过模型的事件派发器已经足够。

重要心得 :一定要在组件的 onDestroy 或合适的生命周期里 取消事件监听 ,否则会导致内存泄漏和报错(尝试调用已销毁组件的方法)。

protected onDestroy(): void {
    if (this._model) {
        this._model.eventTarget.off('onPiecePlaced', this.onPiecePlaced, this);
        this._model.eventTarget.off('onGameReset', this.onGameReset, this);
    }
    // 同时取消节点自身的事件监听
    this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
}

这是新手极易忽略的一点,尤其是在反复切换场景时,问题会集中爆发。

4. 实操过程与核心环节实现

4.1 场景搭建与节点结构

  1. 创建场景 :新建一个 GameScene
  2. 构建节点树
    • Canvas
      • Background (Sprite) - 放置棋盘背景图。
      • GameBoard (Node) - 棋盘逻辑节点。
        • GridGraphics (Graphics) - 用于绘制棋盘网格线(可选,也可以用图片)。
        • BlockInput (Node + BlockInput 组件) - 一个大小覆盖整个棋盘的透明节点,专门处理点击。
      • PiecesLayer (Node) - 一个空节点,作为所有动态生成的棋子节点的父节点,便于统一管理。
      • UI (Node) - UI层。
        • StatusLabel (Label) - 显示“黑方回合”、“白方胜利”等状态。
        • RestartBtn (Button) - 重新开始按钮。

这个结构清晰地将背景、交互层、动态元素、UI分层,符合Cocos Creator的渲染和事件处理顺序。

4.2 连接交互、模型与视图

现在,我们需要把 BlockInput GameModel BoardManager UIManager 串联起来。

BlockInput 组件(控制器的一部分)

// BlockInput.ts
import { _decorator, Component, Node, EventTouch, input, Input, UITransform, v3 } from 'cc';
import { GameModel } from './GameModel';

const { ccclass, property } = _decorator;

@ccclass('BlockInput')
export class BlockInput extends Component {
    // 通过属性绑定传入模型实例,或者通过全局访问。这里假设BoardManager创建了模型并传递过来。
    private _model: GameModel = null!;

    // 通常由BoardManager在start时设置
    public setModel(model: GameModel) {
        this._model = model;
    }

    start() {
        // 监听触摸结束事件
        this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
    }

    private onTouchEnd(event: EventTouch) {
        if (!this._model) return;
        // 坐标转换逻辑(同上文)
        const touchPos = event.getUILocation();
        const uiTrans = this.node.getComponent(UITransform);
        const localPos = uiTrans!.convertToNodeSpaceAR(v3(touchPos.x, touchPos.y, 0));
        const gridSize = 40;
        const row = Math.floor(localPos.y / gridSize + 7.5);
        const col = Math.floor(localPos.x / gridSize + 7.5);
        
        if (row >= 0 && row < 15 && col >= 0 && col < 15) {
            // 调用模型的落子方法
            const success = this._model.placePiece(row, col);
            if (!success) {
                console.log('落子无效:位置已有棋子或游戏已结束');
            }
        }
    }

    onDestroy() {
        this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
    }
}

BoardManager 组件(视图管理器) : 它创建模型,并将模型传递给 BlockInput ,同时监听模型事件来更新棋子视图。

// BoardManager.ts (补充)
start() {
    this._model = new GameModel();
    this.initBoardView();
    
    // 将模型传递给交互控制器
    const blockInputComp = this.node.parent.getChildByName('BlockInput')?.getComponent(BlockInput);
    if (blockInputComp) {
        blockInputComp.setModel(this._model);
    }
    
    // 监听模型事件
    this._model.eventTarget.on('onPiecePlaced', this.onPiecePlaced, this);
    this._model.eventTarget.on('onGameReset', this.onGameReset, this);
    this._model.eventTarget.on('onGameOver', this.onGameOver, this);
}

UIManager 组件 : 它同样监听模型的事件,来更新界面上的状态文字。

// UIManager.ts
export class UIManager extends Component {
    @property(Label)
    public statusLabel: Label = null!;

    private _model: GameModel = null!;

    start() {
        // 如何获取model?可以从BoardManager传来,或者设计成单例/全局访问。这里假设通过依赖注入。
        // this._model = ... 
        this._model.eventTarget.on('onPiecePlaced', this.updateStatus, this);
        this._model.eventTarget.on('onGameOver', this.onGameOver, this);
        this.updateStatus();
    }

    private updateStatus() {
        if (this._model.gameState === GameState.Playing) {
            this.statusLabel.string = this._model.currentPlayer === PieceType.Black ? '黑方回合' : '白方回合';
        }
    }

    private onGameOver(data: {winner: PieceType}) {
        if (data.winner === PieceType.Black) {
            this.statusLabel.string = '游戏结束,黑方胜利!';
        } else if (data.winner === PieceType.White) {
            this.statusLabel.string = '游戏结束,白方胜利!';
        } else {
            this.statusLabel.string = '游戏结束,平局!';
        }
    }
}

至此,一个完整的数据流闭环就形成了: 用户点击 -> BlockInput 捕获并转换坐标 -> 调用 GameModel.placePiece -> 模型更新数据并派发事件 -> BoardManager UIManager 监听事件并更新视图 。整个过程清晰解耦。

4.3 实现简单AI(随机落子)

人机对战,电脑对手的AI是灵魂。我们从最简单的开始:随机选择一个空位落子。

  1. GameModel 中提供一个方法,获取所有空位。
    public getEmptyPositions(): [number, number][] {
        const empties: [number, number][] = [];
        for (let i = 0; i < 15; i++) {
            for (let j = 0; j < 15; j++) {
                if (this._board[i][j] === PieceType.Empty) {
                    empties.push([i, j]);
                }
            }
        }
        return empties;
    }
    
  2. 创建一个 AIController 组件,它同样监听游戏状态。当轮到AI(例如白方)时,延迟一段时间后执行落子。
    // AIController.ts
    export class AIController extends Component {
        private _model: GameModel = null!;
        private _aiPlayer: PieceType = PieceType.White; // AI执白
        private _isAITurn: boolean = false;
    
        start() {
            // 获取model
            this._model.eventTarget.on('onPiecePlaced', this.onPlayerMove, this);
            this._model.eventTarget.on('onGameReset', this.onGameReset, this);
        }
    
        private onPlayerMove() {
            if (this._model.gameState !== GameState.Playing) return;
            // 检查当前回合是否是AI
            if (this._model.currentPlayer === this._aiPlayer) {
                this._isAITurn = true;
                // 延迟一段时间,模拟思考,避免立即响应显得不自然
                this.scheduleOnce(this.makeAIMove, 0.5);
            }
        }
    
        private makeAIMove() {
            if (!this._isAITurn || this._model.gameState !== GameState.Playing) return;
            const empties = this._model.getEmptyPositions();
            if (empties.length > 0) {
                const randomIndex = Math.floor(Math.random() * empties.length);
                const [row, col] = empties[randomIndex];
                this._model.placePiece(row, col);
            }
            this._isAITurn = false;
        }
    
        private onGameReset() {
            this._isAITurn = false;
            this.unschedule(this.makeAIMove);
        }
    }
    

这个AI虽然笨,但已经完成了“自动对战”的闭环。你可以在此基础上,轻松替换 makeAIMove 方法,实现更复杂的算法(如贪心算法、极小化极大算法等)。

5. 常见问题与排查技巧实录

在开发过程中,我遇到了不少典型问题,这里记录下排查思路和解决方案。

5.1 事件相关典型问题

问题1:点击事件完全没有反应。

  • 检查点1:节点是否可交互? 确保接收点击的节点(如 BlockInput )的 enabled 属性为 true ,并且其 UITransform 组件的 ContentSize 覆盖了期望的点击区域。一个快速测试方法是给该节点临时加一个带颜色的 Sprite 组件,看看它是否可见且大小位置正确。
  • 检查点2:是否被其他节点遮挡? Cocos Creator中,节点树靠下的节点(在层级管理器中显示在下面)会渲染在上层,并可能拦截触摸事件。确保你的交互层节点在渲染顺序上位于可被点击的位置。可以尝试暂时禁用可能遮挡的节点。
  • 检查点3:事件监听注册成功了吗? start onEnable 方法中添加 console.log ,确认监听代码被执行了。确保 this 指向正确,特别是在使用箭头函数或需要绑定时。
  • 检查点4:项目设置中的事件系统 :如前所述,检查 项目设置 -> 功能裁剪 ,确保没有误删事件模块。

问题2:点击位置坐标计算不准,落子总是偏移。

  • 根源:坐标空间转换错误。 这是最高频的问题。
    • 再次确认你使用的转换方法: convertToNodeSpaceAR (基于锚点)和 convertToNodeSpace (基于左下角)区别很大,通常使用 AR 版本更直观。
    • 打印出每一步的坐标: event.getUILocation() , localPos.x , localPos.y ,以及计算后的 row , col 。对照你棋盘节点的实际位置和锚点(通常是(0.5, 0.5))进行检查。
    • 关键技巧 :在 BlockInput 节点下创建一个子节点 DebugPos ,在 onTouchEnd 中将其位置设置为 localPos ,你可以直观地看到点击的本地坐标原点在哪里。
    • 检查网格大小( gridSize )和棋盘中心偏移量的计算是否正确。如果棋盘节点中心不在世界原点,计算行列时需要加上偏移。

问题3:重复监听导致事件触发多次。

  • 现象 :点击一次,逻辑执行了两次或更多。
  • 原因 :事件监听器被重复注册了。常见于:
    • 脚本组件被多次 addComponent
    • onEnable 中注册监听,但在 onDisable 中没有取消,导致节点重复激活时监听器叠加。
    • 全局事件在多个地方被监听,且没有正确管理生命周期。
  • 解决 :严格遵守“在哪监听,在哪取消”的原则。在 onDestroy onDisable 中,使用 off 方法取消监听,并且确保 on off 的回调函数是同一个引用(使用类方法并绑定 this 是最稳妥的方式)。

5.2 游戏逻辑与性能问题

问题4:AI落子或玩家快速点击导致状态错乱。

  • 现象 :游戏状态(如 _isAITurn )判断失效,可能连续落子或AI在玩家回合行动。
  • 原因 :异步操作和事件触发顺序问题。 scheduleOnce 是异步的,如果在延迟期间玩家又点击了,或者游戏被重置,状态可能被污染。
  • 解决
    1. 加锁 :在 AIController 中设置一个 _isProcessing 标志,在开始AI思考和落子期间设为 true ,阻止新的AI决策。
    2. 及时清理 :在 onGameReset 中,不仅重置标志,还要用 this.unscheduleAllCallbacks() 取消所有计划任务。
    3. 模型层校验 :在 GameModel.placePiece 方法入口处进行严格的状态和输入校验(如游戏是否已结束、位置是否为空),这是最后一道防线。

问题5:动态生成的棋子节点越来越多,可能导致内存泄漏或性能下降。

  • 解决
    • 对象池 :对于频繁创建和销毁的节点(如棋子、特效),一定要使用对象池( cc.NodePool )。在我们的例子中,虽然一局游戏最多225个棋子,但使用对象池是良好的习惯。
    • 及时销毁 :在游戏重置 onGameReset 时,遍历 _pieceMap ,手动调用 pieceNode.destroy() 。确保在节点销毁前,从地图中移除引用。
    • 避免在 update 中创建节点 :创建和销毁节点的操作尽量放在事件回调中,而不是每帧执行的 update 里。

5.3 调试与优化技巧

  1. 善用Cocos Creator调试器 :在浏览器中运行游戏,打开开发者工具。在 Sources 面板中可以给你的TypeScript源码打断点。在 Console 中多打印关键变量(坐标、状态、行列索引)。
  2. 可视化调试 :如前所述,创建调试节点来显示坐标。还可以在棋盘格子上动态创建编号Label,方便快速定位行列。
  3. 性能预览 :对于简单的五子棋,性能通常不是问题。但可以打开Chrome的 Performance 面板录制一段时间,查看是否有不必要的重绘或频繁的垃圾回收。确保事件监听器在场景切换时被正确清理。
  4. 代码结构反思 :随着AI逻辑变复杂, GameModel 可能会膨胀。考虑将胜负判断 checkWin 、AI算法单独抽离成工具类或独立的类,保持模型简洁。 AIController 也可以进一步抽象,定义 IAIStrategy 接口,方便切换不同难度的AI。

通过这个项目,你将深刻体会到,在Cocos Creator中开发游戏, “节点”是骨骼,“组件”是肌肉,而“事件”是神经系统 。处理好它们之间的关系,就能构建出响应迅速、结构清晰的游戏。从随机AI开始,试着为它加入一些简单的规则(比如优先下在对方棋子旁边),你会立刻感受到游戏性提升的乐趣。这只是一个起点,基于这个框架,你可以尝试加入悔棋、存盘读盘、甚至网络对战功能,每一步都是对Cocos Creator更深层次的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值