news 2026/9/4 8:02:15

Android五子棋开发实战:从自定义View到游戏逻辑完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android五子棋开发实战:从自定义View到游戏逻辑完整实现

简介:本资源是一个基于Android Studio开发的五子棋实战项目,面向Android初学者与移动应用开发学习者,旨在通过完整游戏案例掌握UI布局、事件响应、游戏逻辑封装与Kotlin编程实践。压缩包共49个文件,含17个XML布局与配置文件(定义界面结构与资源引用)、15个PNG/JPG图像资源(棋盘、棋子及UI图标)、4个Kotlin源码文件(实现MainActivity、棋盘控制、胜负判定等核心逻辑),以及Gradle构建脚本、Git配置与IDE配置文件等,整体仅877KB,轻量易导入。已有2304人学习下载,项目采用清晰模块化设计:ChessBoard类管理棋局状态,GameRule类封装五连判断算法,MoveValidator保障落子合法性,并配备重新开始功能与轮次提示,代码结构规范、注释充分,适合作为课程设计参考、毕业设计基础模板或Android进阶练习范例。

1. 项目概述与核心价值

最近在整理自己的项目库,翻到了一个几年前用Android Studio做的五子棋小游戏。这个项目虽然不大,但麻雀虽小五脏俱全,它几乎涵盖了Android应用开发从界面绘制、事件处理到核心逻辑判断的完整流程。对于想从“Hello World”迈向第一个完整功能应用的Android新手来说,五子棋是一个绝佳的练手项目。它不像电商、社交App那样业务复杂,但又比计算器、记事本多了不少交互和逻辑深度,能让你在实现一个具体游戏规则的过程中,把Activity、View、事件监听、自定义View、算法这些知识点都串起来用一遍。

很多人学Android开发,跟着教程敲完基础控件就卡住了,不知道下一步该做什么。这个五子棋项目就是一个很好的“下一站”。你不仅能学会如何用Java或Kotlin在Android Studio里搭建一个可运行的应用,更重要的是,你会亲身体验到如何将脑海中的游戏规则(比如“五子连珠即获胜”)翻译成严谨的代码逻辑。这个过程会逼着你去思考棋盘的数据结构用什么(二维数组?)、落子事件怎么响应、输赢判断算法如何设计(四个方向遍历?)等等实际问题。做完之后,你对Android开发的理解会从“知道有哪些组件”跃升到“知道如何用组件解决问题”的层面。

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

2.1 技术选型与架构思考

做一个五子棋App,技术上有很多条路可以走。你可以用最传统的Activity+View,通过重写onDraw方法在Canvas上画棋盘和棋子;也可以用更现代的Jetpack Compose声明式UI来构建。对于初学者,我强烈建议从自定义View入手。原因很简单:自定义View是理解Android绘图机制和触摸事件传递的基石。虽然Compose是未来趋势,但掌握了View体系,你再学Compose会感觉降维打击,知其然更知其所以然。

整个项目的骨架可以这样设计:一个主Activity,它承载一个全屏的自定义View(我们叫它GobangView)作为棋盘。GobangView内部需要维护几个核心数据:

  1. 棋盘数据模型:一个15x15的二维整型数组board就足够了。用0表示空位,1表示黑子,2表示白子。这是整个游戏状态的核心。
  2. 当前执子方:一个布尔变量isBlackTurn,标记当前该黑棋下还是白棋下。
  3. 游戏状态:比如GAME_STATE_PLAYING(进行中)、GAME_STATE_BLACK_WIN(黑胜)、GAME_STATE_WHITE_WIN(白胜)、GAME_STATE_DRAW(平局,五子棋较少见但可预留)。

为什么要用二维数组而不是更复杂的结构?因为对于棋盘这种规整的网格,数组的随机访问效率是最高的,坐标(i, j)直接对应数组board[i][j],在判断输赢时需要高频访问周围棋子状态,数组的优势就体现出来了。

2.2 核心功能模块划分

基于以上设计,我们可以把项目分解成几个相对独立的模块来逐个击破:

  • UI绘制模块:负责在onDraw方法中,根据board数组和游戏状态,绘制棋盘网格、黑白棋子、以及可能的胜负提示。
  • 触摸交互模块:重写onTouchEvent方法,将用户触摸的屏幕坐标(x, y)换算成棋盘数组的索引(row, col),并触发落子逻辑。
  • 游戏逻辑模块:这是大脑。包含落子有效性判断(该位置是否已有子?)、胜负判定算法(核心中的核心)、回合切换、以及悔棋、重新开始等功能的逻辑。
  • 状态控制模块:管理游戏流程,比如点击重新开始按钮后,如何重置board数组和游戏状态。

