Android实现五子棋游戏(一) 游戏基本逻辑

转载请注明出处:http://blog.csdn.net/a512337862/article/details/74165085

最近,写一个简单的五子棋游戏,效果如下:

这里写图片描述

现在其实还算不上一个真正的游戏,因为只是实现了在同一设备上五子棋最基本的逻辑。下面简单介绍一下逻辑,仅供参考。

思路

1.五子棋游戏的基本逻辑:包括棋盘棋子的绘制,游戏胜利判断,重开游戏。
2.添加背景图片,游戏胜负信息,重开按钮等。

代码分析

FiveChessView

/**
 * Created by ZhangHao on 2017/6/27.
 * 五子棋 View
 */

public class FiveChessView extends View implements View.OnTouchListener {

    //画笔
    private Paint paint;
    //棋子数组
    private int[][] chessArray;
    //当前下棋顺序(默认白棋先下)
    private boolean isWhite = true;
    //游戏是否结束
    private boolean isGameOver = false;

    //bitmap
    private Bitmap whiteChess;
    private Bitmap blackChess;
    //Rect
    private Rect rect;
    //棋盘宽高
    private float len;
    //棋盘格数
    private int GRID_NUMBER = 10;
    //每格之间的距离
    private float preWidth;
    //边距
    private float offset;
    //回调
    private GameCallBack callBack;
    //当前黑白棋胜利次数
    private int whiteChessCount, blackChessCount;
    /**
     * 一些常量
     */
    //白棋
    public static final int WHITE_CHESS = 1;
    //黑棋
    public static final int BLACK_CHESS = 2;
    //白棋赢
    public static final int WHITE_WIN = 101;
    //黑棋赢
    public static final int BLACK_WIN = 102;
    //平局
    public static final int NO_WIN = 103;

    public FiveChessView(Context context) {
        this(context, null);
    }

