
简介这是一份面向计算机科学与技术专业学生及安卓开发初学者的实战项目资源基于Android Studio实现功能完整的五子棋小游戏覆盖人机对战含评分决策AI与人人对战双模式支持悔棋、新局重开、背景音乐开关及游戏规则介绍等交互功能有效解决移动应用开发课程设计与课程实训中缺乏可运行、可拓展GUI项目案例的问题。压缩包共657个文件包含27个核心Java源码、39个XML布局与配置文件、45个PNG图标资源、124个JSON配置及Gradle构建相关文件整体体积30.71MB结构清晰、模块分离明确便于理解MVC架构与Android事件响应机制。已有2089人学习下载资源附带可直接安装的APK、完整工程目录及音效资源mp3无需额外配置即可编译运行是掌握安卓基础控件、Canvas绘图、二维数组逻辑判断与简单AI策略的优质入门范例。1. 项目概述从棋盘到代码一个经典游戏的安卓实现五子棋这个规则简单却蕴含无限策略的古老游戏几乎是每个程序员在学习图形界面和游戏逻辑时都会尝试的练手项目。但把它搬到安卓平台上用Android Studio来实现就不仅仅是写个算法那么简单了。这背后涉及到安卓应用开发的全套流程从UI布局的绘制、触摸事件的精准处理到游戏核心逻辑的封装、胜负判断算法的优化再到最终打包成一个可以安装在手机上的APK文件。对于计算机专业的学生尤其是正在学习《安卓程序设计》这门课的同学们来说亲手实现一个五子棋小游戏是一次绝佳的综合性实践。它能让你把课堂上分散的知识点——Activity生命周期、View的自定义绘制、事件监听、数据模型——像拼图一样完整地串联起来。我当年做课程设计时也选了这个题目踩过不少坑也积累了一些让代码更优雅、逻辑更清晰的心得。这篇文章我就以一个过来人的身份带你从零开始用Android Studio和Java语言一步步构建一个功能完整、界面美观的五子棋游戏。我们不止于实现功能更会探讨如何写出“优秀”的、易于维护和扩展的代码结构这正是“计科必看”的价值所在。2. 项目整体设计与架构思路在动手写第一行代码之前花点时间思考整体架构是至关重要的。一个混乱的项目结构会让你在后期添加功能比如悔棋、难度选择、联网对战时举步维艰。我推崇的是模型-视图-控制器MVC的变体在安卓中的实践虽然安卓本身更倾向于MVVM但对于这个规模的游戏清晰的MVC分离足以带来巨大好处。2.1 核心模块划分我的设计将项目分为三个核心层数据模型层Model这是游戏的大脑纯粹处理数据逻辑。它完全独立于安卓的API。核心是一个GobangGame类五子棋英文常用Gobang内部维护一个15x15的二维整型数组board用0代表空位1代表黑棋2代表白棋。这个类负责所有核心操作落子需检查位置是否合法、是否已有棋子、判断胜负每次落子后从该点向八个方向扫描、切换当前玩家。它不应该知道任何关于界面绘制的事情。视图层View这是游戏的脸面负责一切用户能看到的东西。核心是一个自定义View——ChessBoardView。它继承自View或SurfaceView。对于五子棋这种刷新不频繁的绘制继承View并重写onDraw方法完全够用也更简单。它的职责包括绘制棋盘背景、网格线、所有已落的棋子。它持有对GobangGame模型的一个引用以便获取棋盘数据来绘制。控制层Controller这是连接大脑和脸面的神经。在我们的设计中这个角色由MainActivity和自定义View的事件监听共同承担。MainActivity负责初始化游戏模型和视图而ChessBoardView通过重写onTouchEvent方法来接收用户的触摸事件。当用户点击屏幕时View将触摸坐标转换为棋盘上的行列索引然后调用GobangGame模型的placePiece方法。模型执行业务逻辑落子、判断胜负并更新内部状态然后通知View重绘调用invalidate()。同时控制器还需要更新UI上的状态信息比如当前轮到谁、游戏是否结束等。为什么选择这种架构最大的好处是解耦。假设你后来想增加一个AI对手你只需要修改或替换GobangGame模型让它能在收到玩家落子后自动计算AI落子位置。视图层完全不用动。或者你想把棋盘从15路改成19路也只需修改模型和视图的尺寸常量逻辑清晰不易出错。2.2 技术选型与工具准备开发环境Android Studio这是谷歌官方的IDE对安卓开发的支持最完善。建议使用较新的稳定版本以确保Gradle构建过程的顺畅。语言Java。虽然Kotlin现在是安卓开发的首选但很多高校课程仍以Java为主且其语法对于理解面向对象和游戏逻辑更为直观。本文代码示例将使用Java。最小SDK版本建议设置为API 24Android 7.0或以上这样可以避免处理过多低版本兼容性问题也能使用更多现代API。关键依赖这是一个纯原生开发项目不需要引入任何第三方网络或图形库。我们完全依靠安卓SDK自带的Canvas和Paint进行绘制。实操心得在创建新项目时选择“Empty Activity”模板即可。建议立即将项目名称和包名设置好例如com.yourname.gobang。避免使用中文或特殊字符。创建后第一件事是检查build.gradle文件确保没有不必要的依赖保持项目简洁。3. 核心实现细节拆解接下来我们深入每个模块看看代码具体怎么写以及为什么要这么写。3.1 数据模型层GobangGame.java的实现这是项目的基石必须健壮且高效。public class GobangGame { public static final int BOARD_SIZE 15; public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2; private int[][] board; // 棋盘数据 private int currentPlayer; // 当前行棋方 private boolean gameOver; private int winner; public GobangGame() { board new int[BOARD_SIZE][BOARD_SIZE]; currentPlayer BLACK; // 黑棋先行 gameOver false; winner EMPTY; // 初始化棋盘为空 for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { board[i][j] EMPTY; } } } /** * 在指定位置落子 * param row 行 (0-14) * param col 列 (0-14) * return true 表示落子成功并切换玩家false 表示位置无效或游戏已结束 */ public boolean placePiece(int row, int col) { if (gameOver || !isValidPosition(row, col) || board[row][col] ! EMPTY) { return false; } board[row][col] currentPlayer; // 判断是否获胜 if (checkWin(row, col)) { gameOver true; winner currentPlayer; return true; // 落子成功但游戏结束 } // 切换玩家 currentPlayer (currentPlayer BLACK) ? WHITE : BLACK; return true; } private boolean isValidPosition(int row, int col) { return row 0 row BOARD_SIZE col 0 col BOARD_SIZE; } /** * 核心胜负判断算法 * 以刚落子的点(row, col)为中心向四个方向横、竖、左斜、右斜扫描 * 每个方向检查是否有连续5个同色棋子 */ private boolean checkWin(int row, int col) { int player board[row][col]; // 四个方向向量: (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; // 刚落下的这颗子已经算一个 // 向正方向检查 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; } /** * 向指定方向(dx, dy)连续查找同色棋子个数 */ private int countDirection(int row, int col, int dx, int dy, int player) { int count 0; int r row dx; int c col dy; while (isValidPosition(r, c) board[r][c] player) { count; r dx; c dy; } return count; } // Getter 方法 public int[][] getBoard() { return board; } public int getCurrentPlayer() { return currentPlayer; } public boolean isGameOver() { return gameOver; } public int getWinner() { return winner; } public void resetGame() { // 重置棋盘和状态 for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { board[i][j] EMPTY; } } currentPlayer BLACK; gameOver false; winner EMPTY; } }关键点解析胜负判断算法这是模型层的核心。checkWin方法采用了经典的“方向扫描法”。效率很高时间复杂度是O(1)因为每次只检查刚落子点周围的有限位置。countDirection函数实现了单向的连续计数代码复用性高。状态封装棋盘数据board、当前玩家currentPlayer、游戏状态gameOver和赢家winner都被设为private通过公共的getter方法提供只读访问并仅通过placePiece和resetGame两个公共方法修改状态。这保证了模型内部状态的一致性是面向对象封装性的体现。清晰的职责模型只关心规则。它不知道棋子画多大棋盘什么颜色。这种纯粹性让单元测试成为可能你可以单独写个JUnit测试来验证checkWin的正确性。注意事项checkWin中count从1开始计数因为刚落下的子肯定属于当前玩家。循环中count 5就返回true覆盖了“长连”超过5个子也算赢的规则。如果你要实现“长连不算赢”的特定规则需要修改这里的判断逻辑。3.2 视图层ChessBoardView.java的实现视图层负责将枯燥的数据变成屏幕上生动的棋盘和棋子。public class ChessBoardView extends View { private GobangGame game; private Paint gridPaint, blackPaint, whitePaint, lastMovePaint; private float cellSize; // 每个格子的宽度像素 private float boardPadding; // 棋盘边距 private int lastMoveRow -1, lastMoveCol -1; // 记录最后一步落子用于高亮 public ChessBoardView(Context context, GobangGame game) { super(context); this.game game; initPaints(); } private void initPaints() { // 网格线画笔 gridPaint new Paint(Paint.ANTI_ALIAS_FLAG); gridPaint.setColor(Color.BLACK); gridPaint.setStrokeWidth(3); // 黑棋画笔 blackPaint new Paint(Paint.ANTI_ALIAS_FLAG); blackPaint.setColor(Color.BLACK); blackPaint.setStyle(Paint.Style.FILL); // 白棋画笔 whitePaint new Paint(Paint.ANTI_ALIAS_FLAG); whitePaint.setColor(Color.WHITE); whitePaint.setStyle(Paint.Style.FILL); whitePaint.setStrokeWidth(2); // 最后一步高亮画笔在棋子周围画个圈 lastMovePaint new Paint(Paint.ANTI_ALIAS_FLAG); lastMovePaint.setColor(Color.RED); lastMovePaint.setStyle(Paint.Style.STROKE); lastMovePaint.setStrokeWidth(5); } Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 计算棋盘绘制参数使棋盘居中且为正方形 int boardSizePixels Math.min(w, h); boardPadding (Math.min(w, h) - boardSizePixels) / 2 20; // 加20像素作为边距 cellSize boardSizePixels / (GobangGame.BOARD_SIZE 1); // 留出一些边距 } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBoardBackground(canvas); drawGrid(canvas); drawPieces(canvas); drawLastMoveHighlight(canvas); } private void drawBoardBackground(Canvas canvas) { // 绘制一个木纹色棋盘背景 Paint bgPaint new Paint(); bgPaint.setColor(Color.parseColor(#DEB887)); // burlywood 木色 float boardLength cellSize * (GobangGame.BOARD_SIZE - 1); canvas.drawRect(boardPadding, boardPadding, boardPadding boardLength, boardPadding boardLength, bgPaint); } private void drawGrid(Canvas canvas) { float start boardPadding; float boardLength cellSize * (GobangGame.BOARD_SIZE - 1); // 画横线 for (int i 0; i GobangGame.BOARD_SIZE; i) { float y start i * cellSize; canvas.drawLine(start, y, start boardLength, y, gridPaint); } // 画竖线 for (int i 0; i GobangGame.BOARD_SIZE; i) { float x start i * cellSize; canvas.drawLine(x, start, x, start boardLength, gridPaint); } } private void drawPieces(Canvas canvas) { int[][] board game.getBoard(); float radius cellSize * 0.4f; // 棋子半径为格子宽度的0.4倍 for (int i 0; i GobangGame.BOARD_SIZE; i) { for (int j 0; j GobangGame.BOARD_SIZE; j) { if (board[i][j] ! GobangGame.EMPTY) { float cx boardPadding j * cellSize; float cy boardPadding i * cellSize; if (board[i][j] GobangGame.BLACK) { canvas.drawCircle(cx, cy, radius, blackPaint); } else { // 白棋需要画一个带黑边的圆更有立体感 canvas.drawCircle(cx, cy, radius, whitePaint); Paint borderPaint new Paint(Paint.ANTI_ALIAS_FLAG); borderPaint.setColor(Color.BLACK); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(2); canvas.drawCircle(cx, cy, radius, borderPaint); } } } } } private void drawLastMoveHighlight(Canvas canvas) { if (lastMoveRow ! -1 lastMoveCol ! -1) { float cx boardPadding lastMoveCol * cellSize; float cy boardPadding lastMoveRow * cellSize; float radius cellSize * 0.45f; // 比棋子略大一圈 canvas.drawCircle(cx, cy, radius, lastMovePaint); } } /** * 将触摸事件的坐标转换为棋盘的行列索引 */ public int[] convertPointToBoardIndex(float touchX, float touchY) { int row Math.round((touchY - boardPadding) / cellSize); int col Math.round((touchX - boardPadding) / cellSize); // 确保索引在有效范围内 if (row 0 row GobangGame.BOARD_SIZE col 0 col GobangGame.BOARD_SIZE) { return new int[]{row, col}; } return null; // 触摸点在棋盘外 } public void setLastMove(int row, int col) { this.lastMoveRow row; this.lastMoveCol col; invalidate(); // 请求重绘以显示高亮 } public void clearLastMove() { this.lastMoveRow -1; this.lastMoveCol -1; invalidate(); } }关键点解析onSizeChanged与onDraw这是自定义View绘制的核心。onSizeChanged在View尺寸确定时调用我们在这里计算cellSize和boardPadding确保棋盘能自适应不同大小的屏幕并居中。onDraw中我们按顺序绘制背景、网格、棋子、高亮标记顺序很重要后绘制的会覆盖先绘制的。坐标转换convertPointToBoardIndex方法至关重要。它把屏幕的像素坐标(touchX, touchY)通过之前计算好的boardPadding和cellSize转换成棋盘的二维数组索引(row, col)。这里使用Math.round进行四舍五入让落子点自动吸附到最近的交叉点上提升用户体验。绘制优化所有Paint对象都在initPaints中初始化并设置好属性如抗锯齿ANTI_ALIAS_FLAG避免在onDraw中频繁创建对象这是安卓绘制的性能优化常识。为白棋添加一个黑色边框能使其在浅色背景上更清晰这也是从实际用户体验出发的细节。高亮最后一步setLastMove和drawLastMoveHighlight提供了额外的用户体验优化。在快节奏对弈中高亮显示上一步落子位置能帮助玩家快速定位战局焦点。3.3 控制层与主活动MainActivity.java的整合现在我们需要把模型和视图粘合起来并处理用户交互。public class MainActivity extends AppCompatActivity { private GobangGame game; private ChessBoardView chessBoardView; private TextView statusTextView; private Button resetButton; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化游戏模型 game new GobangGame(); // 绑定视图 chessBoardView findViewById(R.id.chess_board); statusTextView findViewById(R.id.tv_status); resetButton findViewById(R.id.btn_reset); // 将游戏模型传递给自定义View chessBoardView.setGame(game); // 需要在ChessBoardView中添加setGame方法 // 设置触摸监听 chessBoardView.setOnTouchListener(new View.OnTouchListener() { Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN) { // 获取触摸点坐标相对于View自身 float x event.getX(); float y event.getY(); // 转换为棋盘索引 int[] index chessBoardView.convertPointToBoardIndex(x, y); if (index ! null) { int row index[0]; int col index[1]; // 尝试落子 boolean success game.placePiece(row, col); if (success) { // 落子成功高亮这一步 chessBoardView.setLastMove(row, col); // 更新状态显示 updateGameStatus(); // 请求View重绘 chessBoardView.invalidate(); } } } return true; // 消费此触摸事件 } }); // 重置按钮事件 resetButton.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { game.resetGame(); chessBoardView.clearLastMove(); updateGameStatus(); chessBoardView.invalidate(); } }); // 初始化状态显示 updateGameStatus(); } private void updateGameStatus() { String status; if (game.isGameOver()) { int winner game.getWinner(); status (winner GobangGame.BLACK) ? 游戏结束黑棋获胜 : 游戏结束白棋获胜; } else { status (game.getCurrentPlayer() GobangGame.BLACK) ? 当前回合黑棋 : 当前回合白棋; } statusTextView.setText(status); } }对应的布局文件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:padding16dp android:gravitycenter_horizontal android:background#F5F5DC TextView android:idid/tv_status android:layout_widthwrap_content android:layout_heightwrap_content android:textSize24sp android:textStylebold android:text当前回合黑棋 android:layout_marginBottom20dp/ com.yourname.gobang.view.ChessBoardView android:idid/chess_board android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / Button android:idid/btn_reset android:layout_widthwrap_content android:layout_heightwrap_content android:text重新开始 android:layout_marginTop20dp android:paddingHorizontal30dp android:paddingVertical10dp/ /LinearLayout关键点解析事件处理流程这是控制逻辑的核心。用户在ChessBoardView上触摸ACTION_DOWN时事件被OnTouchListener捕获。控制器Activity调用View的坐标转换方法得到棋盘索引然后命令模型game.placePiece执行落子逻辑。根据模型的返回结果控制器更新View的状态高亮最后一步、调用invalidate重绘和UI状态更新statusTextView。这是一个清晰的“View捕获输入 - Controller处理并更新Model - Model通知数据变化 - Controller更新View”的循环。Activity的职责MainActivity在这里完美扮演了控制器的角色。它初始化所有组件设置监听器并根据模型状态协调视图更新。它不包含任何游戏规则逻辑也不包含具体的绘制代码。布局设计使用LinearLayout垂直排列。状态文本在上方棋盘占据剩余主要空间通过layout_weight1实现重置按钮在下方。背景色设为米黄色#F5F5DC比纯白更护眼也更接近真实棋盘的质感。实操心得在ChessBoardView的onTouch监听器中我直接处理了ACTION_DOWN事件这对于落子操作是足够的。如果你需要实现更复杂的手势如拖动、长按悔棋可以在这里扩展。另外注意onTouch方法返回true表示这个触摸事件已被消费不会继续传递给其他View这是处理触摸事件的常见做法。4. 功能扩展与优化实践一个基础版本完成后我们可以考虑添加更多功能让这个小游戏更像一个真正的产品。这里分享几个我实践过的扩展方向。4.1 实现悔棋功能悔棋是棋类游戏的基本功能。实现它需要模型层支持状态回溯。在GobangGame.java中添加public class GobangGame { // ... 原有成员变量 ... private Stackint[] moveHistory; // 用栈保存每一步的落子位置 [row, col] public GobangGame() { // ... 原有初始化 ... moveHistory new Stack(); } public boolean placePiece(int row, int col) { // ... 原有检查 ... board[row][col] currentPlayer; moveHistory.push(new int[]{row, col}); // 记录这一步 // ... 胜负判断和切换玩家 ... } /** * 悔棋一步 * return true 悔棋成功false 无法悔棋如没有历史步骤或游戏已结束 */ public boolean undo() { if (gameOver || moveHistory.isEmpty()) { return false; } int[] lastMove moveHistory.pop(); int row lastMove[0]; int col lastMove[1]; board[row][col] EMPTY; // 清空该位置 // 切换回上一个玩家 currentPlayer (currentPlayer BLACK) ? WHITE : BLACK; return true; } public void resetGame() { // ... 原有重置逻辑 ... moveHistory.clear(); } }在MainActivity.java中添加悔棋按钮和逻辑 在布局中加入一个悔棋按钮并在onCreate中设置监听Button undoButton findViewById(R.id.btn_undo); undoButton.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { if (game.undo()) { chessBoardView.clearLastMove(); // 如果历史中还有上一步则高亮它 if (!game.isMoveHistoryEmpty()) { // 需要在GobangGame中添加此方法 int[] lastMove game.peekLastMove(); // 添加此方法查看栈顶 chessBoardView.setLastMove(lastMove[0], lastMove[1]); } updateGameStatus(); chessBoardView.invalidate(); } else { Toast.makeText(MainActivity.this, 无法悔棋, Toast.LENGTH_SHORT).show(); } } });设计思考使用Stack来保存历史步骤是自然的选择符合“后进先出”的悔棋特性。注意悔棋后需要更新当前玩家为上一次行棋的一方并且要同步更新View中高亮的最后一步。4.2 添加简单AI对手让游戏支持人机对战能极大增加可玩性。我们可以实现一个极其简单的AI——随机落子。虽然蠢但完整展示了接入AI的架构。首先修改模型以支持AI模式 在GobangGame中增加游戏模式属性和AI落子方法。public class GobangGame { public static final int MODE_PVP 0; public static final int MODE_PVE 1; private int gameMode MODE_PVP; private Random random new Random(); public void setGameMode(int mode) { this.gameMode mode; } public boolean placePiece(int row, int col) { // ... 原有逻辑 ... // 落子后如果是PVE模式且游戏未结束且轮到AI白棋则触发AI落子 if (gameMode MODE_PVE !gameOver currentPlayer WHITE) { // 注意这里在同一个方法调用中触发了AI需要小心处理递归或异步 // 更好的做法是让placePiece返回一个事件由控制器决定何时调用AI } return true; } /** * AI进行一步落子随机策略 * return 落子的位置 [row, col]若无位置可下则返回null */ public int[] aiMakeMove() { Listint[] emptyCells new ArrayList(); for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { if (board[i][j] EMPTY) { emptyCells.add(new int[]{i, j}); } } } if (emptyCells.isEmpty()) { return null; // 棋盘已满 } return emptyCells.get(random.nextInt(emptyCells.size())); } }然后在控制器中协调AI行动 修改MainActivity中的触摸事件处理在玩家落子后如果处于PVE模式则延迟调用AI落子。chessBoardView.setOnTouchListener(new View.OnTouchListener() { Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN !game.isGameOver()) { // ... 坐标转换和玩家落子逻辑 ... if (success game.getGameMode() GobangGame.MODE_PVE !game.isGameOver()) { // 玩家落子成功且是PVE模式游戏未结束则延迟执行AI回合 chessBoardView.postDelayed(new Runnable() { Override public void run() { executeAIMove(); } }, 500); // 延迟500毫秒让玩家能看到自己的棋子落下 } } return true; } }); private void executeAIMove() { if (game.isGameOver() || game.getCurrentPlayer() ! GobangGame.WHITE) { return; } int[] aiMove game.aiMakeMove(); if (aiMove ! null) { boolean success game.placePiece(aiMove[0], aiMove[1]); if (success) { chessBoardView.setLastMove(aiMove[0], aiMove[1]); updateGameStatus(); chessBoardView.invalidate(); } } }架构优势体现你会发现添加AI功能我们主要修改的是GobangGame模型增加AI算法和MainActivity控制器增加调用AI的逻辑。ChessBoardView视图层几乎不需要改动。这证明了前期架构分离带来的巨大灵活性。你可以很容易地将随机AI替换为更复杂的算法如极大极小值搜索、Alpha-Beta剪枝而无需重写其他部分。4.3 持久化与状态保存安卓应用可能被随时中断如来电、切换应用。我们需要保存游戏状态。在MainActivity中重写生命周期方法Override protected void onSaveInstanceState(NonNull Bundle outState) { super.onSaveInstanceState(outState); // 我们需要将游戏状态棋盘、当前玩家等序列化并保存到Bundle中 // 一种简单方式是将board数组转换成一维字符串保存 StringBuilder boardState new StringBuilder(); for (int i 0; i GobangGame.BOARD_SIZE; i) { for (int j 0; j GobangGame.BOARD_SIZE; j) { boardState.append(game.getBoard()[i][j]); } } outState.putString(BOARD_STATE, boardState.toString()); outState.putInt(CURRENT_PLAYER, game.getCurrentPlayer()); outState.putBoolean(GAME_OVER, game.isGameOver()); outState.putInt(WINNER, game.getWinner()); } Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... 原有代码 ... if (savedInstanceState ! null) { // 恢复游戏状态 String savedBoard savedInstanceState.getString(BOARD_STATE); int savedPlayer savedInstanceState.getInt(CURRENT_PLAYER); boolean savedGameOver savedInstanceState.getBoolean(GAME_OVER); int savedWinner savedInstanceState.getInt(WINNER); // 这里需要为GobangGame添加一个从状态恢复的方法或者重新初始化并应用状态 game GobangGame.fromSavedState(savedBoard, savedPlayer, savedGameOver, savedWinner); chessBoardView.setGame(game); updateGameStatus(); } else { game new GobangGame(); } // ... 后续绑定等代码 ... }注意onSaveInstanceState只在Activity被系统临时销毁如屏幕旋转时自动调用。对于真正的退出应用如果需要保存应该考虑使用SharedPreferences或数据库将状态持久化到本地存储中。5. 常见问题排查与性能优化在实际开发和测试中你可能会遇到以下问题。这里是我总结的一些排查思路和优化建议。5.1 触摸不灵敏或落子位置不准问题现象点击棋盘边缘或棋子时程序没反应或者棋子画在了奇怪的位置。排查步骤检查坐标转换在convertPointToBoardIndex方法中打印touchX,touchY,boardPadding,cellSize和计算后的row,col。确认boardPadding计算正确且触摸点减去边距后除以格子大小能得到合理的索引0-14。检查onSizeChanged确保cellSize和boardPadding是在View尺寸确定后才计算的。有时在onCreate中View的尺寸可能还是0。检查四舍五入使用Math.round是正确的它实现了“自动吸附”。你也可以尝试用(int) Math.floor((pos - padding cellSize/2) / cellSize)原理是判断触摸点是否落在某个格子的中心区域内。优化技巧可以增加一个touchSlop触摸容差参数。在convertPointToBoardIndex中先判断触摸点距离最近交叉点的像素距离是否小于touchSlop比如10像素如果是则吸附到该交叉点否则认为点击无效。这能防止误触。5.2 棋盘绘制模糊或锯齿严重问题现象网格线或棋子边缘看起来有锯齿不光滑。原因与解决启用抗锯齿确保所有Paint对象在初始化时都设置了Paint.ANTI_ALIAS_FLAG标志。这是消除锯齿最基本有效的方法。使用浮点数坐标在onDraw中绘制时Canvas的drawLine、drawCircle等方法使用float类型的坐标参数避免使用int因为浮点数计算更精确绘制引擎能进行更好的亚像素渲染。检查DPI在不同屏幕密度的设备上测试。确保你的cellSize和strokeWidth是基于像素密度DisplayMetrics.density动态计算的而不是固定像素值。例如网格线宽度可以是2 * density。5.3 游戏逻辑BUG胜负判断错误问题现象五子连珠没判赢或者四子就判赢了或者斜向判断有问题。排查步骤单元测试为GobangGame的checkWin方法编写独立的JUnit测试用例。覆盖所有可能赢的情况横向五连、纵向五连、左上到右下斜向五连、右上到左下斜向五连。同时测试边界情况比如棋子在棋盘边缘。调试输出在checkWin方法中临时添加Log输出打印每次扫描的起点、方向和连续棋子数count。对照棋盘数据人工验证逻辑。方向向量检查确认directions数组{{1,0},{0,1},{1,1},{1,-1}}定义正确。{1, -1}代表向右上方向行增加列减少。countDirection逻辑确保countDirection函数在向正反两个方向扫描时没有重复计算中心点。我们的实现中checkWin里count从1开始两个countDirection只统计同一方向上除了中心点之外的连续棋子所以是正确的。5.4 性能问题界面卡顿问题现象在低端设备上落子后画面刷新感觉不流畅。优化方案避免在onDraw中创建对象这是最重要的原则。所有Paint、Path、Bitmap等对象都应在View初始化时创建并复用。减少无效区域重绘我们的实现中每次落子都调用invalidate()会导致整个棋盘区域重绘。可以优化为只重绘发生变化的部分区域。使用invalidate(int left, int top, int right, int bottom)或invalidate(Rect dirty)来指定需要重绘的脏矩形区域。例如只需要重绘落子点及周围一小片区域。考虑使用SurfaceView如果将来要加入复杂的动画如棋子落下动画View的onDraw在主线程执行频繁动画可能阻塞UI。SurfaceView在独立的线程中绘制更适合高频刷新。但对于当前静态的五子棋View足够。简化onDraw操作检查onDraw中是否有不必要的循环或计算。我们的绘制循环是O(N²)N15225次迭代对于现代手机来说微不足道无需优化。5.5 内存泄漏预防潜在风险在MainActivity中我们通过内部类如OnTouchListener持有了Activity的引用。如果这些监听器被长生命周期对象持有可能导致Activity无法被回收。解决方案使用弱引用或静态内部类对于可能被长期持有的监听器考虑使用WeakReference来引用Activity或者将监听器声明为静态内部类并通过构造函数传入Activity的弱引用。在onDestroy中解绑在Activity的onDestroy方法中手动将View的监听器设为null。Override protected void onDestroy() { super.onDestroy(); if (chessBoardView ! null) { chessBoardView.setOnTouchListener(null); } // 同样解绑Button的监听器 if (resetButton ! null) { resetButton.setOnClickListener(null); } }对于这个简单的单Activity游戏内存泄漏风险很小但养成好习惯很重要。写完这个项目我最深的体会是一个看似简单的应用背后是软件工程核心思想的绝佳演练场。从MVC架构的清晰分离到触摸事件处理的精确坐标转换再到胜负判断算法的严谨实现每一步都考验着开发者对基础知识的掌握和系统设计的能力。特别是当你尝试添加AI、悔棋、状态保存这些功能时前期架构设计的好坏会立刻显现出来。好的设计能让扩展像搭积木一样自然而糟糕的设计则会让你陷入无尽的bug修改和代码重构中。对于计科的同学我强烈建议你不要止步于让游戏“跑起来”。试着去实现一个更聪明的AI比如基于评分表的贪心算法甚至迷你Max算法或者为棋子落子添加一个缩放动画再或者尝试用SharedPreferences保存多个棋局记录。每一个小功能的添加都是对你安卓开发技能树的一次拓展。这个五子棋项目完全可以作为你课程设计或者个人作品集的一个亮眼起点。本文还有配套的精品资源点击获取