这种模块化的思考方式,能让你在编码时不至于一团乱麻。你可以先实现绘制一个静态棋盘,再加上触摸落子,最后攻克胜负判断这个最难的部分。

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

3.1 棋盘绘制:从像素到逻辑坐标的映射

绘制棋盘听起来简单,但第一个坑往往就在这里。假设我们的自定义View宽高是viewWidthviewHeight,棋盘是15x15,那么每个格子的宽度cellSize应该是多少?通常我们会让棋盘居中显示,并留出一些边距。

// 示例代码,在onDraw或onSizeChanged中计算 private float cellSize; // 每个格子的边长(像素) private float boardPadding; // 棋盘边距 private float boardSize; // 棋盘总边长(像素) @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 取View宽高中的较小值,并留出边距,保证棋盘是正方形 int minSize = Math.min(w, h); boardPadding = minSize * 0.05f; // 边距为5% boardSize = minSize - 2 * boardPadding; cellSize = boardSize / 14; // 注意!是14,不是15。15个点之间有14个间隔。 }

这里有个关键细节:格子数 vs 间隔数。15x15的棋盘,有15行15列棋子落点,但这些落点之间只有14个间隔。所以计算cellSize时,要用boardSize / 14。如果除以15,画出来的棋盘最右边和最下边会多出一截空白,棋子对不齐。

绘制网格线就是循环画横线和竖线:

Paint gridPaint = new Paint(); gridPaint.setColor(Color.BLACK); gridPaint.setStrokeWidth(2); // 画横线 for (int i = 0; i < 15; i++) { float startX = boardPadding; float startY = boardPadding + i * cellSize; float stopX = boardPadding + boardSize; canvas.drawLine(startX, startY, stopX, startY, gridPaint); } // 画竖线,类似...

3.2 触摸事件处理:精准的坐标转换

用户触摸屏幕后,我们需要把触摸点(eventX, eventY)转换成棋盘的(row, col)索引。这里涉及一个“吸附”逻辑:因为手指很难精准点在一个交叉点上,我们需要设定一个吸附半径,如果触摸点距离某个交叉点足够近,就认为用户想落子在那里。

