
简介这是一份基于Android Studio开发的五子棋安卓小游戏完整工程面向计算机科学与技术、软件工程等专业本科生及移动应用开发初学者解决安卓GUI交互、游戏逻辑实现与AI策略设计等核心实践问题。资源包共657个文件包含27个Java源码文件实现棋盘绘制、胜负判定、悔棋逻辑等、39个XML布局与配置文件、45个PNG图标资源、124个JSON配置及Gradle构建相关文件整体压缩包大小为30.71MB结构清晰便于模块化学习与二次开发。已有2089人下载学习适合课程设计、实训项目或毕业设计参考。读者可直接导入Android Studio 4.0.1及以上版本运行完整获得人机对战基于得分评估的AI算法、人人对战、背景音乐控制、实时计分、悔棋与新局重置等全部功能代码注释充分逻辑分层明确是理解安卓事件驱动模型与简单AI决策机制的优质入门案例。1. 项目概述与核心价值最近在整理一些适合计算机专业学生或者安卓开发新手练手的项目发现五子棋这个小游戏真是个“宝藏”。它规则简单但实现起来却能把安卓开发里UI绘制、事件处理、逻辑判断这些核心知识点都串起来。用Android Studio来实现一个五子棋远不止是画个棋盘、摆几个棋子那么简单。你得考虑怎么把用户的触摸点击精准地映射到棋盘的交点上怎么实时判断横、竖、斜任意方向是否连成了五子还要设计一个清晰的数据结构来记录棋盘状态。这个过程对于理解MVC或MVVM架构思想、自定义View的绘制流程、以及Android的基础组件使用有非常大的帮助。如果你正在学《安卓程序设计》这门课或者想找个项目来巩固Java/Kotlin和Android开发基础这个项目再合适不过了。它不像一个完整的社交或电商App那样庞大复杂但又足够完整能让你体验到从界面设计到核心算法实现的完整闭环做完之后成就感十足。2. 整体设计与架构思路拆解2.1 为什么选择五子棋作为练手项目五子棋项目之所以经典是因为它在有限的复杂度内涵盖了安卓应用开发的多个核心层面。首先在视图层View它需要自定义一个View或SurfaceView来绘制棋盘和棋子这涉及到Canvas和Paint的熟练使用包括画线、画圆、处理抗锯齿等细节。其次在控制层Controller需要处理用户的触摸事件onTouchEvent将屏幕坐标转换为棋盘逻辑坐标这是理解Android事件分发机制的好例子。最后在模型层Model需要一个二维数组或更高效的数据结构来存储棋盘状态并实现胜负判断算法这考验的是基本的数据结构和算法能力。这种清晰的分层正好对应了安卓开发中倡导的分离关注点思想。通过这个项目你能直观地感受到数据、逻辑和视图是如何协同工作的而不是仅仅停留在拖拽控件和编写简单事件监听器的层面。2.2 技术选型与核心组件决策在动手之前有几个关键的技术决策点需要想清楚这直接关系到代码结构和后续扩展的难易程度。绘制方式的选择自定义View vs. SurfaceView自定义View继承自View对于五子棋这种刷新频率不高只有落子时重绘的场景自定义View完全够用。它的优点是简单直接利用onDraw方法进行绘制系统会自动管理重绘。代码逻辑清晰易于理解和调试。SurfaceView适用于需要高频、主动刷新的场景比如游戏主循环。五子棋用它会显得“杀鸡用牛刀”引入额外的线程管理Holder和Callback复杂度。因此对于初学者和这个具体项目强烈推荐使用自定义View。我们可以在onDraw方法里一次性绘制棋盘网格和所有已落下的棋子。数据模型的设计二维数组与位运算最直观的模型是一个int[][]或byte[][]二维数组比如board[15][15]用0表示空位1表示黑子2表示白子。这种方案简单易懂访问和修改状态非常方便。更高级或追求极致性能的玩法可以考虑使用位棋盘Bitboard用两个long型整数对于15x15棋盘需要225个位一个long有64位需要4个分别表示黑子和白子的位置。通过位运算与、或、移位来进行胜负判断速度极快。但对于教学和大多数应用场景二维数组的清晰度和可维护性优势更大。本项目将采用二维数组方案重点在于把逻辑表达清楚。胜负判断算法的实现思路这是项目的算法核心。最朴素的方法是在每次落子后以该子为中心向四个方向水平、垂直、左上右下、右上左下进行搜索统计连续同色棋子的数量。一旦某个方向达到5即判胜。虽然听起来简单但实现时要注意数组越界检查以及如何高效地实现双向计数。我们稍后会详细拆解一个既清晰又高效的判断函数。3. 核心模块实现与代码详解3.1 棋盘绘制模块自定义View我们创建一个名为GobangView的类继承自View。它的核心任务是绘制一个15x15的网格棋盘并根据模型数据绘制棋子。public class GobangView extends View { private int mBoardSize 15; // 棋盘格数15x15 private float mCellSize; // 每个格子的宽度像素 private float mOffset; // 棋盘边距 private Paint mGridPaint; // 绘制网格的画笔 private Paint mBlackPiecePaint; // 绘制黑棋的画笔 private Paint mWhitePiecePaint; // 绘制白棋的画笔 private Paint mLastMovePaint; // 高亮最后一手的画笔可选 private int[][] mBoard; // 棋盘数据模型0空1黑2白 public GobangView(Context context) { this(context, null); } public GobangView(Context context, Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 初始化棋盘数据模型 mBoard new int[mBoardSize][mBoardSize]; // 初始化网格画笔 mGridPaint new Paint(Paint.ANTI_ALIAS_FLAG); mGridPaint.setColor(Color.BLACK); mGridPaint.setStrokeWidth(3); // 初始化黑棋画笔实心圆 mBlackPiecePaint new Paint(Paint.ANTI_ALIAS_FLAG); mBlackPiecePaint.setColor(Color.BLACK); mBlackPiecePaint.setStyle(Paint.Style.FILL); // 初始化白棋画笔实心圆黑边 mWhitePiecePaint new Paint(Paint.ANTI_ALIAS_FLAG); mWhitePiecePaint.setColor(Color.WHITE); mWhitePiecePaint.setStyle(Paint.Style.FILL); mWhitePiecePaint.setStrokeWidth(2); // 为白棋增加一个黑色边框更清晰 mWhitePiecePaint.setShadowLayer(2, 0, 0, Color.BLACK); // 简单模拟边框效果也可用两层圆绘制 // 高亮最后一手例如一个红圈 mLastMovePaint new Paint(Paint.ANTI_ALIAS_FLAG); mLastMovePaint.setColor(Color.RED); mLastMovePaint.setStyle(Paint.Style.STROKE); mLastMovePaint.setStrokeWidth(4); } Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 计算棋盘绘制参数确保棋盘居中且为正方形 int minSize Math.min(w, h); mOffset 20; // 边距20像素 float boardTotalWidth minSize - 2 * mOffset; mCellSize boardTotalWidth / (mBoardSize - 1); // 注意格子数-1 间隔数 } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBoard(canvas); drawPieces(canvas); // drawLastMoveHighlight(canvas); // 可选绘制最后一手高亮 } /** * 绘制棋盘网格 */ private void drawBoard(Canvas canvas) { float start mOffset; float end mOffset (mBoardSize - 1) * mCellSize; // 画横线 for (int i 0; i mBoardSize; i) { float y mOffset i * mCellSize; canvas.drawLine(start, y, end, y, mGridPaint); } // 画竖线 for (int i 0; i mBoardSize; i) { float x mOffset i * mCellSize; canvas.drawLine(x, start, x, end, mGridPaint); } } /** * 根据mBoard数据绘制棋子 */ private void drawPieces(Canvas canvas) { float radius mCellSize * 0.4f; // 棋子半径为格子宽度的0.4倍 for (int i 0; i mBoardSize; i) { for (int j 0; j mBoardSize; j) { if (mBoard[i][j] 0) continue; // 空位跳过 float centerX mOffset j * mCellSize; // 注意列j对应x坐标 float centerY mOffset i * mCellSize; // 行i对应y坐标 if (mBoard[i][j] 1) { canvas.drawCircle(centerX, centerY, radius, mBlackPiecePaint); } else if (mBoard[i][j] 2) { // 先画一个稍大的黑圈作为边框再画白棋效果更好 Paint borderPaint new Paint(mBlackPiecePaint); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(4); canvas.drawCircle(centerX, centerY, radius 2, borderPaint); canvas.drawCircle(centerX, centerY, radius, mWhitePiecePaint); } } } } // 提供方法供外部如Activity更新棋盘数据并重绘 public void setBoardData(int[][] board) { this.mBoard board; invalidate(); // 请求重绘 } }关键点解析onSizeChanged: 这是关键回调当View尺寸确定后调用。我们在这里计算每个格子的大小(mCellSize)和棋盘起始偏移(mOffset)确保棋盘在不同屏幕上都能居中并适配。onDraw: 绘制逻辑的核心。分离drawBoard和drawPieces使代码更清晰。注意绘制棋子时mBoard数组的行列索引(i, j)与屏幕坐标(x, y)的对应关系。白棋边框处理为了让白棋在白色背景或浅色棋盘上更醒目为其添加一个黑色边框是常见做法。代码中展示了两种思路使用ShadowLayer简单模拟或者更精确地先画一个稍大的黑圈再画白圈。invalidate(): 当棋盘数据改变后必须调用此方法通知系统重绘View。3.2 触摸事件处理与坐标转换棋盘画好了接下来要让它能响应玩家的触摸落子。这需要在GobangView中重写onTouchEvent方法。public class GobangView extends View { // ... 之前的成员变量 ... // 定义回调接口通知Activity落子事件 public interface OnPiecePlayListener { void onPiecePlayed(int row, int col); } private OnPiecePlayListener mListener; public void setOnPiecePlayListener(OnPiecePlayListener listener) { this.mListener listener; } Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN) { // 1. 获取触摸点坐标 float touchX event.getX(); float touchY event.getY(); // 2. 将屏幕坐标转换为棋盘网格坐标 // 公式 (触摸坐标 - 棋盘起始偏移 半个格子宽度) / 格子宽度 // 加半个格子宽度是为了实现“吸附”效果点击格子附近区域也能落到交叉点上 int col Math.round((touchX - mOffset mCellSize / 2) / mCellSize); int row Math.round((touchY - mOffset mCellSize / 2) / mCellSize); // 3. 检查坐标是否在棋盘有效范围内 (0 到 mBoardSize-1) if (row 0 row mBoardSize col 0 col mBoardSize) { // 4. 通过接口回调将落子位置通知给逻辑控制器Activity if (mListener ! null) { mListener.onPiecePlayed(row, col); } return true; // 消费此触摸事件 } } return super.onTouchEvent(event); // 其他事件交给父类处理 } }关键点解析坐标转换这是触摸处理的核心。event.getX()和event.getY()获得的是相对于GobangView左上角的像素坐标。我们需要将其转换为棋盘的(行, 列)索引。通过Math.round进行四舍五入结合“加半个格子”的技巧实现了自动吸附到最近交叉点的效果提升了用户体验。接口回调自定义View不应该直接处理游戏逻辑如当前轮到谁、是否可落子、胜负判断。它只负责“感知”用户想在哪里落子然后通过OnPiecePlayListener接口将这个意图“报告”给外部的逻辑控制器通常是Activity或ViewModel。这是遵守单一职责原则和实现模块解耦的关键。边界检查必须检查转换后的row和col是否在[0, mBoardSize-1]范围内防止用户点击棋盘外区域导致数组越界。3.3 游戏逻辑控制与胜负判断现在我们创建一个GameController类或者直接在Activity中来管理游戏状态和核心逻辑。public class GameController { public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2; private int mBoardSize; private int[][] mBoard; private int mCurrentPlayer; // 当前行棋方BLACK 或 WHITE private boolean mGameOver; private GameStateListener mListener; public interface GameStateListener { void onBoardChanged(int[][] board); void onGameOver(int winner); // winner: BLACK, WHITE, 或 EMPTY和棋 void onCurrentPlayerChanged(int player); } public GameController(int boardSize, GameStateListener listener) { mBoardSize boardSize; mBoard new int[mBoardSize][mBoardSize]; mCurrentPlayer BLACK; // 黑棋先行 mGameOver false; mListener listener; } /** * 尝试在指定位置落子 * param row 行 * param col 列 * return 落子是否成功位置为空且游戏未结束 */ public boolean placePiece(int row, int col) { if (mGameOver || mBoard[row][col] ! EMPTY) { return false; // 游戏已结束或该位置已有子 } // 落子 mBoard[row][col] mCurrentPlayer; // 通知界面更新 if (mListener ! null) { mListener.onBoardChanged(mBoard); } // 检查是否获胜 if (checkWin(row, col)) { mGameOver true; if (mListener ! null) { mListener.onGameOver(mCurrentPlayer); } return true; } // 检查是否和棋棋盘已满 if (isBoardFull()) { mGameOver true; if (mListener ! null) { mListener.onGameOver(EMPTY); // 用EMPTY表示和棋 } return true; } // 切换行棋方 switchPlayer(); if (mListener ! null) { mListener.onCurrentPlayerChanged(mCurrentPlayer); } return true; } /** * 胜负判断核心算法 * param row 刚落子的行 * param col 刚落子的列 * return 是否形成五连珠 */ private boolean checkWin(int row, int col) { int player mBoard[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; } /** * 向指定方向统计连续同色棋子数量 * param row 起始行 * param col 起始列 * param dRow 行方向增量 * param dCol 列方向增量 * param player 棋子颜色 * return 该方向上的连续同色棋子数不包括起始点 */ 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 mBoardSize c 0 c mBoardSize mBoard[r][c] player) { count; r dRow; c dCol; } return count; } /** * 判断棋盘是否已满和棋条件 */ private boolean isBoardFull() { for (int i 0; i mBoardSize; i) { for (int j 0; j mBoardSize; j) { if (mBoard[i][j] EMPTY) { return false; } } } return true; } private void switchPlayer() { mCurrentPlayer (mCurrentPlayer BLACK) ? WHITE : BLACK; } // 重置游戏 public void resetGame() { for (int i 0; i mBoardSize; i) { Arrays.fill(mBoard[i], EMPTY); } mCurrentPlayer BLACK; mGameOver false; if (mListener ! null) { mListener.onBoardChanged(mBoard); mListener.onCurrentPlayerChanged(mCurrentPlayer); } } }关键点解析状态管理GameController集中管理了棋盘数据mBoard、当前行棋方mCurrentPlayer和游戏是否结束mGameOver的状态。所有对游戏状态的修改都应通过它提供的方法如placePiece进行。事件驱动与监听器通过GameStateListener接口控制器将“棋盘变化”、“游戏结束”、“玩家切换”等事件通知给UIActivity。这是一种松耦合的设计便于测试和扩展。胜负判断算法checkWin方法是核心。它从刚落子的位置(row, col)出发向四个方向水平、垂直、两条对角线分别进行搜索。countDirection函数负责向一个方向如(1,0)代表向右累加连续的同色棋子数。关键技巧是向一个方向的正反两侧分别计数然后加上当前子count初始为1判断总和是否5。这个算法清晰高效时间复杂度为O(1)因为最多检查约4*832个位置。和棋判断简单的遍历棋盘检查是否还有空位(EMPTY)。3.4 主Activity整合与UI交互最后我们需要一个Activity来把View、Controller和用户界面如状态提示、重新开始按钮粘合起来。public class MainActivity extends AppCompatActivity implements GobangView.OnPiecePlayListener, GameController.GameStateListener { private GobangView mGobangView; private GameController mGameController; private TextView mStatusText; private Button mRestartButton; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mGobangView findViewById(R.id.gobang_view); mStatusText findViewById(R.id.tv_status); mRestartButton findViewById(R.id.btn_restart); // 1. 初始化游戏控制器 mGameController new GameController(15, this); // 15x15棋盘this作为监听器 // 2. 为自定义View设置落子监听 mGobangView.setOnPiecePlayListener(this); // 3. 初始化视图的棋盘数据 mGobangView.setBoardData(mGameController.getBoard()); // 假设GameController提供了getBoard方法 // 4. 设置重新开始按钮的点击事件 mRestartButton.setOnClickListener(v - { mGameController.resetGame(); mStatusText.setText(游戏开始黑方先行。); }); // 5. 初始化状态显示 updateStatus(); } // 实现 OnPiecePlayListener 接口 Override public void onPiecePlayed(int row, int col) { // 将触摸事件传递给控制器处理 boolean success mGameController.placePiece(row, col); // 如果落子失败如位置已有子可以给个Toast提示 if (!success) { Toast.makeText(this, 此处不能落子, Toast.LENGTH_SHORT).show(); } } // 实现 GameStateListener 接口 Override public void onBoardChanged(int[][] board) { // 控制器通知棋盘数据已更新刷新View mGobangView.setBoardData(board); } Override public void onCurrentPlayerChanged(int player) { runOnUiThread(() - updateStatus()); } Override public void onGameOver(int winner) { runOnUiThread(() - { String message; if (winner GameController.BLACK) { message 游戏结束黑方获胜; } else if (winner GameController.WHITE) { message 游戏结束白方获胜; } else { message 游戏结束棋盘已满和棋; } mStatusText.setText(message); // 可以禁用触摸或显示一个对话框 mGobangView.setOnPiecePlayListener(null); // 暂时禁用落子 }); } private void updateStatus() { int currentPlayer mGameController.getCurrentPlayer(); // 假设有getter方法 String playerName (currentPlayer GameController.BLACK) ? 黑方 : 白方; mStatusText.setText(当前轮到 playerName); } }对应的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 TextView android:idid/tv_status android:layout_widthwrap_content android:layout_heightwrap_content android:textSize18sp android:textStylebold android:text游戏开始黑方先行。 android:layout_marginBottom16dp/ 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_marginTop16dp/ /LinearLayout关键点解析粘合层MainActivity扮演了“粘合剂”的角色。它实例化了GameController和GobangView并实现了两者的监听器接口负责在它们之间传递消息。UI线程更新注意在onGameOver和onCurrentPlayerChanged回调中我们使用了runOnUiThread来更新TextView。这是因为这些回调可能发生在非UI线程如果游戏逻辑计算复杂未来可能放在子线程而UI操作必须在主线程进行。游戏流程用户触摸GobangView-onPiecePlayed被调用 -Activity调用GameController.placePiece()- 控制器更新数据、判断胜负 - 通过监听器回调通知Activity-Activity更新View和状态文本。这是一个清晰的事件驱动流程。4. 功能扩展与性能优化思路一个基础的五子棋已经完成了。但如果你想让它更完善、更像一个真正的产品或者作为课程设计/毕业设计有更高的要求可以考虑以下扩展方向。4.1 悔棋与棋谱记录功能悔棋实现核心是维护一个Stack栈来记录每一步的落子位置行、列、玩家。当用户点击悔棋时从栈中弹出上一步将对应棋盘位置重置为EMPTY并切换回上一个玩家。public class GameController { private StackMove mMoveHistory new Stack(); class Move { int row; int col; int player; } public boolean undo() { if (mMoveHistory.isEmpty() || mGameOver) return false; Move lastMove mMoveHistory.pop(); mBoard[lastMove.row][lastMove.col] EMPTY; mCurrentPlayer lastMove.player; // 切换回上一步的玩家 // ... 通知UI更新 ... return true; } // 在placePiece成功时将Move压入栈中 }棋谱记录存盘/读盘可以将每一步的Move序列或整个mBoard数组用JSON或自定义二进制格式保存到文件或SharedPreferences中。实现saveGame()和loadGame()方法。这涉及到Android的本地存储操作。4.2 人机对战AI实现入门为游戏增加一个简单的AI对手能极大提升项目的挑战性和技术含量。可以从最简单的“随机落子”开始逐步实现有一定智能的算法。随机AI在空白位置中随机选择一个落子。毫无智能但实现简单。基于规则的AI这是入门级AI的常见选择。可以定义一些规则优先级规则1进攻检查AI白方是否有位置可以形成“活四”两头无阻挡的四连子或“冲四”一头被堵的四连子有则落子取胜或防守。规则2防守检查玩家黑方是否有位置可以形成“活四”或“冲四”有则必须堵住。规则3创造机会寻找能让自己形成“活三”两头无阻挡的三连子的位置。规则4随机以上都不满足则在棋盘中心区域或对方棋子附近随机落子。 实现时可以遍历所有空位为每个位置根据上述规则打分选择分数最高的位置落子。极小化极大算法Minimax与Alpha-Beta剪枝这是棋类游戏AI的经典算法。它通过模拟未来几步棋的所有可能走法构建一棵博弈树并假设对手总是做出对自己最不利的走法极小化自己则选择最优走法极大化。Alpha-Beta剪枝用于优化减少不必要的搜索分支。对于15x15的五子棋搜索深度不能太深通常3-4层否则计算量会指数级增长。实现这个算法是计算机科学中关于搜索和博弈论的绝佳实践。4.3 性能优化与体验提升绘制优化在GobangView的onDraw中避免创建新的Paint对象。所有Paint对象应在init()中初始化并复用。对于固定不变的背景如木纹棋盘图可以考虑用Bitmap缓存起来。胜负判断优化我们实现的checkWin算法已经很快。但如果追求极致可以在每次落子后只更新该子所在行、列、对角线的“连续子状态”缓存用空间换时间。动画效果落子时增加一个缩放动画ValueAnimator让棋子从无到有、从小变大落下提升视觉体验。同样获胜时可以高亮显示连成五子的那一条线。声音与振动落子、获胜时添加音效和短振动让游戏更有沉浸感。使用SoundPool播放短音效使用Vibrator实现振动注意权限声明。5. 开发中常见问题与调试技巧在实际编码和测试过程中你可能会遇到下面这些问题。这里记录了我自己踩过的一些坑和解决方法。5.1 触摸不灵敏或落子位置偏差现象点击棋盘交叉点附近棋子却下在了别处或者根本没反应。排查检查坐标转换公式这是最常见的问题。确保onTouchEvent中的转换公式正确特别是mOffset棋盘起始偏移和mCellSize格子大小的计算是否准确。可以在onDraw里临时画一个十字标记你计算出的落子点看是否与触摸点匹配。打印日志在onTouchEvent中打印touchX,touchY,row,col的值观察转换过程。检查onSizeChanged确保mCellSize和mOffset是在onSizeChanged中正确计算的并且View的尺寸已经确定。有时在onCreate中获取的View宽高为0。解决仔细核对onSizeChanged和坐标转换代码。确保mOffset是棋盘线开始的位置mCellSize (总宽度 - 2*mOffset) / (格子数-1)。5.2 胜负判断逻辑错误现象四连子就判赢或者五连子没反应。排查检查计数逻辑checkWin函数中count初始值应为1代表刚落下的子。countDirection函数向一个方向搜索时不应包含起始点。确保正反两个方向计数的和加上1当前子等于5才判赢。边界条件countDirection函数中的while循环条件必须首先检查数组索引r和c是否越界再检查棋子颜色。顺序反了会导致ArrayIndexOutOfBoundsException。方向向量确保四个方向向量{1,0}, {0,1}, {1,1}, {1,-1}覆盖了所有可能连成五子的方向。解决单步调试checkWin函数。在一个新游戏的第5步在checkWin内部打印每个方向的count值看累加是否正确。5.3 游戏状态不同步或界面不更新现象落子后棋盘没显示或者状态提示没变。排查invalidate()调用在GobangView.setBoardData()方法中修改数据后是否调用了invalidate()或postInvalidate()来请求重绘监听器是否设置MainActivity是否正确地实现了GameController.GameStateListener并设置给了控制器GobangView的OnPiecePlayListener是否设置UI线程更新确保在onBoardChanged,onGameOver等回调中更新UI控件如TextView,Button时是在主线程进行的。如果控制器逻辑运行在子线程必须用runOnUiThread()或Handler包装UI操作。解决在关键回调方法开始处加日志确认它们被调用。检查Activity中设置监听器的代码行。5.4 内存泄漏预防问题在Activity中持有View和Controller的引用是正常的。但要小心在Controller或自定义View中持有Activity的Context或匿名内部类如Listener的强引用且其生命周期长于Activity这会导致Activity无法被回收。解决对于监听器考虑使用弱引用WeakReference。在Activity的onDestroy方法中将View的监听器置为nullmGobangView.setOnPiecePlayListener(null)并让GameController移除对Activity的引用如果可能。5.5 在不同屏幕尺寸上的适配问题现象在平板或不同分辨率的手机上棋盘显示不全或太小。解决在GobangView的onSizeChanged中我们根据View的实际宽高动态计算mCellSize这本身就是一个很好的适配方案。确保布局文件中GobangView的宽度和高度使用match_parent或0dp加weight使其能填充可用空间。可以考虑在onSizeChanged中根据屏幕密度(getResources().getDisplayMetrics().density)动态调整mOffset边距和画笔的StrokeWidth使在不同密度的屏幕上视觉粗细一致。这个项目从零开始实现涵盖了Android开发从UI到逻辑的完整链条。过程中你会遇到并解决具体的问题这种经验比单纯看教程要宝贵得多。建议你在实现基础功能后挑选一两个扩展方向深入做下去比如尝试实现一个简单的规则AI或者给棋子加上落子动画这会让你的项目脱颖而出。本文还有配套的精品资源点击获取