从零实现安卓五子棋课设:棋盘建模、自定义View与人机对战

发布时间:2026/10/9 4:35:23
从零实现安卓五子棋课设:棋盘建模、自定义View与人机对战 简介面向计科学生与安卓初学者的五子棋对战小游戏完整工程基于Android Studio 4.0.1开发兼容近年官方版本。项目包含人机对战与人人对战两种玩法AI按棋盘落点得分决策支持悔棋、新游戏、背景音乐开关与比分记录可完整演示界面布局、触控事件、自绘棋盘及基础AI算法适合课程设计、结课作业或安卓入门实战。资源共657个文件以Java源码、XML布局与配置、Gradle构建脚本、APK安装包及PNG图片资源为主压缩包约30.71MB工程目录结构清晰便于直接导入、编译运行与二次修改。下载后可直接获得可运行安装包与全部源码免去从零搭建环境和设计整体框架的步骤有助于快速理解五子棋胜负判断、AI决策和安卓工程组织方式。目前已有2092人学习下载是移动应用开发学习者不错的参考案例。1. 五子棋课设为什么是安卓程序设计的标准答案计算机专业的课程设计清单里用 Android Studio 做五子棋小游戏几乎是每年都会出现的高频选题。它没有购物App那种复杂的页面跳转也不涉及服务端协议但恰好覆盖了安卓程序设计的核心环节自定义 View 绘制、触摸事件分发、游戏状态管理、资源适配和打包调试。很多同学最初觉得它太简单真正动手才发现坐标换算、胜负判定和回合管理任何一个细节都能让程序翻车。接下来会从零讲清楚棋盘建模、棋盘组件绘制、人机对战的落子策略和答辩前最容易踩的坑让你照着做就能交出一份能跑、能演示、能讲出设计思路的安卓程序课设作业。2. 棋盘建模与胜负判定先把规则引擎做得无懈可击棋盘是五子棋的大脑所有界面操作最终都要落到在某行某列放一颗某颜色的棋子这个动作上而所有展示逻辑也都要从棋盘状态里读数据。先写一个不依赖 Android 的纯 Java 逻辑类还是先画界面看起来只是顺序问题实际上决定了后面调试的难度。很多同学一上来就怼自定义 View等画完了才发现胜负判断写得漏洞百出在模拟器里反复点来点去也测不全面。反过来先把规则引擎独立成类任何一步都可以用单元测试快速验证界面部分只是调用它而已。2.1 用 int[][] 存棋盘而不是直接画布坐标标准五子棋棋盘是 15×15 路也有同学用 13 路或 19 路但课设答辩里老师最熟悉的还是 15 路。常见的做法是用一个 int[][] board 保存棋局状态0 表示空、1 表示黑棋、2 表示白棋。为什么不用 List 或者直接在画布坐标上判断因为二维数组的下标可以直接映射到第几行第几列做胜负判定时只需要围绕落子点查相邻位置逻辑清晰、性能也够。Android 的 View 绘制拿到的是像素坐标但游戏逻辑层不应该关心像素它只认行列索引坐标换算专门交给 View 层去处理。这样分层之后有一个非常实际的好处GobangGame 是一个纯 Java 类不依赖 Context、不依赖 ActivityAndroid Studio 新建项目时自带的 JUnit 测试环境可以直接对它做单元测试。后面人机对战的评分 AI 也是纯算法同样能脱离模拟器跑。答辩时老师问你游戏逻辑和界面是怎么解耦的这就是最直观的答案。public class GobangGame { public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2; private final int[][] board; private final int size; public GobangGame(int size) { this.size size; board new int[size][size]; } public boolean move(int row, int col, int player) { if (row 0 || row size || col 0 || col size) { return false; } if (board[row][col] ! EMPTY) { return false; } board[row][col] player; return true; } public int getSize() { return size; } public int[][] getBoard() { return board; } }逻辑说明move() 方法先做越界和占位校验通过才落子。这个校验非常关键触摸事件换算出来的行列索引是不是在棋盘范围内、这个位置是不是已经有棋子全部在这一层挡住。在这里返回 false 之后View 层就不必再写一堆 if 判断。getBoard() 直接暴露内部数组是为了让绘制层每帧读取棋局状态课设阶段这样写够用如果后续要做多人对战或网络同步再改成不可变视图或者加副本。参数说明size 是棋盘路数按 15 传入EMPTY、BLACK、WHITE 三个常量在逻辑层和绘制层共用建议把它们放在这个类里而不是在 View 里再定义一遍。很多同学习惯在 MyView 里又写一个 private static final int BLACK 1两份定义一旦不一致就会出现明明赢了却不弹窗的玄学问题。2.2 五子连珠判定局部计数比全盘遍历快且稳新手最容易交出来的判定写法是双重 for 循环遍历整个棋盘每遇到一个非空点就以它为起点往四个方向数连子。这种写法的问题不是不能用而是做了大量无用功每下一步棋都要把 225 个点全部扫一遍而且方向计数写得稍微绕一点就容易出 bug。实际上每次落子只影响以落子点为中心的那一行、一列和两条斜线只需要检查这四个方向上有没有连续五个同色棋子即可。public boolean checkWin(int row, int col) { int player board[row][col]; if (player EMPTY) { return false; } // 四个方向水平、垂直、主对角线、副对角线 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; } private int countDirection(int row, int col, int dr, int dc, int player) { int count 0; int r row dr; int c col dc; while (r 0 r size c 0 c size board[r][c] player) { count; r dr; c dc; } return count; }逻辑说明checkWin() 以当前落子点为中心沿四个方向分别向正反两个方向数同色棋子总数大于等于 5 就判胜。countDirection() 用一个 while 循环沿着方向增量累进直到遇到空位、异色棋子或越界。如果把正反两个方向的 count 加起来恰好是包含落子点在内的最长连子数。参数说明directions 数组存的是四个方向的单位增量{1,-1} 对应副对角线行增大、列减小。用数组存方向比写四段几乎一样的 if-else 分支更不容易漏方向新增判断六个方向之类的变体规则时也只需改数组。注意 countDirection 里用了行和列分别判断越界而不是一次性判断因为二维数组下标一旦越界会直接抛异常这个细节在边界棋子比如棋盘左上角上很容易暴露。2.3 悔棋与棋谱栈是后悔药的默认实现答辩时老师大概率会问一句能悔棋吗。如果没做悔棋场面会很难看做了之后这是可以展开讲两分钟的亮点。常见做法是用一个 Dequeint[] 记录每一步落子的行列索引悔棋时弹出栈顶把对应位置重置为 EMPTY。这里不建议用 ArrayList 然后 remove 末尾元素因为栈的语义更明确代码一眼就能看出后进先出。import java.util.ArrayDeque; import java.util.Deque; public class GobangGame { private final Dequeint[] history new ArrayDeque(); public boolean move(int row, int col, int player) { if (!moveRaw(row, col, player)) { return false; } history.push(new int[]{row, col}); return true; } public boolean undo() { if (history.isEmpty()) { return false; } int[] last history.pop(); board[last[0]][last[1]] EMPTY; return true; } private boolean moveRaw(int row, int col, int player) { if (row 0 || row size || col 0 || col size) { return false; } if (board[row][col] ! EMPTY) { return false; } board[row][col] player; return true; } }逻辑说明move() 里先调用 moveRaw() 完成实际落子落子成功后再把坐标压栈。undo() 弹出栈顶坐标并清空棋盘位置。这里用 int[]{row, col} 这个轻量数组做坐标对象两个元素的数组足够表达一个落子点不必专门建一个 Point 类。参数说明用 ArrayDeque 而不是 Stack 是因为 ArrayDeque 不是同步的单线程游戏场景性能更好而且官方文档本身也更推荐 ArrayDeque。有一点要注意如果做的是人机对战悔棋时需要把AI 刚下的那步和玩家刚下的那步一起弹出否则棋盘上会残留 AI 的棋子局面变成玩家凭空多走一手。我一般会在 undo 方法里返回实际弹出的步数让界面层决定是弹一步还是连续弹两步。这一步一定要在开发前就想好否则做到人机模式时再改悔棋逻辑很容易把双方的 history 搞串。提示逻辑层写完最好的验证方式不是跑模拟器而是写一个简单的 JUnit 测试把横五连、竖五连、斜五连、边界五连、禁手位这些用例全跑一遍。这一步能省下后面大量界面调试时间。3. 自定义棋盘组件绘制、触摸与坐标换算都交给 View棋盘组件是整个 App 最核心的界面元素也是安卓程序设计课程评分时老师最看重的地方。用 ImageView 拼出棋盘、用 Button 摆棋子能不能实现五子棋能但页面一复杂就非常痛苦而且老师一眼就能看出来你只是会摆控件没有真正理解安卓的绘图体系。自定义 View 的核心思路是把画什么、怎么画完全交给 View 的 onDraw()把用户在哪里点了交给 onTouchEvent()View 自己维护显示状态Activity 只负责接收回调通知。3.1 为什么课设里的棋盘组件要自己画很多人会问网上有现成的五子棋 HTML 页面打开浏览器就能玩为什么还要用 Android Studio 写一个原生 App这里面的差别恰恰是课设的考察点。HTML 五子棋是在浏览器里用 DOM 元素堆出来的而安卓程序设计的棋盘组件要求你理解 View 的测量、布局、绘制和事件分发四个环节。自定义组件做得好说明你对安卓的渲染机制有真实手感而不是只会拖控件。这个技能以后做图表库、做地图标注、做自绘控件都用得上。自定义 View 还有一个天然优势它把棋盘和棋子的绘制逻辑封装在一个类里Activity 的代码会非常干净。棋盘尺寸变化、屏幕旋转、不同分辨率适配全部集中在 View 内部处理Activity 不需要关心这些细节。这就是高内聚低耦合在安卓里的一个很直观的案例。3.2 onMeasure 与 onDraw把 15 路棋盘画到屏幕上自定义 View 的第一件事是测量。棋盘组件是一个正方形宽度和高度应该保持一致。在 onMeasure() 里把 View 的尺寸修正为正方形以较短边为准这样在不同屏幕密度的手机上都能保持比例。注意这个正方形不一定是布局里指定的 match_parent遇到底部导航栏或软键盘挤压以实际测量结果为准。public class GobangView extends View { private final Paint gridPaint new Paint(); private final Paint blackPaint new Paint(); private final Paint whitePaint new Paint(); private final GobangGame game; private int cellSize; private int boardOffset; public GobangView(Context context, AttributeSet attrs) { super(context, attrs); gridPaint.setColor(0xFF000000); gridPaint.setStrokeWidth(2f); blackPaint.setColor(0xFF111111); blackPaint.setStyle(Paint.Style.FILL); whitePaint.setColor(0xFFF5F5F5); whitePaint.setStyle(Paint.Style.FILL); game new GobangGame(15); boardOffset 0; } Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width MeasureSpec.getSize(widthMeasureSpec); int height MeasureSpec.getSize(heightMeasureSpec); int size Math.min(width, height); setMeasuredDimension(size, size); } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int boardSize game.getSize(); float boardWidth getWidth() - 2 * boardOffset; cellSize (int) (boardWidth / (boardSize - 1)); for (int i 0; i boardSize; i) { int y boardOffset i * cellSize; canvas.drawLine(boardOffset, y, getWidth() - boardOffset, y, gridPaint); int x boardOffset i * cellSize; canvas.drawLine(x, boardOffset, x, getHeight() - boardOffset, gridPaint); } for (int row 0; row boardSize; row) { for (int col 0; col boardSize; col) { int piece game.getBoard()[row][col]; if (piece GobangGame.BLACK || piece GobangGame.WHITE) { float cx boardOffset col * cellSize; float cy boardOffset row * cellSize; Paint paint (piece GobangGame.BLACK) ? blackPaint : whitePaint; canvas.drawCircle(cx, cy, cellSize * 0.42f, paint); } } } } }逻辑说明onMeasure() 里用 MeasureSpec.getSize() 取出父布局给的建议宽高取较小值作为正方形的边长。onDraw() 里先算 cellSize这里除以的是 (boardSize - 1)因为 15 路棋盘有 15 条线线间距是总宽度除以 14很多同学在这里除错画出来的棋盘会差一条线的距离。画棋子时按行列索引反推圆心的屏幕坐标注意 row 对应 y、col 对应 x别写反。参数说明gridPaint.setStrokeWidth(2f) 是网格线宽度2 像素在大多数手机上看起来刚好。如果你做的是平板适配建议用 dp 转 px 而不是直接写 2f。棋子半径取 cellSize * 0.42f 是为了让相邻棋子之间留一点空隙视觉上更干净如果设成 0.5f相邻两格的棋子会相切下满棋盘时画面很挤。3.3 落子交互与 Activity 通信坐标换算和接口回调用对才行棋盘画出来了接下来要让用户点下去能落子。onTouchEvent() 里接收手指坐标通过公式换算成行列索引先用手指坐标减去 boardOffset再除以 cellSize然后四舍五入。这个换算看起来简单实际是整篇代码里最容易出 bug 的地方。public class GobangView extends View { private int currentPlayer GobangGame.BLACK; private OnMoveListener listener; public interface OnMoveListener { void onMove(boolean win, int player); } public void setOnMoveListener(OnMoveListener listener) { this.listener listener; } Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() ! MotionEvent.ACTION_DOWN) { return true; } float x event.getX(); float y event.getY(); int col Math.round((x - boardOffset) / cellSize); int row Math.round((y - boardOffset) / cellSize); if (row 0 || row game.getSize() || col 0 || col game.getSize()) { return true; } if (game.move(row, col, currentPlayer)) { boolean win game.checkWin(row, col); if (listener ! null) { listener.onMove(win, currentPlayer); } invalidate(); } return true; } }逻辑说明只处理 ACTION_DOWN避免手指按下后拖动造成重复落子。换算行列时用 Math.round() 而不是直接强转 int因为手指点在某条线的左侧还是右侧决定了它更接近哪一个交叉点四舍五入的误差更小。落子和胜负判断都在逻辑层完成View 只负责把结果通过 listener 抛给 Activity。参数说明currentPlayer 是 View 内部的当前手标记初始为黑棋。它在每次 onMove 回调之后需要更新更规范的做法是把它移到 GameController 里集中管理后面第 4 章会展开。onMove 回调里的两个参数是给 Activity 用的win 决定是否弹胜利对话框player 决定弹窗里显示黑棋胜还是白棋胜。3.4 布局接入与日志打印让组件真正跑起来在布局文件里把组件放进去并在 Activity 里注册回调棋盘就跑起来了。这里有一个容易翻车的点自定义 View 的类名必须写全限定名否则布局解析器找不到这个类直接抛异常。com.example.gobang.GobangView android:idid/gobangView android:layout_widthmatch_parent android:layout_heightmatch_parent android:padding16dp /GobangView board findViewById(R.id.gobangView); board.setOnMoveListener((win, player) - { Log.d(Gobang, win win player player); if (win) { String msg (player GobangGame.BLACK) ? 黑棋胜 : 白棋胜; Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); } });布局文件里 android:padding16dp 会在 View 四周留出内边距。自定义 View 里需要在构造器读取这个 padding 并赋给 boardOffset否则棋盘会顶着边。Activity 里先 findViewById 拿到实例再 setOnMoveListener 注册回调落子和胜负结果都通过这个回调通知界面。这个阶段建议把 Log.d 加上它能帮你确认每次触摸换算出来的 row 和 col 是否合理——比如点棋盘正中间换算出来应该是 7,7如果得到 7,6 或 6,7说明 offset 或 cellSize 算错了。提示boardOffset 一定要在 onDraw 里计算一次并保存不要在 onTouchEvent 里重新算。因为绘制和触摸用的是同一套坐标系绘制时算出来的值触摸时必须复用两处各算一遍很可能因为执行顺序或布局变化而不一致。4. 人机对战与回合管理一个能应付答辩的评分 AI双人模式把棋盘画好、落子和胜负判定做完就能演示了但大多数课设要求里会写着实现人机对战这一条。很多同学一听到AI两个字就头大直接去抄一个几百行的 Minimax 剪枝代码结果完全看不懂、改不动答辩被老师一问就露馅。实际上课设阶段用评分法启发式评估就足够了它代码量小、思路清晰而且能打出像模像样的棋。4.1 课设 AI 选评分法而不是搜索树五子棋的分支因子非常大15×15 棋盘上第一手就有 225 个可选点Minimax 即使配合 Alpha-Beta 剪枝想要达到能看的水平也需要做很多工程优化比如走法排序、启发式裁剪、杀棋检测等。这些技术不是说不能学而是课设周期内要写清楚、讲明白成本太高。评分法的思路完全不一样对每一个空格子假设 AI 落在这里算一个进攻分假设玩家落在这里算一个防守分加权求和后取最高分的位置落子。它不做搜索只看一步但防守权重和连子分数调好之后AI 已经知道堵活三、挡冲四了。答辩时老师最关心的不是 AI 多强而是你能不能讲清楚AI 是怎么决定下一步走哪的。评分法是所有棋类 AI 的基础从这个切入点讲起比直接背一段 Minimax 代码效果好得多。4.2 给每个空位打分进攻分与防守分的权重怎么配评分函数的核心是连子数到分数的映射。这里给出一个课设阶段最容易理解的版本public class GobangAi { private static final int[][] DIRECTIONS {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; public int evaluatePosition(GobangGame game, int row, int col, int player) { int score 0; for (int[] dir : DIRECTIONS) { int count 1; count countDirection(game, row, col, dir[0], dir[1], player); count countDirection(game, row, col, -dir[0], -dir[1], player); score scoreOfCount(count); } return score; } private int countDirection(GobangGame game, int row, int col, int dr, int dc, int player) { int count 0; int r row dr; int c col dc; while (r 0 r game.getSize() c 0 c game.getSize() game.getBoard()[r][c] player) { count; r dr; c dc; } return count; } private int scoreOfCount(int count) { switch (count) { case 5: return 100000; case 4: return 10000; case 3: return 1000; case 2: return 100; case 1: return 10; default: return 0; } } public int[] findBestMove(GobangGame game, int aiPlayer, int humanPlayer) { int bestScore -1; int bestRow -1; int bestCol -1; int[][] board game.getBoard(); int size game.getSize(); for (int row 0; row size; row) { for (int col 0; col size; col) { if (board[row][col] ! GobangGame.EMPTY) { continue; } int attackScore evaluatePosition(game, row, col, aiPlayer); int defendScore evaluatePosition(game, row, col, humanPlayer); int total attackScore (int) (defendScore * 0.9); if (total bestScore) { bestScore total; bestRow row; bestCol col; } } } return new int[]{bestRow, bestCol}; } }逻辑说明evaluatePosition 是在假设某个选手在 (row, col) 落子的前提下统计四方向连子数并映射为分数。findBestMove 遍历所有空位分别算 AI 进攻分和玩家进攻分防守分乘 0.9 系数——这个系数的作用是让 AI 在进攻机会和防守需要同等强烈时优先进攻。课设答辩时老师一定会问为什么乘 0.9答防守权重略低是为了避免 AI 过于保守就很完整。参数说明分数从 10 到 100000 跨度非常大让连五和活四的优先级远高于其他棋形。如果你觉得 AI 太激进把防守系数从 0.9 提到 1.1它会变得更保守甚至主动去堵玩家的活二。这里的实现没有处理跳子比如 X_XXX 中间空一格的情况所以 AI 对跳活三的威胁不敏感这是简化版本可以接受的局限答辩时主动说出来反而加分。4.3 回合状态机双人、人机、胜负弹窗和 300ms 延迟游戏逻辑需要一个轻量级控制器来管理当前模式、轮到谁、游戏是否结束。这个控制器可以放在 Activity 里也可以单独建一个 GameController 类。推荐单独建类因为 Activity 本身要处理的东西够多了代码堆在一起很难看。public class GameController { public static final int MODE_TWO_PLAYER 0; public static final int MODE_AI 1; private final GobangView board; private final GobangAi ai; private int mode; private int currentPlayer GobangGame.BLACK; private boolean gameOver false; public GameController(GobangView board, int mode) { this.board board; this.mode mode; this.ai new GobangAi(); board.setOnMoveListener(this::onMove); } public void onMove(boolean win, int player) { if (win) { gameOver true; return; } currentPlayer (currentPlayer GobangGame.BLACK) ? GobangGame.WHITE : GobangGame.BLACK; if (mode MODE_AI currentPlayer GobangGame.WHITE) { board.postDelayed(() - { int[] move ai.findBestMove(board.getGame(), GobangGame.WHITE, GobangGame.BLACK); boolean ok board.getGame().move(move[0], move[1], GobangGame.WHITE); if (ok) { boolean aiWin board.getGame().checkWin(move[0], move[1]); if (aiWin) { gameOver true; } board.invalidate(); } }, 300); } } }逻辑说明onMove 是棋盘 View 回调上来的入口。先看是否胜利胜利就置 gameOver 并弹出结果否则切换到对方回合。人机模式下如果当前回合是 AI就 postDelayed 延迟 300ms 再落子。这个延迟很重要不加的话 AI 瞬间落子玩家还没看清上一步棋在哪体验非常突兀。参数说明MODE_TWO_PLAYER 和 MODE_AI 在 GameController 里用常量定义Activity 创建 Controller 时传入。currentPlayer 的切换逻辑集中在 onMove 中不要在 Activity 和 View 里各写一份否则很容易出现黑白双方连走两步的 bug。postDelayed 的 300ms 是一个手感比较好的值可以根据需要调整为 500ms 或者更短。5. 避坑与常见问题答辩前让安卓程序不翻车的 5 个排查点五子棋项目代码量不大但坑一点都不少。这一章集中记录做课设和帮学生改代码时最常见的问题按现象 → 原因 → 解决的方式写每一条都是真实翻车现场。5.1 旋转屏幕后棋盘清零Activity 重建与状态保存现象下到一半手机横过来回来发现棋盘全空了从头开始下。 原因默认配置下屏幕旋转会导致 Activity 销毁重建存内存里的棋盘数组全部丢失。 解决两个做法二选一。一是让 GobangGame 实现 Serializable 或 Parcelable在 onSaveInstanceState 里保存、onRestoreInstanceState 里恢复二是在 AndroidManifest.xml 的 activity 标签里加 android:configChangesorientation|screenSize旋转时不重建 Activity。课设阶段用 configChanges 最省事但答辩老师如果问起来要能说出为什么平时不建议这样用——因为正常情况下我们希望重建来适配不同布局但游戏棋盘不涉及布局切换可以接受。5.2 onDraw 里 new 对象导致掉帧现象落子后棋盘重绘有明显卡顿拖动窗口或快速落子时更明显模拟器里尤为严重。 原因onDraw 每帧调用如果在里面 new Paint、new Path 甚至 new ArrayListGC 会被频繁触发系统在低端机或模拟器上直接掉帧。 解决所有 Paint、Path 在构造器里初始化好每次重绘只改坐标参数不新建对象。如果实在需要在 onDraw 里创建轻量对象考虑把它们提到类字段里复用。判断这个问题最直接的方式是打开开发者选项里的显示 Surface 更新或者用 Android Studio 自带的 Profiler 看 GC 频率。5.3 落子偏了一个格子坐标换算里的 offset 之争现象明明点在格子上棋子却落在隔壁格子而且越靠近棋盘边缘偏得越远。 原因boardOffset 没有正确参与换算。比如布局里设了 padding但触摸换算时直接用 x / cellSize没减掉偏移量。 解决onTouchEvent 里换算行列时先减去 boardOffset 再除以 cellSize。boardOffset 在 onDraw 里计算一次并保存不要在 onTouchEvent 里重新算。另外Math.round() 比直接强转 int 更不容易出现负偏差因为强转会直接截断小数而 round 会四舍五入到最近邻。5.4 模拟器卡顿与 adb 连不上环境和构建版本排查现象模拟器启动慢或者启动后黑屏adb devices 里看不到设备偶尔还有安装失败INSTALL_FAILED_OLDER_SDK之类的报错。 原因模拟器加速没开启磁盘镜像路径有中文或空格或者项目里的 buildToolsVersion 和本地 SDK 不一致。 解决在 SDK Manager 里检查模拟器加速组件是否安装项目 build.gradle 的 compileSdkVersion 和 buildToolsVersion 保持默认推荐值不要手动改到很新的版本。Android Studio 安装包建议从官方渠道下载第三方便携版经常缺 SDK 组件排查起来非常折磨。5.5 资源重复错误从别的项目复制代码时的暗雷现象同步或打包时报 Duplicate resources提示 res/values/strings.xml 里某条 name 重复。 原因最常见的是从别的项目复制自定义 View 代码时把对方的 attrs.xml 或 strings.xml 一并复制过来了里面有一个同名的字符串或属性定义。 解决打开 Resource Manager 面板看重复项直接删掉旧的那份。这不是改 build.gradle 能绕过去的资源名是全局唯一的。以后复制代码时只复制 .java 文件资源文件逐个确认名称再往里放。提示以上 5 条里坐标换算和 Activity 重建是出现频率最高的两个。如果调试时发现落子不准第一步不是改触摸代码而是先把 row 和 col 打印出来和实际点的位置对比一遍定位是换算问题还是绘制问题。6. 从能跑到能讲动画、测试与答辩话术的最后一公里很多同学做完一个课设只满足于能跑但优秀安卓程序设计这个评分标准要求的不只是能跑还要能演示、能讲。最后这一章聊三个进阶方向把胜利瞬间做得有反馈、用单元测试证明逻辑正确、以及准备一段经得起追问的答辩话术。给胜利判定加上动画和音效是最直接的加分项。五子棋不需要复杂动画一个简单的落子缩放动画就能让手感提升一个档次。做法是在 onDraw() 里给棋子半径加一个插值落子时半径从 0 慢慢放大到 cellSize * 0.42f用 ValueAnimator 驱动300ms 的 duration 就够。音效用 SoundPool 播放一个很短的啪声资源文件放在 res/raw 下注意声音文件要小于 100KB否则会增加安装包体积。这些细节不需要多一两个就足够让演示看起来完整。代码写好只代表我认为对用 JUnit 把核心算法验证一遍才叫我知道对。在 test 目录下写一个 GobangGameTest测试横五连、竖五连、斜五连、边界落子、重复落子和悔棋六个用例。比如在 (7,7)、(7,8)、(7,9)、(7,10)、(7,11) 依次落黑棋checkWin(7,11) 必须返回 true。这些测试跑通之后答辩时直接展示 Android Studio 的 Test Runner 界面比口头说我测试过有力得多。答辩演示时老师的关注点通常集中在三块界面是自绘还是拼图、AI 怎么决定落子、异常情况有没有处理。准备一段两分钟的话术先打开棋盘双人模式下一手黑一手白展示落子和胜负弹窗然后切换到人机模式故意走出一个活三让 AI 堵你最后点悔棋说明栈的工作原理。老师如果追问AI 为什么下这里把评分法的思路讲一遍遍历空位、算进攻分和防守分、加权取最大。讲清楚这三个问题课设的分数基本就稳了。做完这个项目最深的感受是五子棋看起来简单但真正把棋盘建模、绘制、交互、AI 和状态管理串起来之后你对安卓应用结构的理解完全是另一层。我当年第一次做的版本就栽在坐标换算上落子总是歪一格后来打日志才发现是 padding 没减那个教训到现在写自定义 View 都还记得。如果你也能把这类问题一个个解决掉这份课设就真正从能跑变成了能讲、能改、能扩展的作品——希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询