@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { float touchX = event.getX(); float touchY = event.getY(); // 1. 判断触摸点是否在棋盘区域内 if (touchX < boardPadding || touchX > boardPadding + boardSize || touchY < boardPadding || touchY > boardPadding + boardSize) { return false; // 点在棋盘外,不处理 } // 2. 将像素坐标转换为棋盘索引(浮点数) float relativeX = touchX - boardPadding; float relativeY = touchY - boardPadding; float exactRow = relativeY / cellSize; // 可能得到如 3.7 这样的值 float exactCol = relativeX / cellSize; // 3. 四舍五入到最近的整数索引 int row = Math.round(exactRow); int col = Math.round(exactCol); // 4. 边界检查(防止四舍五入到15,数组越界) row = Math.max(0, Math.min(14, row)); col = Math.max(0, Math.min(14, col)); // 5. 调用落子逻辑 if (placePiece(row, col)) { invalidate(); // 重绘视图 checkWinner(row, col); // 检查是否获胜 } return true; } return super.onTouchEvent(event); }

注意Math.round是“四舍六入五成双”的银行家舍入法,在边界情况下可能不如直接(int)(exactRow + 0.5)来得直观可控。但在这个场景下差别不大。关键是一定要做数组索引的边界检查,这是避免ArrayIndexOutOfBoundsException的必备操作。

3.3 胜负判定算法:思路与优化

这是五子棋项目的算法核心。最直观的方法是,每当新落下一颗子(位置(row, col),颜色currentColor),就以该子为中心,向四个方向(水平、垂直、左上-右下、右上-左下)进行搜索,统计每个方向上的连续同色棋子数。

基础实现思路(以水平方向为例)

  1. 向左搜索:col-1,col-2... 直到棋盘边界或棋子颜色不同,计数leftCount
  2. 向右搜索:col+1,col+2... 直到棋盘边界或棋子颜色不同,计数rightCount
  3. 总连续数 =leftCount + 1 + rightCount(1是中心子本身)。
  4. 如果总连续数 >= 5,则判定当前执子方获胜。
  5. 其他三个方向同理。
private boolean checkWinner(int row, int col) { int currentColor = board[row][col]; // 四个方向向量:水平(0,1)、垂直(1,0)、左上右下(1,1)、右上左下(1,-1) int[][] directions = {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (int[] dir : directions) { int count = 1; // 中心子自己 // 向正方向搜索 count += countDirection(row, col, dir[0], dir[1], currentColor); // 向反方向搜索 count += countDirection(row, col, -dir[0], -dir[1], currentColor); if (count >= 5) { // 游戏结束,当前执子方获胜 gameState = (currentColor == BLACK) ? GAME_STATE_BLACK_WIN : GAME_STATE_WHITE_WIN; showWinnerDialog(); return true; } } // 检查是否平局(棋盘下满) if (isBoardFull()) { gameState = GAME_STATE_DRAW; showDrawDialog(); return true; } return false; } private int countDirection(int row, int col, int dRow, int dCol, int color) { int count = 0; int r = row + dRow; int c = col + dCol; while (r >= 0 && r < 15 && c >= 0 && c < 15 && board[r][c] == color) { count++; r += dRow; c += dCol; } return count; }

这个算法的时间复杂度是O(1),因为无论棋盘多大,它只检查固定四个方向上的有限格子(最多各检查4格)。效率完全足够。

实操心得:在实现countDirection时,循环的边界条件(r >= 0 && r < 15 && c >= 0 && c < 15)一定要写在board[r][c] == color之前,利用逻辑与&&的短路特性,防止数组越界。这是新手很容易忽略的一个错误点。

4. 完整实现流程与关键代码

4.1 环境搭建与项目初始化

首先确保你的Android Studio已经安装配置好。创建一个新的Empty Activity项目,语言选择Java或Kotlin(本文以Java为例)。在res/layout/activity_main.xml中,我们只需要一个全屏的FrameLayout,用于承载我们的自定义棋盘View。

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout>

然后在MainActivity.javaonCreate方法中,将我们自定义的GobangView添加到这个容器中。

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FrameLayout container = findViewById(R.id.container); GobangView gobangView = new GobangView(this); container.addView(gobangView); } }

4.2 自定义GobangView的实现骨架

接下来是重头戏,创建GobangView类。建议单独创建一个GobangView.java文件。

