持续创造,加速生长!这是我参与「日新计划 10 月更文挑战」的第2天,点击检查活动详情

前言

为什么写这系列文章

尽管 compose 正式版已经出来很久了,也有许多大佬写了许多教程文章和实例 demo ,可是关于 compose 其实我也还是一知半解的。

特别是关于 compose 的状况办理,由于 compose 声明式的特性,假如不对状况进行完善的办理,那么界面代码和事务逻辑代码将会杂糅在一起,导致代码可读性、可维护性非常差。

许多大佬们都说运用 MVI 架构来办理 compose 的状况是天生一对。

我也测验运用 MVI 架构编写了一个简略的游戏:根据 Jetpack Compose,运用MVI架构+自定义布局完成的康威生命游戏。

不过,这就存在一个很大的问题,大佬们几乎都是运用 ViewModel 来完成 MVI 架构。可是, ViewModel 是强依赖于安卓原生 API,这就导致无法将这个项目移植到 compose-jb 完成跨渠道。

我也收集了大佬们的解决方案,无非以下几种:

  1. 不再运用安卓的 ViewModel,而是自己参照源码手撸一个跨渠道的相似功能的库。例如:Compose Mutiplatform 实战联机小游戏
  2. 给不同的渠道封装不同的状况办理完成类,例如:不止 Android,Compose Multiplatform 初探
  3. 索性直接不运用 ViewModel ,改用其它能够跨渠道运用的库,例如:Compose 下的 MVI 架构实践,用 Compose 写事务逻辑,取代 ViewModel

回到咱们标题的问题,为什么我要写这系列文章?

由于现在关于 compose 的运用办法,最佳实践都尚在探索期。我也不确定什么才是最适合自己运用的,唯有多测验才知道。

所以我觉得我应该再试试不同的完成办法,这次就运用上面所说的办法3进行测验。

由于这系列文章不同于以往采用的是代码已经写完而且测试没问题后才开端撰写文章,而是采用边测验写代码,边记载的办法。

所以文章可能会有所疏忽或过错,可是我会在发现问题后第一时间在后续文章中阐明并校正。

是非棋是什么

是非棋(英语:Reversi),又称翻转棋、苹果棋或奥赛罗棋(Othello),是一种双人对弈的棋类游戏。

一般棋子双面为是非两色,故称“是非棋”;由于行棋之时将对方棋子翻转,变为己方棋子,故又称“翻转棋”(Reversi);棋子双面为红、绿色的称为“苹果棋”,因苹果有红苹果和青苹果。

游戏规矩:

棋盘共有8行8列共64格。局面时,棋盘正中央的4格先置放是非相隔的4枚棋子(亦有求变化相邻放置)。一般黑子先行。双方轮流落子。只要落子和棋盘上任一枚己方的棋子在一条在线(横、直、斜线皆可)夹着对方棋子,就能将对方的这些棋子转变为我己方(翻面即可)。假如在任一方位落子都不能夹住对手的任一颗棋子,就要让对手下子。当双方皆不能下子时,游戏就完毕,子多的一方胜。

跟我一起使用 compose 做一个跨平台的黑白棋游戏(1)整体实现思路

以上内容和图片摘自 维基百科 是非棋 条目

完成思路

我的目标

这个项目的目标是首要运用 Jetpack compose 完成安卓端的是非棋游戏,然后移植到 compose-jb 完成跨渠道。

我会优先完成单机版游戏,后期考虑加入联机游戏。

关于游戏的状况办理,依旧运用 MVI 作为架构,可是不再运用 Jetpack ViewModel 完成,而是测验运用 composable 和 Flow 做一个渠道无关的状况办理。

关于单机游戏的AI

由于这个项目的目的是找到关于我来说 compose 开发的最佳实践,所以算法逻辑不在这个项目的要点。

可是假如要做单机游戏,对战AI是必不可少的,所以我找到了一个开源项目 reversi , 之后项目中的AI算法将运用这个项目的,部分UI可能也会直接从这个项目里面拿。

准确的说,我现在是在将这个项目移植为运用 compose 完成。 (*^_^*)

所以在开端之前咱们需求先简略分析一下这个项目的组成结构。

跟我一起使用 compose 做一个跨平台的黑白棋游戏(1)整体实现思路

不得不说,大佬的项目看起来便是赏心悦目,各个模块分工明确:

模块 阐明
activity 这个不用多解释,便是 Activity,咱们需求留意的是 GameActivity ,承载游戏界面的 Activity
bean 一些数据 bean
game AI核心算法逻辑自定义的棋盘view
util 一些工具办法
widget 大佬在这儿封装了几个 dialog

这是大佬的游戏主界面:

跟我一起使用 compose 做一个跨平台的黑白棋游戏(1)整体实现思路

在这儿咱们需求要点重视的是 GameActivity 这个 Activity 承载了游戏的主界面和操控逻辑。

game layout 的布局结构如下:

跟我一起使用 compose 做一个跨平台的黑白棋游戏(1)整体实现思路

能够看到,除了棋盘运用的是自定义 view : ReversiView 外,其他都是运用根底控件组成的 游戏信息操控按钮

ReversiView 的内容这儿咱们就不详细看了,假如咱们要移植到 compose 的话能够很轻松的直接将它的代码 “copy” 过来并转成 compose 的 canvas 代码。当然,咱们也能够完全自己重写,详细的制作内容,咱们将在下一篇文章详细阐明。

