资讯动态

Android五子棋开发实战:从Canvas绘制到AI算法实现

发布时间:2026/9/5 12:13:25 来源:尧图企业网站定制
简介这是一份面向Android初学者与进阶开发者的五子棋实战项目资源聚焦Android Studio环境下的UI布局、事件响应与游戏逻辑实现帮助学习者系统掌握Activity生命周期、自定义View绘制、棋局状态管理及胜负判定算法等核心技能。压缩包共49个文件含17个XML布局与配置文件如activity_main.xml、manifest、15个PNG/JPG棋子与界面素材、4个Kotlin源码文件含MainActivity及ChessBoard等核心逻辑类、3个Gradle构建脚本整体877KB结构清晰适配AS最新稳定版本。已有2304人学习下载资源完整包含可直接运行的工程目录含.gitignore、gradlew、proguard规则等无需额外配置即可导入编译代码注释详实胜负判断逻辑覆盖横竖斜五连检测辅以轮次提示与重新开始功能是理解Android游戏开发全流程的优质入门范例。1. 项目概述从零到一用Android Studio构建一个完整的五子棋应用最近在整理自己的项目库翻到了一个几年前用Android Studio做的五子棋应用。这个项目虽然不大但麻雀虽小五脏俱全从界面绘制、触摸交互、游戏逻辑到AI算法几乎涵盖了Android原生开发中UI、事件处理、数据结构和算法等多个核心知识点。对于想从“Hello World”迈向一个完整功能应用的Android新手来说它是一个绝佳的练手项目。今天我就把这个项目的核心实现思路、关键代码以及我踩过的那些坑系统地梳理一遍希望能给你带来一些启发。这个五子棋应用的核心目标很明确实现一个双人对战或人机对战的棋盘游戏。用户能在手机屏幕上看到清晰的棋盘格子和黑白棋子通过触摸来落子系统能实时判断胜负并提供重新开始等功能。听起来简单但要把每个环节都做得流畅、健壮里面有不少门道。我们不仅要用Canvas和Paint在View上画出棋盘和棋子还要精准处理用户的触摸事件将屏幕坐标映射到棋盘逻辑坐标并实现一个高效且无bug的胜负判定算法。如果还想挑战一下加入一个简单的AI对手那又会涉及到搜索算法和评估函数的设计。2. 核心需求与功能模块拆解在动手写代码之前我们必须把需求想清楚把功能模块划分好。一个结构清晰的项目是成功的一半。对于这个五子棋应用我们可以将其分解为以下几个核心模块。2.1 游戏状态与数据模型Model这是游戏的大脑负责存储一切与游戏逻辑相关的数据并且不依赖于界面View。它的核心是一个二维数组比如int[][] board用来表示15x15标准五子棋棋盘的每个交叉点状态。我们可以用0表示空位1表示黑子2表示白子。除了棋盘数据Model还需要管理当前游戏状态比如当前该谁落子黑方还是白方、游戏是否已经结束以及谁是赢家、是否是人机对战模式等。将数据与界面分离是保证代码可维护性和可测试性的关键。后续如果你想更换UI比如用更炫酷的渲染方式或者为游戏增加网络对战功能一个独立的Model层会让这些扩展变得容易得多。2.2 游戏界面绘制View这部分负责将Model中的数据“画”到屏幕上。在Android中我们通常会自定义一个View或SurfaceView。在onDraw(Canvas canvas)方法里我们需要完成两件事绘制棋盘计算好棋盘左上角起点、每个格子的宽度cellSize然后用canvas.drawLine画出横线和竖线。绘制棋子遍历board数组对于每一个非空的位置根据其值是1还是2在对应的交叉点坐标上用canvas.drawCircle画一个黑色或白色的实心圆。这里的关键在于坐标转换。我们需要建立一个从“棋盘逻辑坐标行i, 列j”到“屏幕绘制坐标像素x, y”的映射关系。公式通常是x startX j * cellSizey startY i * cellSize。startX和startY是棋盘左上角在View中的坐标需要根据View的尺寸和棋盘大小计算出来以保证棋盘居中。2.3 用户交互处理Controller用户通过触摸屏幕来落子。我们需要在自定义View的onTouchEvent(MotionEvent event)方法中处理触摸事件。处理流程如下获取触摸点的屏幕坐标(eventX, eventY)。将这个屏幕坐标反向转换为棋盘逻辑坐标。这是整个交互中最容易出错的一步。算法是j Math.round((eventX - startX) / cellSize)i Math.round((eventY - startY) / cellSize)。这里用Math.round进行四舍五入意味着用户触摸点只要落在某个交叉点附近的一定范围内通常是半个格子内就会被吸附到该交叉点。这比判断触摸点是否在某个圆形区域内要简单且符合直觉。转换后必须检查坐标(i, j)是否在棋盘有效范围内0到14之间并且该位置是否为空board[i][j] 0。如果检查通过则在Model的board[i][j]处落下当前玩家的棋子。调用checkWin(i, j)函数判断这步棋是否导致了胜利。如果未获胜则切换当前玩家黑变白或白变黑。如果是人机对战模式在切换为AI玩家后需要触发AI计算并落子。最后调用invalidate()方法请求重绘界面就会更新显示出新落的棋子。2.4 胜负判定逻辑这是游戏逻辑的核心算法。最直观的想法是每当落下一颗子就以这个子为中心向四个方向水平、垂直、左上到右下、右上到左下进行搜索统计连续的同色棋子数量。如果任意一个方向上的连续数量达到5则判定当前玩家获胜。算法细节与优化方向向量我们可以定义四个方向向量(0, 1)水平向右(1, 0)垂直向下(1, 1)右下(1, -1)左下。对于每个方向分别向正反两个方向搜索。边界处理在搜索时每次移动都要判断新坐标是否还在棋盘范围内。计数从落子点开始向一个方向遍历遇到同色棋子则计数加1遇到不同色或空位则停止。然后向相反方向做同样操作。两个方向的计数之和再加上自身的1就是该方向上的连续总数。效率这个算法只检查最新落子点的影响时间复杂度是O(1)因为方向固定搜索步数不超过4步非常高效。绝对不要每次判定都全盘扫描那将是性能灾难。2.5 可选简单AI实现如果想让游戏更有挑战性可以加入一个AI对手。对于五子棋一个简单但有效的AI可以通过“极大极小值搜索”配合“Alpha-Beta剪枝”来实现但这对于初学者可能有些复杂。一个更入门级的AI可以采用以下思路评估函数设计一个函数为棋盘上每一个空位打分。分数基于如果在此落子能形成多么“好”的棋型。例如成五已经赢了极高分数直接选择。活四两头无阻挡的四子连线次高分数。冲四一头被堵的四子较高分数。活三、死四、活二等依次递减。贪心算法AI回合时遍历所有空位用评估函数计算每个位置的得分然后选择得分最高的位置落子。这相当于一个“只思考一步”的AI。防守优先可以在评估函数中不仅计算自己在此落子能形成的棋型也计算对手在此落子会形成的棋型。如果发现对手有活四、冲四等威胁优先进行堵截。虽然这个贪心AI很弱但实现起来简单能帮助理解AI的基本工作流程。想提升AI强度就必须引入多层搜索和更复杂的评估。3. 关键实现细节与代码剖析理论说完了我们进入实战环节。我会用代码片段来展示几个最关键部分的实现并解释其中的注意事项。3.1 自定义棋盘View的实现我们创建一个GobangView继承自View。首先定义一些成员变量public class GobangView extends View { private int rows 15; // 棋盘行数 private int cols 15; // 棋盘列数 private int[][] board; // 棋盘数据0空1黑2白 private int currentPlayer 1; // 当前玩家1黑2白 private boolean gameOver false; private int winner 0; private Paint gridPaint; // 画棋盘的笔 private Paint blackPiecePaint; // 画黑子的笔 private Paint whitePiecePaint; // 画白子的笔 private Paint highlightPaint; // 高亮显示最后落子的笔 private float cellSize; // 每个格子的像素大小 private float startX, startY; // 棋盘左上角起点坐标 private int lastMoveRow -1, lastMoveCol -1; // 记录最后落子位置 // ... 构造函数中初始化Paint等 ... }在onSizeChanged或onDraw中计算棋盘位置和格子大小这是一个关键步骤要保证棋盘在各种屏幕尺寸下都能正确居中。Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 计算格子大小取宽高中较小值减去一些边距再除以格子数-1因为线是画在交叉点上。 int minSize Math.min(w, h); float margin 50; // 边距 float boardSize minSize - 2 * margin; cellSize boardSize / (rows - 1); // 计算起点使棋盘居中 startX (w - (cols - 1) * cellSize) / 2; startY (h - (rows - 1) * cellSize) / 2; }onDraw方法负责绘制Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBoard(canvas); drawPieces(canvas); if (lastMoveRow ! -1) { drawLastMoveHighlight(canvas); } } private void drawBoard(Canvas canvas) { // 画横线 for (int i 0; i rows; i) { float y startY i * cellSize; canvas.drawLine(startX, y, startX (cols - 1) * cellSize, y, gridPaint); } // 画竖线 for (int j 0; j cols; j) { float x startX j * cellSize; canvas.drawLine(x, startY, x, startY (rows - 1) * cellSize, gridPaint); } } private void drawPieces(Canvas canvas) { float radius cellSize * 0.4f; // 棋子半径略小于格子一半 for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (board[i][j] ! 0) { float cx startX j * cellSize; float cy startY i * cellSize; if (board[i][j] 1) { canvas.drawCircle(cx, cy, radius, blackPiecePaint); } else { canvas.drawCircle(cx, cy, radius, whitePiecePaint); } } } } }注意drawPieces中遍历了整个棋盘数组。在15x15的规模下这完全没问题。但如果棋盘很大且需要频繁重绘可以考虑只绘制发生变化的部分或者使用更底层的SurfaceView配合子线程绘制。3.2 触摸事件与落子逻辑触摸事件处理是连接用户与游戏模型的桥梁。Override public boolean onTouchEvent(MotionEvent event) { if (gameOver || event.getAction() ! MotionEvent.ACTION_DOWN) { return super.onTouchEvent(event); } float touchX event.getX(); float touchY event.getY(); // 1. 将触摸坐标转换为棋盘数组下标 int col Math.round((touchX - startX) / cellSize); int row Math.round((touchY - startY) / cellSize); // 2. 检查有效性 if (row 0 || row rows || col 0 || col cols) { return true; // 触摸点在棋盘外 } if (board[row][col] ! 0) { return true; // 该位置已有棋子 } // 3. 执行落子 board[row][col] currentPlayer; lastMoveRow row; lastMoveCol col; // 4. 检查胜负 if (checkWin(row, col)) { gameOver true; winner currentPlayer; // 可以在这里触发游戏结束的对话框或动画 Log.d(Gobang, Player winner wins!); } else { // 5. 切换玩家 currentPlayer (currentPlayer 1) ? 2 : 1; // 如果是人机对战且当前是AI回合可以在这里触发AI计算 // if (isAIMode currentPlayer aiPlayer) { // new AITask().execute(); // } } // 6. 请求重绘 invalidate(); return true; }实操心得坐标转换时的Math.round是关键。它提供了“吸附”效果让用户体验更好。你也可以尝试用Math.floor或Math.ceil但round是最符合直觉的。务必在转换后立即进行有效性检查这是避免数组越界异常和逻辑错误的重要防线。3.3 胜负判定算法的实现下面是一个清晰且高效的checkWin实现private boolean checkWin(int row, int col) { int player board[row][col]; if (player 0) return false; // 四个方向向量水平、垂直、左上到右下、右上到左下 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], player); // 向反方向搜索 count countDirection(row, col, -dir[0], -dir[1], player); if (count 5) { return true; } } return false; } private int countDirection(int row, int col, int dRow, int dCol, int player) { int count 0; int r row dRow; int c col dCol; while (r 0 r rows c 0 c cols board[r][c] player) { count; r dRow; c dCol; } return count; }这个算法的优点在于局部性和高效性。它只检查受最新落子影响的四条线而不是扫描整个棋盘。countDirection函数封装了向某个方向计数的逻辑代码非常清晰。4. 项目搭建与开发环境配置要点虽然项目核心在逻辑但一个顺畅的开发环境是基础。这里针对Android Studio的使用分享几个新手容易卡住的地方。4.1 Android Studio的安装与初始设置现在安装Android Studio已经非常傻瓜化官网下载安装包一路Next即可。但有几点需要注意SDK路径安装时建议将Android SDK放在一个没有中文和空格的路径下比如D:\Android\Sdk。这可以避免后续Gradle构建时可能出现的各种诡异问题。代理问题首次启动时Android Studio会尝试下载Gradle和组件。如果你在网络环境受限的地方可能会一直卡在“正在下载Gradle...”或“正在获取组件信息”。这时需要在File-Settings-Appearance Behavior-System Settings-HTTP Proxy中设置正确的网络代理。或者更一劳永逸的方法是查阅如何配置国内镜像源如阿里云镜像大幅提升下载速度。创建项目选择Empty Views Activity模板即可。Minimum SDK建议选择API 24: Android 7.0或更高以覆盖足够多的用户同时能使用较新的API。4.2 关键依赖与权限我们这个五子棋应用是纯本地单机游戏不需要网络权限和任何第三方库非常干净。但如果你想加入声音、振动或者将棋谱保存到文件就需要在AndroidManifest.xml中添加相应权限。!-- 如果需要保存棋谱到外部存储Android 10及以上需要适配作用域存储 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- 仅针对旧版本 -- !-- 如果需要振动反馈 -- uses-permission android:nameandroid.permission.VIBRATE /对于简单的文件操作更推荐使用应用的内部私有目录无需权限。4.3 布局与Activity集成我们的GobangView需要放在Activity的布局中。在activity_main.xml里可以这样写?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:gravitycenter TextView android:idid/tv_status android:layout_widthwrap_content android:layout_heightwrap_content android:text黑方回合 android:textSize20sp android:layout_margin16dp/ com.yourpackage.GobangView android:idid/gobang_view android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1/ Button android:idid/btn_restart android:layout_widthwrap_content android:layout_heightwrap_content android:text重新开始 android:layout_margin16dp/ /LinearLayout在MainActivity中初始化View并设置按钮监听器public class MainActivity extends AppCompatActivity { private GobangView gobangView; private TextView tvStatus; private Button btnRestart; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); gobangView findViewById(R.id.gobang_view); tvStatus findViewById(R.id.tv_status); btnRestart findViewById(R.id.btn_restart); // 可以传递一个回调给GobangView用于更新状态文本 gobangView.setGameCallback(new GobangView.GameCallback() { Override public void onPlayerChanged(int player) { String text (player 1) ? 黑方回合 : 白方回合; tvStatus.setText(text); } Override public void onGameOver(int winner) { String text (winner 1) ? 游戏结束黑方胜 : 游戏结束白方胜; tvStatus.setText(text); } }); btnRestart.setOnClickListener(v - gobangView.restartGame()); } }这样一个基本的游戏框架就搭建起来了。GobangView需要暴露restartGame()方法用于重置棋盘以及提供一个设置回调的接口。5. 进阶功能与性能优化思考基础功能实现后我们可以考虑一些增强体验和代码质量的点。5.1 实现一个简易的贪心AI我们来实现前面提到的“一步棋”贪心AI。首先我们需要一个评估函数为单个空位打分。这里实现一个极度简化的版本只检查该位置落子后在四个方向上能形成的最长连续长度。// 在GobangView或一个单独的AI类中 private int evaluatePosition(int row, int col, int player) { // 假设在此处落子 board[row][col] player; int maxLength 0; int[][] dirs {{0,1},{1,0},{1,1},{1,-1}}; for (int[] d : dirs) { int len 1 countDirection(row, col, d[0], d[1], player) countDirection(row, col, -d[0], -d[1], player); if (len maxLength) { maxLength len; } } // 撤销假设的落子 board[row][col] 0; return maxLength; // 分数就是最长连续长度 }然后在AI回合时遍历所有空位找到评估分数最高的位置落子。为了增加一点智能我们可以让AI不仅考虑自己的进攻evaluatePosition(空位, aiPlayer)也考虑对手的威胁evaluatePosition(空位, humanPlayer)并给予防守一定的权重。public void aiMove() { int bestScore -1; int bestRow -1, bestCol -1; for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (board[i][j] 0) { // 进攻分数 int attackScore evaluatePosition(i, j, aiPlayer); // 防守分数评估如果对手下这里的威胁 int defendScore evaluatePosition(i, j, humanPlayer); // 综合分数可以给防守更高的权重比如防守分数*1.2 int totalScore attackScore (int)(defendScore * 1.2); if (totalScore bestScore) { bestScore totalScore; bestRow i; bestCol j; } } } } if (bestRow ! -1) { // 执行AI落子 board[bestRow][bestCol] aiPlayer; lastMoveRow bestRow; lastMoveCol bestCol; // ... 检查胜负、切换玩家、重绘 ... } }这个AI非常弱但足以让新手玩家感到一点挑战。要让它变强必须引入递归搜索极大极小算法和更精细的棋型评估识别活三、冲四、双三等。5.2 性能优化与体验提升避免在onDraw中创建对象Paint、Path等对象应该在View初始化时创建并复用。在onDraw中频繁创建对象会引发GC导致界面卡顿。局部刷新如果只更新了一颗棋子调用invalidate()会重绘整个View。我们可以使用invalidate(Rect dirtyRect)指定需要重绘的脏矩形区域只更新该区域提升性能。对于五子棋可以计算新落子影响的区域棋子所在圆的外接矩形进行局部刷新。使用SurfaceView如果游戏动画复杂比如有棋子落下的动画、胜利连线的闪烁效果View的onDraw在主线程执行可能会阻塞UI。这时可以考虑使用SurfaceView它拥有独立的绘制线程更适合频繁刷新的游戏场景。状态保存与恢复在Activity被系统销毁重建如屏幕旋转时需要保存游戏状态。可以在GobangView中实现onSaveInstanceState和onRestoreInstanceState将board数组、currentPlayer等数据存入Bundle。5.3 功能扩展方向游戏模式除了双人对战、人机对战还可以实现局域网联机对战利用Socket或蓝牙。悔棋功能使用一个栈Stack来记录每一步的落子位置。悔棋时从栈中弹出上一步并清空对应棋盘位置。棋谱记录与回放将每一步棋行、列、玩家按顺序保存到文件或数据库。回放时按顺序定时重演这些步骤。难度选择实现不同搜索深度的AI。贪心算法是深度1极大极小算法可以设置深度为2、3、4等。深度越深AI越强但计算时间也呈指数级增长可能需要放在后台线程。音效与动画为落子、胜利等事件添加音效。为棋子落下添加缩放动画为胜利连线添加高亮动画。6. 开发中常见问题与调试技巧即使思路清晰实际编码中还是会遇到各种问题。这里记录几个我当年遇到的典型“坑”。6.1 触摸不灵敏或落子位置偏差问题描述点击屏幕边缘的格子时落子没反应或者棋子画在了奇怪的位置。排查思路检查坐标转换计算在onTouchEvent中将计算出的row和col打印出来Log.d对比触摸点的真实像素坐标和计算出的逻辑坐标。重点检查startX、startY和cellSize的计算是否正确。确保onSizeChanged中的计算逻辑无误。检查棋盘绘制区域在onDraw中用不同颜色的笔画出棋盘的边界矩形看看它是否在View中正确居中大小是否符合预期。吸附范围Math.round是四舍五入到最近的整数。这意味着触摸点必须离交叉点足够近半个格子以内才能被吸附。如果你希望吸附范围更大可以自己实现一个阈值判断比如if (distance cellSize * 0.4) then ...。6.2 胜负判定出现误判或漏判问题描述明明连成五子了却没赢或者没连成五子却判赢了。排查思路单元测试单独写一个测试函数构造特定的棋盘局面比如一行黑子然后调用checkWin函数断言结果应为true。这是最有效的调试逻辑算法的方法。打印调试在checkWin函数中打印出四个方向搜索得到的count值。看看是哪个方向的计算出了问题。边界条件重点测试棋盘边缘的胜负情况。例如在棋盘最右边落子形成横向五连算法是否因为向右搜索时索引越界而提前终止确保countDirection函数中的循环条件r 0 r rows c 0 c cols是正确的。棋子状态混淆确认checkWin函数中用于比对的player变量是来自board[row][col]的最新值并且这个值是正确的1或2。6.3 界面刷新问题棋子不显示或闪烁问题描述落子后棋子没有立刻显示或者界面出现奇怪的闪烁。排查思路确认invalidate()被调用在落子逻辑的最后确保调用了invalidate()或postInvalidate()如果在非UI线程来请求重绘。检查onDraw逻辑在drawPieces中遍历board数组时确认你使用的是更新后的数组。确保board数组是成员变量并且在落子时被正确修改。避免背景色覆盖检查自定义View的背景色。如果你在onDraw中先画了一个全屏的背景色然后又画棋盘通常没问题。但如果有重叠绘制顺序问题可能会导致闪烁。可以尝试在onDraw开始时调用super.onDraw(canvas)让系统先处理默认的背景绘制。SurfaceView的双缓冲如果使用SurfaceView闪烁可能是由于直接在显示的前台缓冲区绘制造成的。确保你是在SurfaceHolder.lockCanvas()获得的Canvas上绘制并通过SurfaceHolder.unlockCanvasAndPost()来提交。6.4 Android Studio相关配置与构建问题Gradle构建慢这是最常见的问题。解决方法包括使用国内镜像源、开启Gradle离线模式对于稳定项目、升级Gradle和Android Gradle Plugin到较新版本、增加Gradle堆内存在gradle.properties中设置org.gradle.jvmargs-Xmx4096m。模拟器无法启动/卡顿确保在BIOS中开启了CPU虚拟化支持Intel VT-x或AMD-V。在Android Studio的SDK Manager中检查并安装Intel x86 Emulator Accelerator (HAXM installer)。如果电脑不支持Hyper-V如某些Windows 10/11家庭版HAXM是必须的。也可以考虑使用真机调试速度更快。代码提示慢或无提示尝试File-Invalidate Caches / Restart...。检查项目是否正在索引Android Studio右下角有进度条等待其完成。这个五子棋项目就像一把瑞士军刀虽然小但能帮你练习Android开发的多个核心技能。从画布绘制到事件处理从算法实现到简单的AI设计每一步都充满了学习的乐趣。我最开始做的时候胜负判定算法写错了导致斜向的胜利总是不被承认调试了半天才发现方向向量设反了。这种自己发现问题并解决的过程比单纯看教程要深刻得多。如果你在做的时候加入了AI并且尝试优化它的评估函数你会发现让自己写的程序变得“聪明”一点是一件非常有成就感的事。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价