    public FiveChessView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FiveChessView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化Paint
        paint = new Paint();
        //设置抗锯齿
        paint.setAntiAlias(true);
        paint.setColor(Color.BLACK);
        //初始化chessArray
        chessArray = new int[GRID_NUMBER][GRID_NUMBER];
        //初始化棋子图片bitmap
        whiteChess = BitmapFactory.decodeResource(context.getResources(), R.drawable.white_chess);
        blackChess = BitmapFactory.decodeResource(context.getResources(), R.drawable.black_chess);
        //初始化胜利局数
        whiteChessCount = 0;
        blackChessCount = 0;
        //初始化Rect
        rect = new Rect();
        //设置点击监听
        setOnTouchListener(this);
    }

    /**
     * 重新测量宽高,确保宽高一样
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取高宽值
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //获取宽高中较小的值
        int len = width > height ? height : width;
        //重新设置宽高
        setMeasuredDimension(len, len);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //棋盘为一个GRID_NUMBER*GRID_NUMBER的正方形,所有棋盘宽高必须一样
        len = getWidth() > getHeight() ? getHeight() : getWidth();
        preWidth = len / GRID_NUMBER;
        //边距
        offset = preWidth / 2;
        //棋盘线条
        for (int i = 0; i < GRID_NUMBER; i++) {
            float start = i * preWidth + offset;
            //横线
            canvas.drawLine(offset, start, len - offset, start, paint);
            //竖线
            canvas.drawLine(start, offset, start, len - offset, paint);
        }
        //绘制棋子
        for (int i = 0; i < GRID_NUMBER; i++) {
            for (int j = 0; j < GRID_NUMBER; j++) {
                //rect中点坐标
                float rectX = offset + i * preWidth;
                float rectY = offset + j * preWidth;
                //设置rect位置
                rect.set((int) (rectX - offset), (int) (rectY - offset),
                        (int) (rectX + offset), (int) (rectY + offset));
                //遍历chessArray
                switch (chessArray[i][j]) {
                    case WHITE_CHESS:
                        //绘制白棋
                        canvas.drawBitmap(whiteChess, null, rect, paint);
                        break;
                    case BLACK_CHESS:
                        //绘制黑棋
                        canvas.drawBitmap(blackChess, null, rect, paint);
                        break;
                }
            }
        }
    }

    /**
     * 判断是否结束
     */
    private void checkGameOver() {
        //获取落子的颜色(如果当前是白棋,则落子是黑棋)
        int chess = isWhite ? BLACK_CHESS : WHITE_CHESS;
        //棋盘是否填满
        boolean isFull = true;
        //遍历chessArray
        for (int i = 0; i < GRID_NUMBER; i++) {
            for (int j = 0; j < GRID_NUMBER; j++) {
                //判断棋盘是否填满
                if (chessArray[i][j] != BLACK_CHESS && chessArray[i][j] != WHITE_CHESS) {
                    isFull = false;
                }
                //只需要判断落子是否五连即可
                if (chessArray[i][j] == chess) {
                    //判断五子相连
                    if (isFiveSame(i, j)) {
                        //五子相连游戏结束
                        isGameOver = true;
                        if (callBack != null) {
                            if (chess == WHITE_CHESS) {
                                whiteChessCount++;
                            } else {
                                blackChessCount++;
                            }
                            callBack.GameOver(chess == WHITE_CHESS ? WHITE_WIN : BLACK_WIN);
                        }
                        return;
                    }
                }
            }
        }
        //如果棋盘填满,平局结束
        if (isFull) {
            isGameOver = true;
            if (callBack != null) {
                callBack.GameOver(NO_WIN);
            }
        }
    }

    /**
     * 重置游戏
     */
    public void resetGame() {
        isGameOver = false;
        //重置棋盘状态
        for (int i = 0; i < GRID_NUMBER; i++) {
            for (int j = 0; j < GRID_NUMBER; j++) {
                chessArray[i][j] = 0;
            }
        }
        //更新UI
        postInvalidate();
    }

    /**
     * 判断是否存在五子相连
     *
     * @return
     */
    private boolean isFiveSame(int x, int y) {
        //判断横向
        if (x + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x + 1][y] && chessArray[x][y] == chessArray[x + 2][y]
                    && chessArray[x][y] == chessArray[x + 3][y] && chessArray[x][y] == chessArray[x + 4][y]) {
                return true;
            }
        }
        //判断纵向
        if (y + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x][y + 1] && chessArray[x][y] == chessArray[x][y + 2]
                    && chessArray[x][y] == chessArray[x][y + 3] && chessArray[x][y] == chessArray[x][y + 4]) {
                return true;
            }
        }
        //判断斜向(左上到右下)
        if (y + 4 < GRID_NUMBER && x + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x + 1][y + 1] && chessArray[x][y] == chessArray[x + 2][y + 2]
                    && chessArray[x][y] == chessArray[x + 3][y + 3] && chessArray[x][y] == chessArray[x + 4][y + 4]) {
                return true;
            }
        }
        //判断斜向(左下到右上)
        if (y - 4 > 0 && x + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x + 1][y - 1] && chessArray[x][y] == chessArray[x + 2][y - 2]
                    && chessArray[x][y] == chessArray[x + 3][y - 3] && chessArray[x][y] == chessArray[x + 4][y - 4]) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!isGameOver) {
                    //获取按下时的位置
                    float downX = event.getX();
                    float downY = event.getY();
                    //点击的位置在棋盘上
                    if (downX >= offset / 2 && downX <= len - offset / 2
                            && downY >= offset / 2 && downY <= len - offset / 2) {
                        //获取棋子对应的位置
                        int x = (int) (downX / preWidth);
                        int y = (int) (downY / preWidth);
                        //判断当前位置是否已经有子
                        if (chessArray[x][y] != WHITE_CHESS &&
                                chessArray[x][y] != BLACK_CHESS) {
                            //给数组赋值
                            chessArray[x][y] = isWhite ? WHITE_CHESS : BLACK_CHESS;
                            //修改当前执子
                            isWhite = !isWhite;
                            //更新棋盘
                            postInvalidate();
                            //判断是否结束
                            checkGameOver();
                            //回调当前执子
                            if (callBack != null) {
                                callBack.ChangeGamer(isWhite);
                            }
                        }
                    }
                } else {
                    Toast.makeText(mContext, "游戏已经结束,请重新开始!",
                            Toast.LENGTH_SHORT).show();
                }
                break;
        }
        return false;
    }

    public void setCallBack(GameCallBack callBack) {
        this.callBack = callBack;
    }

    public int getWhiteChessCount() {
        return whiteChessCount;
    }

    public int getBlackChessCount() {
        return blackChessCount;
    }
}