public class GobangView extends View { // 常量定义 private static final int EMPTY = 0; private static final int BLACK = 1; private static final int WHITE = 2; private static final int GAME_STATE_PLAYING = 0; private static final int GAME_STATE_BLACK_WIN = 1; private static final int GAME_STATE_WHITE_WIN = 2; private static final int GAME_STATE_DRAW = 3; // 核心数据 private int[][] board = new int[15][15]; // 棋盘数据 private boolean isBlackTurn = true; // 黑子先行 private int gameState = GAME_STATE_PLAYING; // 绘制相关 private Paint gridPaint, blackPiecePaint, whitePiecePaint, winLinePaint; private float cellSize, boardPadding, boardSize; public GobangView(Context context) { this(context, null); } public GobangView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 初始化画笔 gridPaint = new Paint(); gridPaint.setColor(0xFF8B4513); // 深棕色棋盘 gridPaint.setStrokeWidth(3); gridPaint.setAntiAlias(true); blackPiecePaint = new Paint(); blackPiecePaint.setColor(Color.BLACK); blackPiecePaint.setStyle(Paint.Style.FILL); blackPiecePaint.setAntiAlias(true); whitePiecePaint = new Paint(); whitePiecePaint.setColor(Color.WHITE); whitePiecePaint.setStyle(Paint.Style.FILL); whitePiecePaint.setStrokeWidth(2); whitePiecePaint.setAntiAlias(true); // 白棋通常加一个黑边,更清晰 whitePiecePaint.setStyle(Paint.Style.FILL); // 可以用两个圆叠加来实现带边框的白棋,这里先简化 winLinePaint = new Paint(); winLinePaint.setColor(Color.RED); winLinePaint.setStrokeWidth(5); winLinePaint.setAntiAlias(true); // 初始化棋盘数据 resetGame(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 计算棋盘和格子尺寸,代码见上一节 int minSize = Math.min(w, h); boardPadding = minSize * 0.05f; boardSize = minSize - 2 * boardPadding; cellSize = boardSize / 14; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBoard(canvas); drawPieces(canvas); if (gameState != GAME_STATE_PLAYING) { drawGameOver(canvas); } } // 绘制棋盘网格 private void drawBoard(Canvas canvas) { // 绘制背景(可选) canvas.drawColor(0xFFFFE4B5); // 米黄色背景 // 画横线竖线,代码见上一节 // ... } // 根据board数组绘制棋子 private void drawPieces(Canvas canvas) { for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (board[i][j] != EMPTY) { float centerX = boardPadding + j * cellSize; float centerY = boardPadding + i * cellSize; float radius = cellSize * 0.4f; // 棋子半径是格子大小的40% if (board[i][j] == BLACK) { canvas.drawCircle(centerX, centerY, radius, blackPiecePaint); } else { // 画白棋,可以先画一个白圆,再画一个稍细的黑圆作为边框 canvas.drawCircle(centerX, centerY, radius, whitePiecePaint); Paint borderPaint = new Paint(whitePiecePaint); borderPaint.setColor(Color.BLACK); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(2); canvas.drawCircle(centerX, centerY, radius, borderPaint); } } } } } // 游戏结束时的绘制,比如高亮显示五连珠 private void drawGameOver(Canvas canvas) { // 可以在这里绘制获胜的五子连线,需要记录获胜的起始和结束位置 // 略... } // 触摸事件处理,代码见上一节 @Override public boolean onTouchEvent(MotionEvent event) { // ... } // 落子逻辑 private boolean placePiece(int row, int col) { if (gameState != GAME_STATE_PLAYING) { return false; // 游戏已结束,不能落子 } if (board[row][col] != EMPTY) { return false; // 该位置已有棋子 } board[row][col] = isBlackTurn ? BLACK : WHITE; isBlackTurn = !isBlackTurn; // 切换执子方 return true; } // 胜负判定,代码见上一节 private void checkWinner(int row, int col) { // ... } private boolean isBoardFull() { for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (board[i][j] == EMPTY) { return false; } } } return true; } // 重新开始游戏 public void resetGame() { for (int i = 0; i < 15; i++) { Arrays.fill(board[i], EMPTY); } isBlackTurn = true; gameState = GAME_STATE_PLAYING; invalidate(); // 请求重绘 } }

这个骨架已经包含了最核心的功能。运行起来,你应该能看到一个可以交互的五子棋棋盘了。

4.3 功能增强:悔棋与重新开始

一个基本的游戏还需要控制功能。我们可以在屏幕下方添加一个简单的控制栏。修改activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <FrameLayout android:id="@+id/board_container" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <!-- GobangView将通过代码添加到这里 --> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center" android:padding="10dp"> <Button android:id="@+id/btn_undo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="悔棋" /> <Button android:id="@+id/btn_restart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="重新开始" android:layout_marginStart="20dp"/> </LinearLayout> </LinearLayout>

MainActivity中绑定按钮并实现功能。悔棋功能需要在GobangView中维护一个落子历史记录。

public class GobangView extends View { // ... 其他成员变量 private Stack<int[]> moveHistory = new Stack<>(); // 用于悔棋,记录每一步的[row, col] private boolean placePiece(int row, int col) { // ... 条件判断同上 board[row][col] = isBlackTurn ? BLACK : WHITE; moveHistory.push(new int[]{row, col}); // 记录这一步 isBlackTurn = !isBlackTurn; return true; } // 悔棋方法 public boolean undo() { if (moveHistory.isEmpty() || gameState != GAME_STATE_PLAYING) { return false; } int[] lastMove = moveHistory.pop(); int row = lastMove[0]; int col = lastMove[1]; board[row][col] = EMPTY; // 清空该位置 isBlackTurn = !isBlackTurn; // 回合回退 invalidate(); return true; } public void resetGame() { // ... 清空棋盘 moveHistory.clear(); // 清空历史 // ... 重置状态 } }

然后在MainActivity中设置按钮点击监听:

public class MainActivity extends AppCompatActivity { private GobangView gobangView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FrameLayout container = findViewById(R.id.board_container); gobangView = new GobangView(this); container.addView(gobangView); Button btnUndo = findViewById(R.id.btn_undo); Button btnRestart = findViewById(R.id.btn_restart); btnUndo.setOnClickListener(v -> gobangView.undo()); btnRestart.setOnClickListener(v -> gobangView.resetGame()); } }

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

在实际开发中,你肯定会遇到各种各样的问题。下面是我在实现和教学过程中总结的一些典型坑点和解决方案。

5.1 画面不刷新或刷新异常