这儿咱们侧重看一下如何运用它的AI算法。

首要,在 GameActivity 中,他运用 setOnTouchListener 监听了 ReversiView 的触摸事件:

reversiView.setOnTouchListener(new OnTouchListener() {
    boolean down = false;
    int downRow;
    int downCol;
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (gameState != STATE_PLAYER_MOVE) { // 没有轮到玩家下子,直接返回
            return false;
        }
        float x = event.getX();
        float y = event.getY();
        if (!reversiView.inChessBoard(x, y)) {  // 判别是否在棋盘范围内
            return false;
        }
        // 核算棋盘的横纵坐标
        int row = reversiView.getRow(y);
        int col = reversiView.getCol(x);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 按下时记载按下的棋盘坐标
                down = true;
                downRow = row;
                downCol = col;
                break;
            case MotionEvent.ACTION_UP:
                if (down && downRow == row && downCol == col) { // 只要在抬起坐标和按下坐标一致时才继续处理
                    down = false;
                    if (!Rule.isLegalMove(chessBoard, new Move(row, col), playerColor)) { // isLegalMove 这个办法用于判别往这个坐标下子是否合法
                        return true;
                    }
                    // 判别完成后开端依照规矩更新数据和UI
                    Move move = new Move(row, col);
                    List<Move> moves = Rule.move(chessBoard, move, playerColor);
                    reversiView.move(chessBoard, moves, move, playerColor);
                    // 轮到AI下子
                    aiTurn();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                down = false;
                break;
        }
        return true;
    }
});

我已经把不重要的代码删除,并加上了注释。

这儿咱们需求重视更新数据的办法:Rule.move() ;AI下子的办法:aiTurn()

aiTurn() 这个办法首要会调用 Rule.analyse() 办法核算当时玩家和 AI 拥有的棋子数量,然后根据核算出的数量更新游戏界面,并将游戏状况更改为 STATE_AI_MOVE 即轮到 AI 下子,最终发动一个新的线程 new ThinkingThread(aiColor).start(); 用于运转AI算法。

ThinkingThread 的代码如下:

class ThinkingThread extends Thread {
    private byte thinkingColor;
    public ThinkingThread(byte thinkingColor) {
        this.thinkingColor = thinkingColor;
    }
    public void run() {
        try {
            sleep(20 * 100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int legalMoves = Rule.getLegalMoves(chessBoard, thinkingColor).size();
        if (legalMoves > 0) {
            Move move = Algorithm.getGoodMove(chessBoard, depth[difficulty], thinkingColor, difficulty);
            List<Move> moves = Rule.move(chessBoard, move, thinkingColor);
            reversiView.move(chessBoard, moves, move, thinkingColor);
        }
        updateUI.handle(0, legalMoves, thinkingColor);
    }
}

能够看到,这个线程首要暂停了自己 2000 ms …… 额,为了让人看起来这个算法很厉害需求算 2s 吗?哈哈~

不论这个奇怪的暂停,咱们接着往下看。

首要,调用 Rule.getLegalMoves().size(); 获取到所有能够下子的方位数量,假如数量大于 0 则继续处理。

经过调用 Algorithm.getGoodMove() 获取到算法核算出的最佳下子方位,然后更新数据和UI。

由于这儿咱们只需求知道怎样复用作者的算法即可,所以咱们不深究算法的详细完成。

假如感兴趣的能够看看作者自己写的解读:android是非棋游戏完成

综上所述,咱们已经明晰应该如何运用这位大佬编写的AI算法了。

根底架构demo

正如上文所述,咱们现在需求运用 Flow 和 composable 完成一个渠道无关的数据办理结构。

这儿咱们依照上文大佬的思路编写一个简略的 demo 验证可行性:

@Composable
fun Demo() {
    val channel = remember { Channel<Action>() }
    val flow = remember(channel) { channel.consumeAsFlow() }
    val state = presenter(action = flow)
    Column {
        Text(text = state.count.toString())
        Button(
            onClick = {
                channel.trySend(Action.ClickAdd)
            }
        ) {
            Text(text = "ADD")
        }
    }
}
sealed class Action {
    object ClickAdd : Action()
}
data class State (
    val count: Int = 0,
)
@Composable
fun presenter(
    action: Flow<Action>,
): State {
    var count by remember { mutableStateOf(0) }
    LaunchedEffect(action) {
        action.collect { action: Action ->
            when (action) {
                is Action.ClickAdd -> count++
            }
        }
    }
    return State(
        count = count
    )
}

由于这儿仅仅为了验证可行性,所以我直接把所有代码写到了一起,实践编写时肯定是要分开的

Android 运转效果:

跟我一起使用 compose 做一个跨平台的黑白棋游戏(1)整体实现思路

Desktop 运转效果:

跟我一起使用 compose 做一个跨平台的黑白棋游戏(1)整体实现思路

总结

经过上面的分析和实践,证明不依赖安卓的 ViewModel 确实是能够完成 MVI 架构,这就意味着之后移植至 compose-jb 将愈加便利。

当然,本文仅仅简略的梳理了一下思路,从下一篇开端咱们将正式开端编写。

下一篇咱们介绍怎样制作棋盘和棋子,以及编写界面布局。