FiveChessView主要实现五子棋游戏的基本逻辑,包括棋子绘制,游戏胜利判断,重开游戏等逻辑:

构造方法

FiveChessView构造方法主要是对一些参数的初始化:
1.int[][] chessArray来保存游戏的棋子信息,chessArray[x][y] = 0/1/2(0->无子,1->白棋,2->黑棋)表示在(x,y)点的棋子信息,并通过判断chessArray各个位置的棋子来判断胜利。
2.whiteChess,blackChess黑白棋子图片对应的Bitmap。
3.rect用来指定绘制的棋子的大小。
4.添加OnTouchListener

 public FiveChessView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化Paint
        paint = new Paint();
        //设置抗锯齿
        paint.setAntiAlias(true);
        paint.setColor(Color.BLACK);
        //初始化chessArray
        chessArray = new int[GRID_NUMBER][GRID_NUMBER];
        //初始化棋子图片bitmap
        whiteChess = BitmapFactory.decodeResource(context.getResources(), R.drawable.white_chess);
        blackChess = BitmapFactory.decodeResource(context.getResources(), R.drawable.black_chess);
        //初始化胜利局数
        whiteChessCount = 0;
        blackChessCount = 0;
        //初始化Rect
        rect = new Rect();
        //设置点击监听
        setOnTouchListener(this);
    }

onMeasure

因为五子棋棋盘是一个N*N的网格,必须确保FiveChessView的宽高一致,所以必须在onMeasure对宽高进行重绘。

/**
     * 重新测量宽高,确保宽高一样
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取高宽值
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //获取宽高中较小的值
        int len = width > height ? height : width;
        //重新设置宽高
        setMeasuredDimension(len, len);
    }

onDraw

onDraw主要用来绘制棋盘网格线条和所有位置的棋子。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //棋盘为一个GRID_NUMBER*GRID_NUMBER的正方形,所有棋盘宽高必须一样
        len = getWidth() > getHeight() ? getHeight() : getWidth();
        preWidth = len / GRID_NUMBER;
        //边距
        offset = preWidth / 2;
        //棋盘线条
        for (int i = 0; i < GRID_NUMBER; i++) {
            float start = i * preWidth + offset;
            //横线
            canvas.drawLine(offset, start, len - offset, start, paint);
            //竖线
            canvas.drawLine(start, offset, start, len - offset, paint);
        }
        //绘制棋子
        for (int i = 0; i < GRID_NUMBER; i++) {
            for (int j = 0; j < GRID_NUMBER; j++) {
                //rect中点坐标
                float rectX = offset + i * preWidth;
                float rectY = offset + j * preWidth;
                //设置rect位置
                rect.set((int) (rectX - offset), (int) (rectY - offset),
                        (int) (rectX + offset), (int) (rectY + offset));
                //遍历chessArray
                switch (chessArray[i][j]) {
                    case WHITE_CHESS:
                        //绘制白棋
                        canvas.drawBitmap(whiteChess, null, rect, paint);
                        break;
                    case BLACK_CHESS:
                        //绘制黑棋
                        canvas.drawBitmap(blackChess, null, rect, paint);
                        break;
                }
            }
        }
    }

isFiveSame

isFiveSame用于判断指定位置是否存在五子相连,因为在checkGameOver中从左到右,从上到下遍历,所以判断五子相连,只需要判断以下四种情况:
1.横向(从左到右)
2.纵向(从下往上)
3.斜向(左上到右下)
4.斜向(左下到右上)
其他的类似横向(从右往左)这种情况就可以无需判断了。

/**
     * 判断是否存在五子相连
     *
     * @return
     */
    private boolean isFiveSame(int x, int y) {
        //判断横向(从左到右)
        if (x + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x + 1][y] && chessArray[x][y] == chessArray[x + 2][y]
                    && chessArray[x][y] == chessArray[x + 3][y] && chessArray[x][y] == chessArray[x + 4][y]) {
                return true;
            }
        }
        //判断纵向(从下往上)
        if (y + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x][y + 1] && chessArray[x][y] == chessArray[x][y + 2]
                    && chessArray[x][y] == chessArray[x][y + 3] && chessArray[x][y] == chessArray[x][y + 4]) {
                return true;
            }
        }
        //判断斜向(左上到右下)
        if (y + 4 < GRID_NUMBER && x + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x + 1][y + 1] && chessArray[x][y] == chessArray[x + 2][y + 2]
                    && chessArray[x][y] == chessArray[x + 3][y + 3] && chessArray[x][y] == chessArray[x + 4][y + 4]) {
                return true;
            }
        }
        //判断斜向(左下到右上)
        if (y - 4 > 0 && x + 4 < GRID_NUMBER) {
            if (chessArray[x][y] == chessArray[x + 1][y - 1] && chessArray[x][y] == chessArray[x + 2][y - 2]
                    && chessArray[x][y] == chessArray[x + 3][y - 3] && chessArray[x][y] == chessArray[x + 4][y - 4]) {
                return true;
            }
        }
        return false;
    }