  • 问题描述:落子后棋盘没有出现新棋子,或者棋子闪烁、残留。
  • 排查思路
    1. 检查invalidate():确保在每次修改了board数组或游戏状态后,都调用了invalidate()postInvalidate()来请求重绘。invalidate()必须在UI线程调用。
    2. 检查onDraw逻辑:在onDraw中打印日志,确认是否被调用以及board数组数据是否正确。确保绘制棋子的循环覆盖了整个数组(0到14)。
    3. 检查坐标计算:确认boardPaddingcellSizeboardSizeonSizeChanged中计算正确,并且在onDrawonTouchEvent中使用的是一致的值。有时在onDraw中重复计算这些值会导致偏差。
  • 解决方案:在onDraw开始处添加Log.d(“Gobang”, “onDraw called”),在placePiece后添加Log.d(“Gobang”, “place at ” + row + “,” + col)。通过日志流观察程序执行顺序和数据变化。

5.2 触摸落子位置不准

  • 问题描述:点击交叉点附近,棋子却下在了别处。
  • 排查思路
    1. 打印坐标:在onTouchEvent中打印touchX,touchY,exactRow,exactCol,row,col,看转换过程哪一步出了问题。
    2. 检查吸附逻辑:确认你的“吸附”算法(Math.round)是否符合预期。有时候因为boardPadding计算有误,导致整个坐标系的偏移。
    3. 检查棋盘绘制区域:确认onDraw中绘制的棋盘网格,其起始点(boardPadding, boardPadding)和总大小boardSizeonTouchEvent中判断触摸区域和计算索引时使用的值完全一致。建议将这些值作为成员变量,只在onSizeChanged中计算一次。
  • 解决方案:在屏幕上绘制出触摸点的位置和最终转换出的索引,进行可视化调试。可以在onDraw的最后添加:
    // 调试用:绘制最后一次触摸的像素坐标和逻辑坐标 if (lastTouchX > 0) { Paint debugPaint = new Paint(); debugPaint.setColor(Color.BLUE); debugPaint.setTextSize(30); canvas.drawText(“Touch: (“ + lastTouchX + “,” + lastTouchY + “)”, 50, 50, debugPaint); canvas.drawText(“Index: (“ + lastRow + “,” + lastCol + “)”, 50, 90, debugPaint); }

5.3 胜负判断逻辑错误

  • 问题描述:五子连珠了没判赢,或者没连成五子却判赢了。
  • 排查思路
    1. 边界检查:这是最常见的错误。在countDirection函数中,一定要先检查数组索引是否越界,再访问数组元素while (r >= 0 && r < 15 && c >= 0 && c < 15 && board[r][c] == color)这个顺序不能错。
    2. 方向向量:检查四个方向向量{0,1},{1,0},{1,1},{1,-1}是否正确,它们必须覆盖水平、垂直和两条对角线。
    3. 计数逻辑:确认count的初始值是1(代表中心子),并且正反两个方向的计数相加。可以用一个简单的测试用例,比如在棋盘中央手动设置一个五连珠,然后单步调试checkWinner函数。
    4. 游戏状态更新:确保在判定获胜后,正确更新了gameState,并且阻止了后续的落子操作(在placePiece开头检查gameState)。
  • 解决方案:编写单元测试。在Android项目中可以创建本地单元测试(JUnit)来单独测试checkWinner函数。准备几个预设的棋盘状态(如刚形成五连珠、四连珠被堵住等),断言函数的返回结果是否符合预期。这能极大提升逻辑的可靠性。

5.4 性能与内存问题

