Android Studio开发五子棋:从自定义View到AI对手的完整实践

发布时间:2026/8/31 3:15:38
Android Studio开发五子棋:从自定义View到AI对手的完整实践 简介这是一份面向Android初学者与进阶开发者的五子棋实战项目资源聚焦Android Studio环境下的UI布局、事件响应与游戏逻辑实现帮助学习者系统掌握移动端交互式游戏开发全流程。压缩包共49个文件含17个XML布局与配置文件定义界面结构与资源引用、15个PNG/JPG图像资源棋子、按钮等UI元素、4个Kotlin源码文件核心的MainActivity、ChessBoard、GameRule等逻辑类以及Gradle构建脚本、Git版本配置等工程必需文件整体仅877KB轻量易导入。已有2304人学习下载资源结构清晰完整呈现标准Android模块化组织方式——从app/src主代码目录、gradle构建体系到.idea开发配置一应俱全附带可直接运行的棋盘渲染、回合切换、五连珠胜负判定及触控落子逻辑是理解Android生命周期、自定义View与状态管理的优质练手案例。1. 项目缘起为什么用Android Studio做五子棋几年前我刚接触Android开发时总想找个能贯穿UI、逻辑、交互的完整项目来练手。那些“Hello World”式的教程总觉得差点意思而一个稍微复杂点的电商或社交App对新手来说又过于庞大容易在复杂的业务逻辑里迷失。后来我发现像五子棋、扫雷、贪吃蛇这类经典小游戏简直是绝佳的练手项目。它们规则明确功能边界清晰但麻雀虽小五脏俱全——你需要设计界面、处理用户交互、编写核心游戏逻辑、甚至还能引入AI对手。这不最近又有朋友问起Android开发入门项目我第一个想到的就是五子棋。今天我就把自己从零开始在Android Studio里实现一个带简单AI的五子棋应用的全过程包括那些新手最容易踩的坑掰开揉碎了分享给你。无论你是想巩固Java/Kotlin基础还是想理解Android的基础组件如View、Canvas绘制、触摸事件这个项目都能给你带来实实在在的收获。2. 环境搭建与项目初始化避开那些“坑爹”的配置万事开头难对于Android开发这个“难”往往就卡在环境配置上。网上的教程五花八门但很多都过时了或者忽略了关键细节导致你跟着做却跑不起来。这里我结合最新的Android Studio版本以当前稳定的版本为例给你梳理一条最稳妥的路径。2.1 Android Studio安装与“第一道坎”首先去官网下载Android Studio。这里第一个坑就来了安装路径千万不要有中文或空格。很多新手喜欢把软件装在“D:\编程软件\”这类目录下这可能导致后续Gradle构建时出现各种诡异错误。我的建议是路径简单点比如D:\Android\AndroidStudio。安装过程中会提示你安装Android Virtual Device (AVD) 组件也就是安卓模拟器。这里务必勾选。但即使勾选了安装完成后首次运行你可能还是会遇到“Android Virtual Device Unavailable”的提示。这通常是因为你的电脑没有开启CPU虚拟化技术Intel的VT-x或AMD的SVM。你需要重启电脑进入BIOS开机按F2、Del等键因电脑品牌而异找到“Virtualization Technology”或类似选项将其设置为“Enabled”。对于使用Windows且不带Hyper-V功能的电脑比如一些家庭版系统运行模拟器可能会更麻烦。这种情况下我强烈建议你直接使用真机调试。用USB线连接手机打开手机的“开发者选项”和“USB调试”在Android Studio中运行项目时选择你的真机设备比折腾模拟器快得多也稳定得多。2.2 创建新项目与Gradle“慢”的问题打开Android Studio选择“New Project”。这里我们选择“Empty Views Activity”语言选择Java为了照顾更广泛的读者本文以Java为例Kotlin的实现逻辑类似。给项目起个名字比如“Gomoku”包名按自己喜欢来保存位置再次确保无中文和空格。点击“Finish”后你会进入漫长的等待——Gradle正在下载项目依赖。这是第二个大坑默认的仓库地址在国外下载速度极慢甚至可能失败。解决方法就是更换国内镜像源。找到项目根目录下的build.gradle文件注意是Project级别的那个不是Module级别的。在buildscript和allprojects的repositories块里将google()和mavenCentral()前面加上阿里云的镜像地址。修改后大致如下// 项目根目录 build.gradle buildscript { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/gradle-plugin } // 原来的 google() 和 mavenCentral() 可以注释掉或保留但阿里的源要在前面 // google() // mavenCentral() } ... } allprojects { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/gradle-plugin } // google() // mavenCentral() } }修改后点击Android Studio右上角的“Sync Now”同步项目。速度应该会有质的提升。如果还遇到某个特定Gradle版本下载慢比如gradle-6.5-bin.zip你可以去Gradle官网手动下载对应版本然后将其放入C:\Users\你的用户名\.gradle\wrapper\dists\目录下对应的文件夹内再重新同步。2.3 项目结构初窥与基础设置项目同步成功后我们简单看一下目录结构。app/src/main/java/下是我们的Java代码目录app/src/main/res/下是资源文件目录包括布局layout、图片drawable、常量values等。为了让开发过程更舒适我建议做两个设置代码字体在File - Settings - Editor - Font中调整一个适合编程的等宽字体和大小。Logcat字体调试时看日志是家常便饭默认的Logcat字体太小。在Logcat窗口右键选择“Use Soft Wraps”并调整字体大小。做完这些我们的开发环境就算准备好了。虽然步骤看起来有点多但这些都是“一劳永逸”的投入配置好之后后续的开发会顺畅很多。3. 游戏界面绘制自定义View与Canvas的运用五子棋的棋盘和棋子显然不能用普通的按钮Button或文本框TextView来堆砌。我们需要一个能自由绘制的画布这就是Android中的自定义View。我们将创建一个继承自View的类在它的onDraw方法里用Canvas画布和Paint画笔画出一切。3.1 创建棋盘View类首先在java目录下你的包名里新建一个Java类命名为ChessBoardView。package com.yourpackage.gomoku; // 替换成你的包名 import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; public class ChessBoardView extends View { // 棋盘参数 private int boardRows 15; // 15路棋盘标准五子棋 private float cellWidth; // 每个格子的宽度 private float offset; // 棋盘边距 private Paint gridPaint; // 画网格的画笔 private Paint blackPiecePaint; // 画黑棋的画笔 private Paint whitePiecePaint; // 画白棋的画笔 // 棋盘数据0空1黑2白 private int[][] boardMatrix new int[boardRows][boardRows]; // 构造方法 public ChessBoardView(Context context) { this(context, null); } public ChessBoardView(Context context, Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 初始化网格画笔 gridPaint new Paint(Paint.ANTI_ALIAS_FLAG); gridPaint.setColor(0xFF8B4513); // 深棕色 gridPaint.setStrokeWidth(3); // 初始化黑棋画笔实心圆 blackPiecePaint new Paint(Paint.ANTI_ALIAS_FLAG); blackPiecePaint.setColor(0xFF000000); // 黑色 blackPiecePaint.setStyle(Paint.Style.FILL); // 初始化白棋画笔实心圆 whitePiecePaint new Paint(Paint.ANTI_ALIAS_FLAG); whitePiecePaint.setColor(0xFFFFFFFF); // 白色 whitePiecePaint.setStyle(Paint.Style.FILL); // 为白棋加一个黑色的边框更清晰 whitePiecePaint.setStrokeWidth(2); // 注意Fill样式下Stroke不生效我们需要另一个画笔来画边框这里先简化 } }这里有几个关键点boardRows15标准五子棋是15x15的棋盘。boardMatrix一个二维数组用来记录棋盘上每个点的状态。这是游戏逻辑的核心数据源。Paint.ANTI_ALIAS_FLAG抗锯齿标志让画出来的线条和图形边缘更平滑。构造方法自定义View通常需要两个构造方法以支持在XML布局中引用。3.2 计算尺寸与绘制网格View的大小不是固定的我们需要在onSizeChanged方法中根据View的实际宽高计算出每个格子的宽度cellWidth和边距offset。Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 取宽高中的较小值作为棋盘的有效区域保证是正方形 int boardSize Math.min(w, h); offset 50; // 留出50像素的边距为了美观 cellWidth (boardSize - 2 * offset) / (boardRows - 1); // 格子数比交叉点数少1 } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawGrid(canvas); drawPieces(canvas); } // 绘制棋盘网格 private void drawGrid(Canvas canvas) { // 画横线 for (int i 0; i boardRows; i) { float startX offset; float startY offset i * cellWidth; float stopX offset (boardRows - 1) * cellWidth; float stopY startY; canvas.drawLine(startX, startY, stopX, stopY, gridPaint); } // 画竖线 for (int i 0; i boardRows; i) { float startX offset i * cellWidth; float startY offset; float stopX startX; float stopY offset (boardRows - 1) * cellWidth; canvas.drawLine(startX, startY, stopX, stopY, gridPaint); } } // 绘制棋子根据boardMatrix数据 private void drawPieces(Canvas canvas) { for (int i 0; i boardRows; i) { for (int j 0; j boardRows; j) { if (boardMatrix[i][j] ! 0) { float centerX offset j * cellWidth; float centerY offset i * cellWidth; float radius cellWidth / 2 * 0.8f; // 棋子半径是格子宽度的0.8倍留点空隙 if (boardMatrix[i][j] 1) { canvas.drawCircle(centerX, centerY, radius, blackPiecePaint); } else if (boardMatrix[i][j] 2) { // 画白棋先画一个白色实心圆 canvas.drawCircle(centerX, centerY, radius, whitePiecePaint); // 再画一个黑色的圆环作为边框简化版可以用另一个画笔 Paint borderPaint new Paint(Paint.ANTI_ALIAS_FLAG); borderPaint.setColor(0xFF000000); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(2); canvas.drawCircle(centerX, centerY, radius, borderPaint); } } } } }onSizeChanged是关键它确保了无论我们的View被布局成什么尺寸棋盘都能正确居中并缩放。drawGrid方法就是简单的画线循环。drawPieces方法遍历boardMatrix数组在非空的位置画出对应的棋子。这里给白棋加边框是为了在白色背景上能看清。3.3 将棋盘View加入布局打开res/layout/activity_main.xml将默认的TextView替换成我们的自定义View。?xml version1.0 encodingutf-8? androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto xmlns:toolshttp://schemas.android.com/tools android:layout_widthmatch_parent android:layout_heightmatch_parent tools:context.MainActivity com.yourpackage.gomoku.ChessBoardView android:idid/chessBoard android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfParent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent / /androidx.constraintlayout.widget.ConstraintLayout注意com.yourpackage.gomoku要替换成你实际的包名。现在运行项目你应该能看到一个棕色的15x15棋盘绘制在屏幕中央了。4. 落子逻辑与触摸交互从点击到棋盘数据更新光有棋盘还不够我们需要让用户能下棋。这需要处理触摸事件onTouchEvent将触摸点的坐标转换成棋盘的网格坐标然后更新boardMatrix数据并重绘。4.1 处理触摸事件在ChessBoardView类中添加一个变量记录当前该谁下棋并重写onTouchEvent方法。public class ChessBoardView extends View { // ... 之前的变量 ... private boolean isBlackTurn true; // 当前回合true为黑方false为白方 private GameCallback gameCallback; // 回调接口用于通知Activity游戏状态变化 // 定义回调接口 public interface GameCallback { void onPiecePlaced(int player, int row, int col); void onGameOver(int winner); // winner: 0-继续1-黑胜2-白胜 } public void setGameCallback(GameCallback callback) { this.gameCallback callback; } Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN) { // 1. 获取触摸点坐标 float touchX event.getX(); float touchY event.getY(); // 2. 将坐标转换为棋盘网格索引 int col (int) ((touchX - offset cellWidth / 2) / cellWidth); int row (int) ((touchY - offset cellWidth / 2) / cellWidth); // 3. 检查点击是否在棋盘范围内并且该位置为空 if (row 0 row boardRows col 0 col boardRows boardMatrix[row][col] 0) { // 4. 更新棋盘数据 boardMatrix[row][col] isBlackTurn ? 1 : 2; // 5. 通知外部如Activity棋子已落下 if (gameCallback ! null) { gameCallback.onPiecePlaced(isBlackTurn ? 1 : 2, row, col); } // 6. 检查游戏是否结束 if (checkWin(row, col)) { if (gameCallback ! null) { gameCallback.onGameOver(isBlackTurn ? 1 : 2); } // 游戏结束可以禁用触摸或显示结果 return true; } // 7. 切换回合 isBlackTurn !isBlackTurn; // 8. 重绘View invalidate(); return true; } } return super.onTouchEvent(event); } }坐标转换的公式(touchX - offset cellWidth / 2) / cellWidth是关键。touchX - offset得到相对于棋盘左上角的坐标加上cellWidth/2是为了实现“点击格子附近即落子在该交叉点”的效果然后除以格子宽度得到索引。强制转换为int即完成了从连续坐标到离散网格索引的映射。4.2 胜负判定算法实现checkWin(int row, int col)是五子棋的核心逻辑。我们需要检查刚落子的位置row, col在横、竖、左斜、右斜四个方向上是否存在连续五个同色棋子。private boolean checkWin(int row, int col) { int currentPlayer boardMatrix[row][col]; if (currentPlayer 0) return false; // 定义八个方向向量横(1,0)、竖(0,1)、左斜(1,1)、右斜(1,-1) // 每个方向检查两次正向和反向 int[][] directions {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int[] dir : directions) { int count 1; // 当前位置已经有一颗棋子 // 正向检查 for (int step 1; step 5; step) { int newRow row step * dir[0]; int newCol col step * dir[1]; if (newRow 0 newRow boardRows newCol 0 newCol boardRows boardMatrix[newRow][newCol] currentPlayer) { count; } else { break; } } // 反向检查 for (int step 1; step 5; step) { int newRow row - step * dir[0]; int newCol col - step * dir[1]; if (newRow 0 newRow boardRows newCol 0 newCol boardRows boardMatrix[newRow][newCol] currentPlayer) { count; } else { break; } } // 如果某个方向连续棋子数达到5则获胜 if (count 5) { return true; } } return false; }这个算法效率很高只检查了落子点周围有限的范围。它的思路是以落子点为中心向每个方向的两边延伸计数只要有一个方向上的连续同色棋子数包括中心点本身达到5就判定胜利。注意边界检查newRow 0 newRow boardRows是必不可少的防止数组越界。4.3 在Activity中连接回调现在我们需要在MainActivity中实例化ChessBoardView并设置回调以响应游戏状态的变化比如更新当前回合提示、显示胜负对话框等。public class MainActivity extends AppCompatActivity implements ChessBoardView.GameCallback { private ChessBoardView chessBoardView; private TextView tvStatus; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); chessBoardView findViewById(R.id.chessBoard); tvStatus findViewById(R.id.tv_status); // 需要在布局中添加一个TextView来显示状态 chessBoardView.setGameCallback(this); updateStatusText(); } private void updateStatusText() { // 这里可以获取chessBoardView的当前回合状态简单示例 tvStatus.setText(chessBoardView.isBlackTurn() ? 黑方回合 : 白方回合); } Override public void onPiecePlaced(int player, int row, int col) { // 棋子落下后的处理比如播放音效、记录棋谱等 runOnUiThread(this::updateStatusText); } Override public void onGameOver(int winner) { runOnUiThread(() - { String message winner 1 ? 黑方获胜 : 白方获胜; new AlertDialog.Builder(MainActivity.this) .setTitle(游戏结束) .setMessage(message) .setPositiveButton(再来一局, (dialog, which) - resetGame()) .setNegativeButton(退出, null) .show(); }); } private void resetGame() { chessBoardView.resetGame(); // 需要在ChessBoardView中实现重置方法 updateStatusText(); } }在ChessBoardView中需要添加resetGame()方法和一个获取当前回合的公共方法isBlackTurn()。// 在 ChessBoardView 类中添加 public void resetGame() { // 清空棋盘数据 for (int i 0; i boardRows; i) { for (int j 0; j boardRows; j) { boardMatrix[i][j] 0; } } isBlackTurn true; invalidate(); // 重绘空棋盘 } public boolean isBlackTurn() { return isBlackTurn; }至此一个双人对战的五子棋应用就完成了核心功能。运行后两个玩家可以轮流在屏幕上点击落子当有一方连成五子时游戏会弹出提示。5. 实现简单AI对手从随机到基础策略让人机对战是让这个小游戏变得更有趣的关键。我们来实现一个简单的AI。最基础的AI是随机落子但体验太差。我们稍微升级一下实现一个“攻防兼备”的初级AI。5.1 评估函数与棋型定义AI下棋的核心是评估每个空位的“价值”然后选择价值最高的点落子。我们定义一个简单的评估函数为每种棋型打分。棋型是指一条线上连续的几个位置构成的模式。public class GomokuAI { private int[][] board; private int boardSize; private int aiPlayer; // AI的棋子颜色2白 private int humanPlayer; // 玩家的棋子颜色1黑 // 棋型评分表简化版 private static final int SCORE_FIVE 100000; // 连五 private static final int SCORE_FOUR 10000; // 活四 private static final int SCORE_SFOUR 1000; // 冲四单边被堵 private static final int SCORE_THREE 100; // 活三 private static final int SCORE_STHREE 10; // 眠三 private static final int SCORE_TWO 1; // 活二 public GomokuAI(int[][] board, int aiPlayer) { this.board board; this.boardSize board.length; this.aiPlayer aiPlayer; this.humanPlayer (aiPlayer 1) ? 2 : 1; } // 获取AI的最佳落子位置 public int[] getBestMove() { int bestScore Integer.MIN_VALUE; int bestRow -1, bestCol -1; // 遍历所有空位 for (int i 0; i boardSize; i) { for (int j 0; j boardSize; j) { if (board[i][j] 0) { // 模拟AI在此落子 board[i][j] aiPlayer; int score evaluatePosition(i, j, aiPlayer); // 再模拟玩家在此落子防守视角 board[i][j] humanPlayer; score evaluatePosition(i, j, humanPlayer) * 0.8; // 防守权重略低于进攻 board[i][j] 0; // 恢复空位 if (score bestScore) { bestScore score; bestRow i; bestCol j; } } } } // 如果没找到比如第一步返回棋盘中心附近 if (bestRow -1) { bestRow boardSize / 2; bestCol boardSize / 2; } return new int[]{bestRow, bestCol}; } // 评估某个位置对指定玩家的价值 private int evaluatePosition(int row, int col, int player) { int score 0; int[][] dirs {{1,0},{0,1},{1,1},{1,-1}}; for (int[] dir : dirs) { score evaluateDirection(row, col, dir[0], dir[1], player); } return score; } // 评估一个方向上的棋型 private int evaluateDirection(int row, int col, int dx, int dy, int player) { // 这个函数需要分析以(row,col)为中心在(dx,dy)方向上的连续棋子情况 // 由于实现稍长这里简述逻辑分别向两个方向扫描统计连续的同色棋子数、空位数、被堵情况。 // 根据扫描结果判断是“活四”、“冲四”、“活三”等棋型并返回对应分数。 // 这是一个简化实现实际完整的棋型判断代码有几十行。 // 为了项目可运行我们可以先实现一个超简版只统计连续棋子数。 int count 1; // 中心点 // 正向 for (int i 1; i 5; i) { int r row dx * i; int c col dy * i; if (r0 || rboardSize || c0 || cboardSize || board[r][c] ! player) break; count; } // 反向 for (int i 1; i 5; i) { int r row - dx * i; int c col - dy * i; if (r0 || rboardSize || c0 || cboardSize || board[r][c] ! player) break; count; } // 超简版评分连续棋子数越多分数越高 switch (count) { case 5: return SCORE_FIVE; case 4: return SCORE_FOUR; case 3: return SCORE_THREE; case 2: return SCORE_TWO; default: return 0; } } }这个AI类接收当前棋盘状态和AI的棋子颜色。getBestMove方法遍历所有空位对每个空位进行“攻防双向评估”先假设AI自己下在这里能得多少分进攻再假设对手下在这里会对自己造成多大威胁防守将两者加权求和作为该点的总价值。最后选择价值最高的点落子。evaluateDirection函数是核心完整的实现需要判断“活三”两头无阻挡、“冲四”一头被堵等复杂棋型。上面是一个极度简化的版本只统计了连续棋子数。一个真正可用的评估函数代码量会大很多你可以将其作为后续的优化方向。5.2 整合AI到游戏循环在ChessBoardView中我们需要修改游戏模式并添加AI落子的逻辑。public class ChessBoardView extends View { // ... 之前变量 ... private boolean isAIMode false; // 是否为人机对战模式 private GomokuAI ai; private int aiPlayer 2; // 假设AI执白 // 设置游戏模式 public void setGameMode(boolean isAIMode) { this.isAIMode isAIMode; if (isAIMode) { ai new GomokuAI(boardMatrix, aiPlayer); // 如果AI先手则AI先下一步 if (!isBlackTurn aiPlayer 2) { // 白棋先手且是AI postDelayed(this::aiMove, 500); // 延迟500毫秒模拟思考 } } } // AI走棋 private void aiMove() { if (!isAIMode || (isBlackTurn aiPlayer 1) || (!isBlackTurn aiPlayer 2)) { // 不是AI模式或者当前不是AI的回合 return; } int[] move ai.getBestMove(); if (move ! null boardMatrix[move[0]][move[1]] 0) { boardMatrix[move[0]][move[1]] aiPlayer; if (gameCallback ! null) { gameCallback.onPiecePlaced(aiPlayer, move[0], move[1]); } if (checkWin(move[0], move[1])) { if (gameCallback ! null) { gameCallback.onGameOver(aiPlayer); } return; } isBlackTurn !isBlackTurn; invalidate(); } } // 修改 onTouchEvent在人机模式下玩家落子后触发AI思考 Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN) { // ... 坐标转换和有效性检查 ... if (isAIMode ((isBlackTurn aiPlayer 2) || (!isBlackTurn aiPlayer 1))) { // 人机模式下且当前是AI的回合忽略玩家触摸 return false; } // ... 玩家落子逻辑 ... // 玩家落子后重绘然后如果是人机模式且游戏未结束则触发AI invalidate(); if (isAIMode !checkWin(row, col)) { postDelayed(this::aiMove, 500); // 延迟一下让玩家看到自己的棋子 } return true; } return super.onTouchEvent(event); } }在MainActivity中我们可以通过一个按钮来切换游戏模式。// 在MainActivity的布局中添加一个按钮 // 在onCreate中 Button btnMode findViewById(R.id.btn_mode); btnMode.setOnClickListener(v - { // 切换模式 chessBoardView.setGameMode(true); // 切换为人机模式 chessBoardView.resetGame(); // 更新按钮文字等 });现在运行应用切换到人机模式你就可以和这个“初级AI”对战了。虽然它的棋力很弱因为评估函数太简单但已经具备了最基本的攻防意识不会乱下。6. 功能完善与体验优化让游戏更完整基础功能完成后我们可以添加一些特性让游戏更像一个真正的产品。6.1 悔棋功能悔棋是棋类游戏的标配。我们需要一个栈Stack来记录每一步的落子位置。public class ChessBoardView extends View { // ... 之前变量 ... private Stackint[] moveHistory new Stack(); // 记录每一步 [row, col, player] // 在落子成功的地方onTouchEvent和aiMove中将步骤存入历史 // boardMatrix[row][col] player; // moveHistory.push(new int[]{row, col, player}); // 悔棋方法 public boolean undo() { if (moveHistory.isEmpty()) { return false; } int[] lastMove moveHistory.pop(); int row lastMove[0]; int col lastMove[1]; boardMatrix[row][col] 0; // 清空该位置 isBlackTurn !isBlackTurn; // 回合回退 invalidate(); if (gameCallback ! null) { gameCallback.onUndo(); // 通知Activity更新状态 } return true; } }在Activity中添加一个悔棋按钮调用chessBoardView.undo()即可。注意在人机模式下悔棋可能需要连续调用两次撤销AI的一步和玩家的一步逻辑会更复杂一些。6.2 游戏状态持久化防止游戏被意外中断我们可以简单保存棋盘状态。使用SharedPreferences或直接序列化boardMatrix数组。// 在ChessBoardView中 public void saveGameState(Context context) { SharedPreferences prefs context.getSharedPreferences(GomokuPrefs, Context.MODE_PRIVATE); SharedPreferences.Editor editor prefs.edit(); // 将二维数组扁平化为字符串存储例如“1,2,0,1,...” StringBuilder sb new StringBuilder(); for (int i 0; i boardRows; i) { for (int j 0; j boardRows; j) { sb.append(boardMatrix[i][j]).append(,); } } editor.putString(board_state, sb.toString()); editor.putBoolean(is_black_turn, isBlackTurn); editor.putBoolean(is_ai_mode, isAIMode); editor.apply(); } public void loadGameState(Context context) { SharedPreferences prefs context.getSharedPreferences(GomokuPrefs, Context.MODE_PRIVATE); String savedState prefs.getString(board_state, ); if (!savedState.isEmpty()) { String[] values savedState.split(,); int index 0; for (int i 0; i boardRows; i) { for (int j 0; j boardRows; j) { if (index values.length) { boardMatrix[i][j] Integer.parseInt(values[index]); } } } isBlackTurn prefs.getBoolean(is_black_turn, true); isAIMode prefs.getBoolean(is_ai_mode, false); invalidate(); } }在MainActivity的onPause()中调用saveGameState在onCreate()中调用loadGameState就可以实现简单的断点续玩。6.3 音效与视觉反馈为落子和胜负添加音效能让体验提升一个档次。在res/raw/目录下放入音效文件如place.mp3,win.mp3。// 在ChessBoardView或Activity中 private void playSound(Context context, int soundResId) { MediaPlayer mediaPlayer MediaPlayer.create(context, soundResId); mediaPlayer.setOnCompletionListener(MediaPlayer::release); mediaPlayer.start(); } // 在落子和游戏结束时调用视觉上可以在落子时添加一个简单的缩放动画或者在获胜时高亮显示连成五子的那排棋子。这需要更复杂的自定义绘制逻辑比如在onDraw中根据获胜坐标数组用高亮颜色的画笔再画一遍线。7. 项目总结与扩展思考走完以上所有步骤一个功能相对完整的Android五子棋应用就诞生了。回顾这个项目它几乎涵盖了Android应用开发入门阶段需要掌握的大部分核心技能从环境搭建、UI自定义绘制、事件处理、到基础算法胜负判断、AI评估、数据持久化、多媒体交互等。在这个过程中最容易出问题的往往是环境配置和坐标转换这些“脏活累活”。比如Gradle下载慢、模拟器启动失败、触摸坐标计算偏差导致落子不准。我的经验是环境问题优先考虑换源和真机调试坐标问题一定要在onDraw里把计算用的关键坐标点如offset,cellWidth用canvas.drawCircle画出来看看它们到底在哪这是最直接的调试方法。这个项目还有很多可以深入和扩展的方向AI强化目前的AI非常弱。你可以研究Minimax算法极大极小值算法配合Alpha-Beta剪枝这是棋类AI的经典方法。网上有很多五子棋AI的开源实现可以参考其评估函数。网络对战使用Socket或WebSocket实现双人在线对战。这涉及到网络编程、数据同步、房间管理等更复杂的知识。UI美化使用更精美的棋盘和棋子图片替代纯色绘制加入更流畅的动画效果。游戏功能加入禁手规则针对专业五子棋、棋谱记录与回放、难度选择等。对于初学者来说不要一开始就追求把所有功能做全。我的建议是先把这个基础版本完完整整地自己敲一遍确保每一步都理解。遇到问题善用Logcat输出调试信息善用搜索引擎和官方文档。当你看到自己写的程序能跑起来并且能和你“对战”时那种成就感是看再多教程都无法替代的。这个项目就像一个骨架后续的血肉更优的AI、更好的UI、更多功能可以由你根据自己的兴趣和时间一点点添加上去。本文还有配套的精品资源点击获取