checkGameOver

checkGameOver用于判断游戏是否结束,即是否存在五子相连或者平局的情况,如果游戏结束回调游戏结果。这里判断五子相连,是通过遍历chessArray判断是否存在落子的五子相连。平局则是遍历chessArray,判断是否存在空(chessArray[x][y] = 0)的情况,如果不存在则棋盘已满,平局。

/**
     * 判断是否结束
     */
    private void checkGameOver() {
        //获取落子的颜色(如果当前是白棋,则落子是黑棋)
        int chess = isWhite ? BLACK_CHESS : WHITE_CHESS;
        //棋盘是否填满
        boolean isFull = true;
        //遍历chessArray
        for (int i = 0; i < GRID_NUMBER; i++) {
            for (int j = 0; j < GRID_NUMBER; j++) {
                //判断棋盘是否填满
                if (chessArray[i][j] != BLACK_CHESS && chessArray[i][j] != WHITE_CHESS) {
                    isFull = false;
                }
                //只需要判断落子是否五连即可
                if (chessArray[i][j] == chess) {
                    //判断五子相连
                    if (isFiveSame(i, j)) {
                        //五子相连游戏结束
                        isGameOver = true;
                        if (callBack != null) {
                            if (chess == WHITE_CHESS) {
                                whiteChessCount++;
                            } else {
                                blackChessCount++;
                            }
                            callBack.GameOver(chess == WHITE_CHESS ? WHITE_WIN : BLACK_WIN);
                        }
                        return;
                    }
                }
            }
        }
        //如果棋盘填满,平局结束
        if (isFull) {
            isGameOver = true;
            if (callBack != null) {
                callBack.GameOver(NO_WIN);
            }
        }
    }

resetGame

这里没有太多的东西,就是重置游戏状态。

/**
     * 重置游戏
     */
    public void resetGame() {
        isGameOver = false;
        //重置棋盘状态
        for (int i = 0; i < GRID_NUMBER; i++) {
            for (int j = 0; j < GRID_NUMBER; j++) {
                chessArray[i][j] = 0;
            }
        }
        //更新UI
        postInvalidate();
    }

onTouch

onTouch通过点击位置实现落子功能。通过点击位置的坐标,来获取落子的位置,并判断当前位置是否已经有落子(chessArray[x][y] == 0 ?),落子后判断游戏是否结束并更新UI。游戏结束则无法落子。

@Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!isGameOver) {
                    //获取按下时的位置
                    float downX = event.getX();
                    float downY = event.getY();
                    //点击的位置在棋盘上
                    if (downX >= offset / 2 && downX <= len - offset / 2
                            && downY >= offset / 2 && downY <= len - offset / 2) {
                        //获取棋子对应的位置
                        int x = (int) (downX / preWidth);
                        int y = (int) (downY / preWidth);
                        //判断当前位置是否已经有子
                        if (chessArray[x][y] != WHITE_CHESS &&
                                chessArray[x][y] != BLACK_CHESS) {
                            //给数组赋值
                            chessArray[x][y] = isWhite ? WHITE_CHESS : BLACK_CHESS;
                            //修改当前执子
                            isWhite = !isWhite;
                            //更新棋盘
                            postInvalidate();
                            //判断是否结束
                            checkGameOver();
                            //回调当前执子
                            if (callBack != null) {
                                callBack.ChangeGamer(isWhite);
                            }
                        }
                    }
                } else {
                    Toast.makeText(mContext, "游戏已经结束,请重新开始!",
                            Toast.LENGTH_SHORT).show();
                }
                break;
        }
        return false;
    }