  • 问题描述:在低端设备上感觉卡顿,或者长时间运行后内存增长。
  • 排查思路
    1. 避免在onDraw中创建对象onDraw会被频繁调用,任何在这里new Paint(),new Path()的操作都会产生大量临时对象,触发GC导致卡顿。所有Paint对象都应在init()中初始化并复用。
    2. 棋盘数据模型:对于15x15的棋盘,用int[][]完全没问题。但如果想支持超大棋盘(比如30x30),可以考虑使用SparseArray等稀疏结构来存储已落子的位置,但会稍微增加判断某点是否有子的复杂度。
    3. 视图层级:我们的例子中只有一个自定义View,性能很好。但如果界面复杂,要避免过度绘制和复杂的视图层级。
  • 解决方案:使用Android Profiler工具监控CPU和内存使用情况。确保onDraw方法执行时间短(理想情况小于16ms以维持60fps)。

5.5 横竖屏切换导致状态丢失

  • 问题描述:旋转屏幕后,棋盘清空了,游戏状态重置。
  • 原因:默认情况下,屏幕旋转会导致Activity销毁重建,成员变量会丢失。
  • 解决方案:需要保存和恢复状态。有两种主要方式:
    1. 使用onSaveInstanceStateonRestoreInstanceState:在GobangView中重写这两个方法,保存board数组、isBlackTurngameStatemoveHistory
      @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); Bundle savedState = new Bundle(); savedState.putParcelable(“superState”, superState); savedState.putSerializable(“board”, board); // 注意:int[][]需要转换 savedState.putBoolean(“isBlackTurn”, isBlackTurn); savedState.putInt(“gameState”, gameState); // moveHistory 需要自己实现序列化 return savedState; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle savedState = (Bundle) state; board = (int[][]) savedState.getSerializable(“board”); isBlackTurn = savedState.getBoolean(“isBlackTurn”); gameState = savedState.getInt(“gameState”); state = savedState.getParcelable(“superState”); } super.onRestoreInstanceState(state); invalidate(); }
    2. 使用ViewModel(推荐):这是更现代的做法。将游戏状态(棋盘数据、回合等)放在一个继承自ViewModel的类中。这样即使Activity因配置更改重建,ViewModel也会保留。这需要结合LiveData或StateFlow使用,稍微复杂但架构更清晰。

把这个五子棋项目从头到尾实现一遍,你会对Android的自定义View、事件处理、游戏逻辑有一个非常扎实的掌握。它就像一块很好的敲门砖,敲开了Android应用开发实战的大门。之后你可以在此基础上添加更多功能,比如AI人机对战(这又是一个有趣的算法挑战)、网络联机、更炫的动画效果等等,每一个扩展都能带你深入一个新的技术领域。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 8:01:59

AI 私聊不该有“账号违规“

一、一个具体的触发场景 笔者在排查一次生产环境故障时&#xff0c;将一段系统日志粘贴到 AI 对话框&#xff0c;请求分析报错原因。日志中包含以下关键词&#xff1a; [FATAL] Segmentation fault at 0x7f... [ERROR] Buffer overflow detected in module X [WARN] Process k…

作者头像 李华
网站建设 2026/9/4 8:01:29

STM32 DDS信号发生器实战:从硬件选型到THD优化

简介&#xff1a;本资源是一套基于STM32F103C6微控制器的DDS&#xff08;直接数字频率合成&#xff09;信号发生器完整仿真开发工程&#xff0c;面向嵌入式初学者、电子类课程设计学生及单片机实践开发者&#xff0c;解决波形生成原理理解难、软硬件协同调试复杂等实际问题。压…

作者头像 李华
网站建设 2026/9/4 7:59:22

PyTorch实现红外与可见光图像融合:从原理到Jupyter实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 7:59:14

AI短剧一键生成:从故事到成片的自动化流水线实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 7:56:50

单因素方差分析结果解读:F统计量与事后比较

单因素方差分析结果解读一、方法概述单因素方差分析&#xff08;One-way Analysis of Variance, One-way ANOVA&#xff09;是检验三个及以上独立组别均值是否存在显著差异的经典参数检验方法。其基本思想是将总变异分解为组间变异&#xff08;Between-group Variation&#xf…

作者头像 李华
网站建设 2026/9/4 7:54:41

基于OpenCV与Python的车牌识别系统:从图像预处理到字符识别的完整实践

简介&#xff1a;这是一套面向计算机及相关专业本科生的车牌识别实战项目资源&#xff0c;专为期末大作业与课程设计打造&#xff0c;聚焦OpenCV图像处理与Python编程的综合应用。资源包含完整可运行源码、详细技术报告及答辩用PPT&#xff0c;覆盖图像预处理、车牌定位、字符分…

作者头像 李华