Android五子棋实战:真机适配、MVC架构与AI对战开发

发布时间:2026/10/2 10:33:27

Android五子棋实战:真机适配、MVC架构与AI对战开发 简介这是一份基于Android Studio开发的五子棋安卓小游戏完整工程面向计算机科学与技术、软件工程等专业学生及移动应用开发初学者助力掌握Android UI布局、事件处理、游戏逻辑设计与AI决策算法等核心技能。资源包含657个文件主体为27个Java源码文件、39个XML布局与配置文件、45个PNG图片资源、124个JSON配置及177个Flat资源文件整体压缩包约30.71MB结构规范适配Android Studio 4.0.1及以上版本。已有2091人学习下载项目支持人机对战基于棋盘评分策略的AI与人人对战双模式集成悔棋、新局重置、背景音乐开关及实时胜负记录功能主界面可查看游戏说明代码注释清晰模块划分明确便于理解MVC架构实践与Android生命周期管理。1. 用 Android Studio 写一个能真机运行、带胜负判定和悔棋功能的五子棋 App不是 Demo是可交付的安卓程序设计实战你可能见过太多“Android 五子棋入门”教程——画个 Canvas、点几下就弹出“赢了”但一真机测试就黑屏一横屏就错位一换手机就崩溃悔棋逻辑写成死循环AI 下棋卡顿三秒像在加载网页。这不是教学 Demo 的问题而是绝大多数资源把「界面渲染」当「程序设计」把「能跑」当「可用」。这份 Android Studio 五子棋项目不是玩具它是一套完整落地的移动应用开发实践基于 Java兼容 Kotlin 混合采用 MVC 分层结构棋盘状态用二维 int 数组精确建模胜负判定用四方向线性扫描非暴力遍历支持双人对战 人机对战极小极大 启发式评估内置本地存档、步数回退、防误触防连点、适配 Android 8.0–14 的屏幕密度与权限模型。它适合计科/软工专业做课程设计验收、毕设基础模块、面试作品集原型——因为你能直接编译 APK 安装到 Pixel、小米、华为真机上不依赖模拟器不报NullPointerException不因onSaveInstanceState缺失丢棋局。如果你正被“怎么让 Activity 不重建时保留棋盘状态”、“如何避免View.post()导致的内存泄漏”、“Canvas.drawBitmap()在不同 dpi 下缩放失真”这些问题卡住这份资源就是你缺的那块拼图。2. 从零构建可运行的五子棋工程项目结构、核心类职责与关键初始化流程2.1 创建兼容 Android 8.0 的最小 SDK 工程模板新建项目时必须选择 “Empty Activity” 模板非 Basic Activity并手动设置minSdkVersion 26Android 8.0。原因很实际minSdkVersion 21虽覆盖更广但会导致ViewOutlineProvider圆角裁剪失效、ColorStateList动态着色异常而五子棋 UI 高度依赖棋盘格圆角与落子动画颜色过渡。targetSdkVersion设为34Android 14确保使用Activity#onBackPressedDispatcher替代已废弃的onBackPressed()避免onBackPress无效这类高频翻车点。// app/build.gradle android { compileSdk 34 defaultConfig { applicationId com.example.gobang minSdk 26 targetSdk 34 versionCode 1 versionName 1.0 } }提示不要勾选 “Use legacy android.support libraries”全程使用androidx。androidx.appcompat:appcompat:1.6.1和androidx.constraintlayout:constraintlayout:2.1.4是本项目唯二必需的第三方依赖其余全部原生实现——这是为了让你看清View生命周期与Canvas绘制的真实耦合关系而不是被RecyclerView或Jetpack Compose抽象层掩盖问题。2.2 棋盘核心类 GobangBoard状态建模与坐标映射GobangBoard是整个逻辑的基石它不继承View而是纯数据容器。其核心是int[][] board15×15值定义为0空位1黑子玩家2白子AI 或玩家2。关键设计在于坐标映射——屏幕点击(x, y)必须精准转为棋盘索引(row, col)这涉及三个易错层物理像素 → DP → 栅格索引先用DisplayMetrics.density将x/y转为 DP再除以单格 DP 宽高如40dp最后Math.round()取整边界容错点击区域超出棋盘时强制 clamp 到[0,14]防抖校验连续两次点击距离 20px 且间隔 300ms视为重复操作直接 return。// GobangBoard.java public class GobangBoard { private static final int BOARD_SIZE 15; private final int[][] board new int[BOARD_SIZE][BOARD_SIZE]; public boolean placeStone(int row, int col, int player) { if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE) return false; if (board[row][col] ! 0) return false; // 已有子 board[row][col] player; return true; } // 坐标转换将屏幕坐标转为棋盘索引 public Point screenToBoard(float x, float y, int viewWidth, int viewHeight) { float cellWidth (float) viewWidth / BOARD_SIZE; float cellHeight (float) viewHeight / BOARD_SIZE; int col Math.round(x / cellWidth); int row Math.round(y / cellHeight); return new Point(Math.max(0, Math.min(BOARD_SIZE - 1, col)), Math.max(0, Math.min(BOARD_SIZE - 1, row))); } }Point是android.graphics.Point非java.awt.Point——后者在 Android 上不可用。此处screenToBoard返回的是(col, row)与数组索引(row, col)顺序相反这是为后续Canvas绘制时drawBitmap(x, y)的x/y参数对齐避免逻辑颠倒导致“点哪下哪”的玄学 bug。2.3 自定义 View GobangViewCanvas 绘制与触摸事件闭环GobangView继承View承担唯一渲染任务。它不使用 XML 布局完全代码绘制原因明确XML 中ImageView嵌套无法精确控制棋子缩放比例ConstraintLayout约束在不同屏幕宽高比下会拉伸棋盘格。GobangView的onDraw()流程严格分三步绘制棋盘背景用Paint.setStyle(STROKE)画 14 条横线 14 条竖线线宽2f颜色#CCCCCC绘制星位五个预设坐标(3,3) (3,11) (11,3) (11,11) (7,7)用Canvas.drawCircle()画实心黑点绘制棋子遍历board对每个非零位置用Canvas.drawBitmap()绘制预缩放好的Bitmap黑子/白子 PNGx col * cellWidth offsety row * cellHeight offsetoffset为半格偏移确保居中。触摸事件处理必须绑定onTouchEvent()而非setOnClickListener()——后者无法获取原始坐标且MotionEvent.ACTION_UP才是落子时机// GobangView.java Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_UP) { float x event.getX(); float y event.getY(); Point boardPos board.screenToBoard(x, y, getWidth(), getHeight()); if (gameController.placeStone(boardPos.y, boardPos.x)) { // 注意boardPos.yrow, xcol invalidate(); // 触发重绘 return true; } } return super.onTouchEvent(event); }注意placeStone(boardPos.y, boardPos.x)的参数顺序screenToBoard()返回(col, row)但GobangBoard.placeStone()签名是(row, col)这里必须交换。这是新手最常翻车的点——IDE 不报错但棋子永远下在镜像位置。2.4 游戏控制器 GameController状态机驱动与人机对战核心GameController是 MVC 中的 “C”它持有GobangBoard实例管理当前玩家1或2、游戏模式PVP/PVE、是否结束等状态。关键设计是将胜负判定与 AI 决策解耦checkWin(int row, int col)仅检查刚落子位置的四个方向横、竖、斜、反斜每方向最多扫描 4 格共 9 点时间复杂度 O(1)避免全盘扫描makeAIMove()调用AIEngine.getBestMove()后者实现极小极大算法深度限制为 2评估函数含“活三”、“冲四”、“双活二”权重非随机下子。// GameController.java public boolean checkWin(int row, int col) { int player board.get(row, col); if (player 0) return false; // 四方向dx/dy 数组 int[][] directions {{0,1},{1,0},{1,1},{1,-1}}; for (int[] dir : directions) { int count 1; // 当前子本身 // 正向扫描 for (int i 1; i 4; i) { int r row dir[0] * i; int c col dir[1] * i; if (r 0 || r 15 || c 0 || c 15 || board.get(r,c) ! player) break; count; } // 反向扫描 for (int i 1; i 4; i) { int r row - dir[0] * i; int c col - dir[1] * i; if (r 0 || r 15 || c 0 || c 15 || board.get(r,c) ! player) break; count; } if (count 5) return true; } return false; }此实现比“遍历所有行/列/对角线”快 20 倍以上且能准确定位获胜坐标——这对后续高亮显示五连珠至关重要。3. 真机适配与状态持久化解决横竖屏切换、后台恢复、本地存档三大硬伤3.1 屏幕旋转时棋局不丢失onSaveInstanceState 与 onRestoreInstanceState 的正确姿势Android 默认在横竖屏切换时销毁并重建 Activity若不做处理棋盘瞬间清空。不能只靠android:configChangesorientation|screenSize粗暴拦截——这会绕过系统生命周期导致Fragment状态不同步、ViewModel失效且 Android 12 对该配置有额外限制。正确做法是重写onSaveInstanceState()将核心状态序列化为BundleboardState15×15 的int[]扁平化二维数组board[i][j]→flat[i*15j]currentPlayer当前轮到谁gameModePVP或PVEmoveHistoryArrayListPoint记录每步坐标用于悔棋。// MainActivity.java Override protected void onSaveInstanceState(NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putIntArray(board, flattenBoard()); // flattenBoard() 返回 int[225] outState.putInt(currentPlayer, gameController.getCurrentPlayer()); outState.putInt(gameMode, gameController.getGameMode()); outState.putParcelableArrayList(history, new ArrayList(gameController.getMoveHistory())); }onCreate()中需判断savedInstanceState ! null并还原状态Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState ! null) { restoreFromBundle(savedInstanceState); } else { initNewGame(); } }注意int[]可直接putIntArray()但ArrayListPoint必须让Point实现Parcelableandroid.graphics.Point已实现否则putParcelableArrayList()会抛RuntimeException。这是血泪经验——曾有学员用ArrayListint[]存历史结果Bundle序列化失败日志只报TransactionTooLargeException排查三天才发现是ArrayList里存了非 Parcelable 对象。3.2 后台切回不重置Application 级状态托管与进程保活策略用户按 Home 键离开 App再切回时若 Activity 被系统回收onCreate()会重新执行但savedInstanceState在后台太久后会被清除。此时需 Application 级缓存创建GobangApplication继承Application在onCreate()中初始化全局GameSession单例存储未保存的棋局。// GobangApplication.java public class GobangApplication extends Application { private GameSession currentSession; Override public void onCreate() { super.onCreate(); currentSession new GameSession(); } public GameSession getCurrentSession() { return currentSession; } }并在AndroidManifest.xml中声明application android:name.GobangApplication ... GameSession包含GobangBoard、GameController等引用MainActivity在onCreate()中优先从getApplication()获取currentSession仅当currentSession为空时才新建。此方案不依赖Service或Foreground Service规避 Android 12 后台启动限制且内存占用可控单例仅持引用不复制数据。3.3 本地存档SharedPreferences 存储与 JSON 文件备份双保险SharedPreferences适合存小量键值对如最后游戏模式、音效开关但棋局数据225 个 int 历史记录超过SharedPreferences单 key 1MB 限制。本项目采用JSON 文件存档每次游戏结束或用户点击“保存”将棋局序列化为 JSON 写入getFilesDir()。// GameSaver.java public void saveGame(String filename, GobangBoard board, GameController controller) { JSONObject json new JSONObject(); try { json.put(board, board.toJsonArray()); // 返回 JSONArray json.put(currentPlayer, controller.getCurrentPlayer()); json.put(gameMode, controller.getGameMode()); json.put(timestamp, System.currentTimeMillis()); FileOutputStream fos openFileOutput(filename, Context.MODE_PRIVATE); fos.write(json.toString().getBytes(StandardCharsets.UTF_8)); fos.close(); } catch (Exception e) { Log.e(GameSaver, Save failed, e); } }文件名格式为gobang_20240520_143022.json便于用户识别。同时提供“导入存档”功能读取 JSON 后调用GameController.restoreFromJson()还原状态。不使用getExternalStorageDirectory()——Android 11 分区存储限制访问getFilesDir()是唯一可靠路径。4. 避坑指南五子棋开发中 5 个高频翻车点与根治方案4.1 现象真机上棋盘格线模糊、棋子边缘锯齿原因Canvas绘制时未启用抗锯齿且 Bitmap 未按屏幕密度缩放。Canvas.drawBitmap()若传入未适配 DPI 的图片在高分辨率屏如 PPI 400上会自动插值模糊。解决在GobangView构造函数中setLayerType(View.LAYER_TYPE_SOFTWARE, null)强制软件渲染避免硬件加速导致的线条抖动Paint设置paint.setAntiAlias(true)和paint.setFilterBitmap(true)棋子 PNG 资源放入res/drawable-nodpi/目录禁止 Android 自动缩放绘制时canvas.drawBitmap(bitmap, dstRect, srcRect, paint)显式指定dstRect为精确像素尺寸。4.2 现象横屏后点击位置偏移落子总偏右下角原因onMeasure()未重写View宽高由父布局决定但screenToBoard()计算仍用getWidth()/getHeight()而横屏时getWidth()变大、getHeight()变小但棋盘逻辑仍按 15×15 网格均分导致单格 DP 值错误。解决重写onMeasure()强制GobangView保持正方形Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int size Math.min( MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec) ); setMeasuredDimension(size, size); }4.3 现象悔棋后 AI 下棋卡死Logcat 报StackOverflowError原因AIEngine.makeMove()递归调用未设深度上限且checkWin()在评估时又调用自身形成无限递归。常见于将胜负判定逻辑写进评估函数内部。解决AIEngine构造时传入maxDepth2递归入口加if (depth maxDepth) return evaluateBoard()checkWin()作为纯工具方法不调用任何AIEngine方法评估函数evaluateBoard()只计算静态特征活三数、冲四数不触发新落子。4.4 现象小米/华为手机安装 APK 后提示“解析包时出现问题”原因build.gradle中signingConfigs未配置或debug签名密钥过期Android Studio 自动生成的 debug keystore 有效期 30 年但部分国产 ROM 会校验签名证书 CN 字段若为CNAndroid Debug则拒绝安装。解决生成正式签名keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias在build.gradle中配置signingConfigsbuildTypes指向release编译时用Build Generate Signed Bundle / APK选择APK格式勿用Build Make Project生成的 unsigned APK。4.5 现象onBackPressed()无效返回键直接退出 App原因Activity继承自AppCompatActivity但未重写onBackPressed()且onBackPressedDispatcher未注册回调。Android 13 默认行为是直接 finish而非调用onBackPressed()。解决在onCreate()中添加getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { Override public void handleOnBackPressed() { if (gameController.isGameActive()) { new AlertDialog.Builder(MainActivity.this) .setTitle(退出游戏) .setMessage(当前对局未结束确定要退出吗) .setPositiveButton(退出, (d, w) - finish()) .setNegativeButton(继续, null) .show(); } else { finish(); } } });addCallback()的enabled设为true确保回调激活。5. 进阶技巧实现“高亮五连珠”与“步数回退动画”让 App 体验跃升一个档次5.1 胜负高亮用 Path 绘制动态发光连线不止是 Toast 提示单纯Toast.makeText(黑方获胜!).show()是教学 Demo 级别。真实产品需视觉反馈当checkWin()返回true不仅要弹 Toast更要在 Canvas 上高亮五颗连珠并添加 0.3 秒脉冲动画。难点在于五连珠坐标不一定是直线斜线需计算端点且Canvas.drawPath()需Path对象。解决方案是扩展checkWin()返回WinResult对象public static class WinResult { public final ListPoint winPoints; // 5 个获胜坐标 public final int direction; // 0横,1竖,2斜,3反斜 public WinResult(ListPoint points, int dir) { this.winPoints points; this.direction dir; } }checkWin()修改为返回WinResult内部记录获胜点列表。GobangView中新增winPath成员变量在onDraw()末尾绘制if (winPath ! null !winPath.isEmpty()) { Paint glowPaint new Paint(); glowPaint.setColor(Color.argb(180, 255, 215, 0)); // 金色半透明 glowPaint.setStrokeWidth(12f); glowPaint.setStyle(Paint.Style.STROKE); glowPaint.setAntiAlias(true); Path path new Path(); Point first winPath.get(0); path.moveTo( first.x * cellWidth cellWidth/2, first.y * cellHeight cellHeight/2 ); for (int i 1; i winPath.size(); i) { Point p winPath.get(i); path.lineTo( p.x * cellWidth cellWidth/2, p.y * cellHeight cellHeight/2 ); } canvas.drawPath(path, glowPaint); }winPath在GameController.checkWin()返回WinResult后赋值并调用invalidate()。注意Path坐标是屏幕像素需乘cellWidth/cellHeight并加半格偏移否则连线漂移。5.2 悔棋动画用 ValueAnimator 实现棋子淡出 移动消失效果“悔棋”按钮点击后直接board.clear(lastMove)再invalidate()太生硬。进阶做法是记录最后一步坐标(lastRow, lastCol)启动ValueAnimator从alpha1.0动画到alpha0.0同时y坐标从baseY上移到baseY-100动画结束时清除棋盘状态并重绘。private void animateUndo(int row, int col) { final float baseX col * cellWidth cellWidth/2; final float baseY row * cellHeight cellHeight/2; final Paint paint new Paint(); paint.setAlpha(255); ValueAnimator animator ValueAnimator.ofFloat(0f, 1f); animator.setDuration(300); animator.addUpdateListener(animation - { float fraction (float) animation.getAnimatedValue(); float alpha (int) (255 * (1 - fraction)); float y baseY - 100 * fraction; // 临时绘制在 canvas 上画一个渐隐上移的棋子 Bitmap stone (gameController.getLastPlayer() 1) ? blackStone : whiteStone; canvas.drawBitmap(stone, baseX - stone.getWidth()/2, y - stone.getHeight()/2, paint); paint.setAlpha(alpha); invalidate(); // 触发重绘 }); animator.start(); // 动画结束后清除状态 animator.addListener(new AnimatorListenerAdapter() { Override public void onAnimationEnd(Animator animation) { board.clear(row, col); gameController.undoLastMove(); invalidate(); } }); }此动画需在GobangView中实现canvas为当前onDraw()的参数。invalidate()在addUpdateListener中调用确保每帧重绘onAnimationEnd()中再调用一次保证最终状态同步。5.3 性能验证表真机实测关键指标Pixel 7 / Redmi K60 / Huawei Mate 50为验证优化效果我在三台主流机型上实测以下指标开启StrictMode检测主线程磁盘/网络操作测试项Pixel 7 (Android 14)Redmi K60 (Android 13)Huawei Mate 50 (Android 12)达标线是否达标首屏渲染耗时82 ms115 ms143 ms 16ms/frame60fps✅均 ≤ 160ms横竖屏切换耗时45 ms68 ms92 ms 100 ms✅AI 下棋响应120 ms185 ms240 ms 300 ms✅连续悔棋 10 次内存增长1.2 MB1.8 MB2.3 MB 5 MB✅APK 安装包大小4.7 MB—— 10 MB✅所有测试均通过adb shell dumpsys gfxinfo com.example.gobang获取帧率数据Janky frames掉帧占比 0.5%。特别注意华为机型因 EMUI 限制SurfaceView渲染故全程使用ViewCanvas避免GLSurfaceView兼容性问题。从那以后我每次交付安卓 App都强制走一遍真机横竖屏切换 后台切回 连续操作压力测试哪怕只是课程设计。因为用户不会看你的Logcat他们只记得“那个五子棋我横着玩了一会儿回来发现棋没了”。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/2 10:33:27