GameCallBack

游戏回调

/**
 * Created by ZhangHao on 2017/6/27.
 * 游戏相关回调
 */

public interface GameCallBack {
    //游戏结束回调
    void GameOver(int winner);
    //游戏更换执子回调
    void ChangeGamer(boolean isWhite);
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/back_ground"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    android:orientation="vertical">
    <!--游戏信息-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:orientation="horizontal">
        <!--白棋信息-->
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:orientation="vertical">

            <ImageView
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="5dp"
                android:background="@drawable/white_chess"
                android:contentDescription="@null" />

            <TextView
                android:id="@+id/white_count_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="10dp"
                android:text="0"
                android:textColor="#ffffff"
                android:textSize="16sp" />
        </LinearLayout>

        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1">

            <ImageView
                android:id="@+id/current_gamer"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_centerInParent="true"
                android:layout_marginTop="30dp"
                android:background="@mipmap/vs"
                android:contentDescription="@null" />
        </RelativeLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:orientation="vertical">

            <ImageView
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="5dp"
                android:background="@drawable/black_chess"
                android:contentDescription="@null" />

            <TextView
                android:id="@+id/black_count_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="10dp"
                android:text="0"
                android:textColor="#ffffff"
                android:textSize="16sp" />
        </LinearLayout>
    </LinearLayout>
    <!--游戏界面-->
    <com.sona.udv.FiveChessView
        android:id="@+id/five_chess_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <!--重新开始-->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageButton
            android:id="@+id/restart_game"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_centerInParent="true"
            android:background="@mipmap/restart"
            android:contentDescription="@null"
            android:onClick="onClick" />
    </RelativeLayout>
</LinearLayout>

Activity

public class MainActivity extends AppCompatActivity implements GameCallBack {

    private FiveChessView fiveChessView;
    private TextView whiteWinTv,blackWinTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fiveChessView = (FiveChessView) findViewById(R.id.five_chess_view);
        fiveChessView.setCallBack(this);
        whiteWinTv = (TextView) findViewById(R.id.white_count_tv);
        blackWinTv = (TextView) findViewById(R.id.black_count_tv);
    }

    @Override
    public void GameOver(int winner) {
        //更新游戏胜利局数
        updateWinInfo();
        switch (winner) {
            case FiveChessView.BLACK_WIN:
                showToast("黑棋胜利!");
                break;
            case FiveChessView.NO_WIN:
                showToast("平局!");
                break;
            case FiveChessView.WHITE_WIN:
                showToast("白棋胜利!");
                break;
        }
    }

    //更新游戏胜利局数
    private void updateWinInfo(){
        whiteWinTv.setText(fiveChessView.getWhiteChessCount()+" ");
        blackWinTv.setText(fiveChessView.getBlackChessCount()+" ");
    }

    @Override
    public void ChangeGamer(boolean isWhite) {

    }

    private void showToast(String str) {
        Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
    }

    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.restart_game:
                //重新开始游戏
                fiveChessView.resetGame();
                break;
        }
    }
}

Activity和布局文件,主要实现了一下功能:
1.添加背景图片
2.增加游戏信息(胜利局数)
3.游戏重开按钮
这里没什么特别需要注意的东西,布局或者背景其实也可以自己重新设置。

结语

1.因为文字功底有限,所以介绍性的文字不多,但是基本上每句代码都加了注释,理解起来应该不难,如果有任何问题,可以留言。
2.人机五子棋对战已经实现:http://blog.csdn.net/a512337862/article/details/76166049
3. Demo下载地址:http://download.csdn.net/detail/a512337862/9911855
4. Github:https://github.com/LuoChen-Hao/GameFiveChess

  • 8
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值