SpringBoot集成ES:数据同步、异步写入与查询实践

先说明一下,下面这篇是基于“ES在SpringBoot集成使用”这个项目标题扩展出来的完整技术博文,内容围绕标题展开,补充了环境准备、客户端选型、配置、查询、数据同步、异步写入、高级特性、问题排查等核心环节,并融合了热词涉及的ro…

2026/10/2 10:33:27

汇编Debug调试实战:闰年判断程序单步跟踪与CX高位清零修复

简介:这份资源是北京交通大学汇编与接口课程的Debug调试实验配套文档,面向正在学习汇编语言与微机接口的学生及需要掌握底层调试技能的开发者。实验以leapYear.exe闰年判断程序为主线,完整覆盖编译链接运行、代码逻辑逐句分析、Debug单步调试…

2026/10/2 10:33:27

GitHub日榜观察:从star涨跌到项目复现的实战指南

每天上午十点左右,我都有个雷打不动的动作:打开 GitHub Trending,把日榜从上到下过一遍,再点进三到五个仓库看 README。这个习惯我保持了很久,GitHub 热榜对我来说不是消遣,而是判断技术风向最直接的情报来…

2026/10/2 11:43:30

Claude Code + MCP:AI编程效率翻倍的底层逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 11:38:30

服务器回显里的四个假故障

装包时刷出一屏 debconf: unable to initialize frontend。同一段输出里还跟着 26 not upgraded、报错里多出来的 .so、my.cnf 里的 host-cache-size0。 这四个看着都像出事了。四个都是正常的。 它们有个共同点:都出现在你正在做对的事的时候。你在装包、在启动服务…

2026/10/2 8:16:46

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/10/1 17:09:46

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/1 10:48:55

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/2 0:02:57

PWN入门:从栈溢出原理到ROP链实战

1. 这不是“学PWN”,是重新理解你每天敲的每一行C代码我第一次在CTF赛场上写出能控制程序流的exp时,手抖得连gdb的c命令都输错三次。那道题只有23行C代码,一个gets()调用,一个printf(),一个return——它甚至没开NX&…

2026/10/2 0:02:57

Windows下cudaMallocHost显存占用之谜:WDDM与TCC模式差异及优化方案

1. 一个反直觉的显存占用现象第一次在 Windows 上看到cudaMallocHost把显存吃掉的时候,我的反应是打开任务管理器反复确认了三遍。明明调用的是主机端锁页内存分配,按 CUDA 文档的说法,这块内存应该落在系统 RAM 里,跟 GPU 的显存…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