尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Android五子棋开发实战:从自定义View到游戏逻辑完整实现

发布时间:2026/9/4 8:02:21

资讯中心
01
ARTICLE

Android五子棋开发实战:从自定义View到游戏逻辑完整实现

Android五子棋开发实战:从自定义View到游戏逻辑完整实现
简介本资源是一个基于Android Studio开发的五子棋实战项目面向Android初学者与移动应用开发学习者旨在通过完整游戏案例掌握UI布局、事件响应、游戏逻辑封装与Kotlin编程实践。压缩包共49个文件含17个XML布局与配置文件定义界面结构与资源引用、15个PNG/JPG图像资源棋盘、棋子及UI图标、4个Kotlin源码文件实现MainActivity、棋盘控制、胜负判定等核心逻辑以及Gradle构建脚本、Git配置与IDE配置文件等整体仅877KB轻量易导入。已有2304人学习下载项目采用清晰模块化设计ChessBoard类管理棋局状态GameRule类封装五连判断算法MoveValidator保障落子合法性并配备重新开始功能与轮次提示代码结构规范、注释充分适合作为课程设计参考、毕业设计基础模板或Android进阶练习范例。1. 项目概述与核心价值最近在整理自己的项目库翻到了一个几年前用Android Studio做的五子棋小游戏。这个项目虽然不大但麻雀虽小五脏俱全它几乎涵盖了Android应用开发从界面绘制、事件处理到核心逻辑判断的完整流程。对于想从“Hello World”迈向第一个完整功能应用的Android新手来说五子棋是一个绝佳的练手项目。它不像电商、社交App那样业务复杂但又比计算器、记事本多了不少交互和逻辑深度能让你在实现一个具体游戏规则的过程中把Activity、View、事件监听、自定义View、算法这些知识点都串起来用一遍。很多人学Android开发跟着教程敲完基础控件就卡住了不知道下一步该做什么。这个五子棋项目就是一个很好的“下一站”。你不仅能学会如何用Java或Kotlin在Android Studio里搭建一个可运行的应用更重要的是你会亲身体验到如何将脑海中的游戏规则比如“五子连珠即获胜”翻译成严谨的代码逻辑。这个过程会逼着你去思考棋盘的数据结构用什么二维数组、落子事件怎么响应、输赢判断算法如何设计四个方向遍历等等实际问题。做完之后你对Android开发的理解会从“知道有哪些组件”跃升到“知道如何用组件解决问题”的层面。2. 项目整体设计与思路拆解2.1 技术选型与架构思考做一个五子棋App技术上有很多条路可以走。你可以用最传统的ActivityView通过重写onDraw方法在Canvas上画棋盘和棋子也可以用更现代的Jetpack Compose声明式UI来构建。对于初学者我强烈建议从自定义View入手。原因很简单自定义View是理解Android绘图机制和触摸事件传递的基石。虽然Compose是未来趋势但掌握了View体系你再学Compose会感觉降维打击知其然更知其所以然。整个项目的骨架可以这样设计一个主Activity它承载一个全屏的自定义View我们叫它GobangView作为棋盘。GobangView内部需要维护几个核心数据棋盘数据模型一个15x15的二维整型数组board就足够了。用0表示空位1表示黑子2表示白子。这是整个游戏状态的核心。当前执子方一个布尔变量isBlackTurn标记当前该黑棋下还是白棋下。游戏状态比如GAME_STATE_PLAYING进行中、GAME_STATE_BLACK_WIN黑胜、GAME_STATE_WHITE_WIN白胜、GAME_STATE_DRAW平局五子棋较少见但可预留。为什么要用二维数组而不是更复杂的结构因为对于棋盘这种规整的网格数组的随机访问效率是最高的坐标(i, j)直接对应数组board[i][j]在判断输赢时需要高频访问周围棋子状态数组的优势就体现出来了。2.2 核心功能模块划分基于以上设计我们可以把项目分解成几个相对独立的模块来逐个击破UI绘制模块负责在onDraw方法中根据board数组和游戏状态绘制棋盘网格、黑白棋子、以及可能的胜负提示。触摸交互模块重写onTouchEvent方法将用户触摸的屏幕坐标(x, y)换算成棋盘数组的索引(row, col)并触发落子逻辑。游戏逻辑模块这是大脑。包含落子有效性判断该位置是否已有子、胜负判定算法核心中的核心、回合切换、以及悔棋、重新开始等功能的逻辑。状态控制模块管理游戏流程比如点击重新开始按钮后如何重置board数组和游戏状态。这种模块化的思考方式能让你在编码时不至于一团乱麻。你可以先实现绘制一个静态棋盘再加上触摸落子最后攻克胜负判断这个最难的部分。3. 核心细节解析与实操要点3.1 棋盘绘制从像素到逻辑坐标的映射绘制棋盘听起来简单但第一个坑往往就在这里。假设我们的自定义View宽高是viewWidth和viewHeight棋盘是15x15那么每个格子的宽度cellSize应该是多少通常我们会让棋盘居中显示并留出一些边距。// 示例代码在onDraw或onSizeChanged中计算 private float cellSize; // 每个格子的边长像素 private float boardPadding; // 棋盘边距 private float boardSize; // 棋盘总边长像素 Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 取View宽高中的较小值并留出边距保证棋盘是正方形 int minSize Math.min(w, h); boardPadding minSize * 0.05f; // 边距为5% boardSize minSize - 2 * boardPadding; cellSize boardSize / 14; // 注意是14不是15。15个点之间有14个间隔。 }这里有个关键细节格子数 vs 间隔数。15x15的棋盘有15行15列棋子落点但这些落点之间只有14个间隔。所以计算cellSize时要用boardSize / 14。如果除以15画出来的棋盘最右边和最下边会多出一截空白棋子对不齐。绘制网格线就是循环画横线和竖线Paint gridPaint new Paint(); gridPaint.setColor(Color.BLACK); gridPaint.setStrokeWidth(2); // 画横线 for (int i 0; i 15; i) { float startX boardPadding; float startY boardPadding i * cellSize; float stopX boardPadding boardSize; canvas.drawLine(startX, startY, stopX, startY, gridPaint); } // 画竖线类似...3.2 触摸事件处理精准的坐标转换用户触摸屏幕后我们需要把触摸点(eventX, eventY)转换成棋盘的(row, col)索引。这里涉及一个“吸附”逻辑因为手指很难精准点在一个交叉点上我们需要设定一个吸附半径如果触摸点距离某个交叉点足够近就认为用户想落子在那里。Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN) { float touchX event.getX(); float touchY event.getY(); // 1. 判断触摸点是否在棋盘区域内 if (touchX boardPadding || touchX boardPadding boardSize || touchY boardPadding || touchY boardPadding boardSize) { return false; // 点在棋盘外不处理 } // 2. 将像素坐标转换为棋盘索引浮点数 float relativeX touchX - boardPadding; float relativeY touchY - boardPadding; float exactRow relativeY / cellSize; // 可能得到如 3.7 这样的值 float exactCol relativeX / cellSize; // 3. 四舍五入到最近的整数索引 int row Math.round(exactRow); int col Math.round(exactCol); // 4. 边界检查防止四舍五入到15数组越界 row Math.max(0, Math.min(14, row)); col Math.max(0, Math.min(14, col)); // 5. 调用落子逻辑 if (placePiece(row, col)) { invalidate(); // 重绘视图 checkWinner(row, col); // 检查是否获胜 } return true; } return super.onTouchEvent(event); }注意Math.round是“四舍六入五成双”的银行家舍入法在边界情况下可能不如直接(int)(exactRow 0.5)来得直观可控。但在这个场景下差别不大。关键是一定要做数组索引的边界检查这是避免ArrayIndexOutOfBoundsException的必备操作。3.3 胜负判定算法思路与优化这是五子棋项目的算法核心。最直观的方法是每当新落下一颗子位置(row, col)颜色currentColor就以该子为中心向四个方向水平、垂直、左上-右下、右上-左下进行搜索统计每个方向上的连续同色棋子数。基础实现思路以水平方向为例向左搜索col-1,col-2... 直到棋盘边界或棋子颜色不同计数leftCount。向右搜索col1,col2... 直到棋盘边界或棋子颜色不同计数rightCount。总连续数 leftCount 1 rightCount1是中心子本身。如果总连续数 5则判定当前执子方获胜。其他三个方向同理。private boolean checkWinner(int row, int col) { int currentColor board[row][col]; // 四个方向向量水平(0,1)、垂直(1,0)、左上右下(1,1)、右上左下(1,-1) 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], currentColor); // 向反方向搜索 count countDirection(row, col, -dir[0], -dir[1], currentColor); if (count 5) { // 游戏结束当前执子方获胜 gameState (currentColor BLACK) ? GAME_STATE_BLACK_WIN : GAME_STATE_WHITE_WIN; showWinnerDialog(); return true; } } // 检查是否平局棋盘下满 if (isBoardFull()) { gameState GAME_STATE_DRAW; showDrawDialog(); return true; } return false; } private int countDirection(int row, int col, int dRow, int dCol, int color) { int count 0; int r row dRow; int c col dCol; while (r 0 r 15 c 0 c 15 board[r][c] color) { count; r dRow; c dCol; } return count; }这个算法的时间复杂度是O(1)因为无论棋盘多大它只检查固定四个方向上的有限格子最多各检查4格。效率完全足够。实操心得在实现countDirection时循环的边界条件(r 0 r 15 c 0 c 15)一定要写在board[r][c] color之前利用逻辑与的短路特性防止数组越界。这是新手很容易忽略的一个错误点。4. 完整实现流程与关键代码4.1 环境搭建与项目初始化首先确保你的Android Studio已经安装配置好。创建一个新的Empty Activity项目语言选择Java或Kotlin本文以Java为例。在res/layout/activity_main.xml中我们只需要一个全屏的FrameLayout用于承载我们的自定义棋盘View。?xml version1.0 encodingutf-8? FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:idid/container android:layout_widthmatch_parent android:layout_heightmatch_parent /FrameLayout然后在MainActivity.java的onCreate方法中将我们自定义的GobangView添加到这个容器中。public class MainActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FrameLayout container findViewById(R.id.container); GobangView gobangView new GobangView(this); container.addView(gobangView); } }4.2 自定义GobangView的实现骨架接下来是重头戏创建GobangView类。建议单独创建一个GobangView.java文件。public class GobangView extends View { // 常量定义 private static final int EMPTY 0; private static final int BLACK 1; private static final int WHITE 2; private static final int GAME_STATE_PLAYING 0; private static final int GAME_STATE_BLACK_WIN 1; private static final int GAME_STATE_WHITE_WIN 2; private static final int GAME_STATE_DRAW 3; // 核心数据 private int[][] board new int[15][15]; // 棋盘数据 private boolean isBlackTurn true; // 黑子先行 private int gameState GAME_STATE_PLAYING; // 绘制相关 private Paint gridPaint, blackPiecePaint, whitePiecePaint, winLinePaint; private float cellSize, boardPadding, boardSize; public GobangView(Context context) { this(context, null); } public GobangView(Context context, Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 初始化画笔 gridPaint new Paint(); gridPaint.setColor(0xFF8B4513); // 深棕色棋盘 gridPaint.setStrokeWidth(3); gridPaint.setAntiAlias(true); blackPiecePaint new Paint(); blackPiecePaint.setColor(Color.BLACK); blackPiecePaint.setStyle(Paint.Style.FILL); blackPiecePaint.setAntiAlias(true); whitePiecePaint new Paint(); whitePiecePaint.setColor(Color.WHITE); whitePiecePaint.setStyle(Paint.Style.FILL); whitePiecePaint.setStrokeWidth(2); whitePiecePaint.setAntiAlias(true); // 白棋通常加一个黑边更清晰 whitePiecePaint.setStyle(Paint.Style.FILL); // 可以用两个圆叠加来实现带边框的白棋这里先简化 winLinePaint new Paint(); winLinePaint.setColor(Color.RED); winLinePaint.setStrokeWidth(5); winLinePaint.setAntiAlias(true); // 初始化棋盘数据 resetGame(); } Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 计算棋盘和格子尺寸代码见上一节 int minSize Math.min(w, h); boardPadding minSize * 0.05f; boardSize minSize - 2 * boardPadding; cellSize boardSize / 14; } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBoard(canvas); drawPieces(canvas); if (gameState ! GAME_STATE_PLAYING) { drawGameOver(canvas); } } // 绘制棋盘网格 private void drawBoard(Canvas canvas) { // 绘制背景可选 canvas.drawColor(0xFFFFE4B5); // 米黄色背景 // 画横线竖线代码见上一节 // ... } // 根据board数组绘制棋子 private void drawPieces(Canvas canvas) { for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (board[i][j] ! EMPTY) { float centerX boardPadding j * cellSize; float centerY boardPadding i * cellSize; float radius cellSize * 0.4f; // 棋子半径是格子大小的40% if (board[i][j] BLACK) { canvas.drawCircle(centerX, centerY, radius, blackPiecePaint); } else { // 画白棋可以先画一个白圆再画一个稍细的黑圆作为边框 canvas.drawCircle(centerX, centerY, radius, whitePiecePaint); Paint borderPaint new Paint(whitePiecePaint); borderPaint.setColor(Color.BLACK); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(2); canvas.drawCircle(centerX, centerY, radius, borderPaint); } } } } } // 游戏结束时的绘制比如高亮显示五连珠 private void drawGameOver(Canvas canvas) { // 可以在这里绘制获胜的五子连线需要记录获胜的起始和结束位置 // 略... } // 触摸事件处理代码见上一节 Override public boolean onTouchEvent(MotionEvent event) { // ... } // 落子逻辑 private boolean placePiece(int row, int col) { if (gameState ! GAME_STATE_PLAYING) { return false; // 游戏已结束不能落子 } if (board[row][col] ! EMPTY) { return false; // 该位置已有棋子 } board[row][col] isBlackTurn ? BLACK : WHITE; isBlackTurn !isBlackTurn; // 切换执子方 return true; } // 胜负判定代码见上一节 private void checkWinner(int row, int col) { // ... } private boolean isBoardFull() { for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (board[i][j] EMPTY) { return false; } } } return true; } // 重新开始游戏 public void resetGame() { for (int i 0; i 15; i) { Arrays.fill(board[i], EMPTY); } isBlackTurn true; gameState GAME_STATE_PLAYING; invalidate(); // 请求重绘 } }这个骨架已经包含了最核心的功能。运行起来你应该能看到一个可以交互的五子棋棋盘了。4.3 功能增强悔棋与重新开始一个基本的游戏还需要控制功能。我们可以在屏幕下方添加一个简单的控制栏。修改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 FrameLayout android:idid/board_container android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 !-- GobangView将通过代码添加到这里 -- /FrameLayout LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:gravitycenter android:padding10dp Button android:idid/btn_undo android:layout_widthwrap_content android:layout_heightwrap_content android:text悔棋 / Button android:idid/btn_restart android:layout_widthwrap_content android:layout_heightwrap_content android:text重新开始 android:layout_marginStart20dp/ /LinearLayout /LinearLayout在MainActivity中绑定按钮并实现功能。悔棋功能需要在GobangView中维护一个落子历史记录。public class GobangView extends View { // ... 其他成员变量 private Stackint[] moveHistory new Stack(); // 用于悔棋记录每一步的[row, col] private boolean placePiece(int row, int col) { // ... 条件判断同上 board[row][col] isBlackTurn ? BLACK : WHITE; moveHistory.push(new int[]{row, col}); // 记录这一步 isBlackTurn !isBlackTurn; return true; } // 悔棋方法 public boolean undo() { if (moveHistory.isEmpty() || gameState ! GAME_STATE_PLAYING) { return false; } int[] lastMove moveHistory.pop(); int row lastMove[0]; int col lastMove[1]; board[row][col] EMPTY; // 清空该位置 isBlackTurn !isBlackTurn; // 回合回退 invalidate(); return true; } public void resetGame() { // ... 清空棋盘 moveHistory.clear(); // 清空历史 // ... 重置状态 } }然后在MainActivity中设置按钮点击监听public class MainActivity extends AppCompatActivity { private GobangView gobangView; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FrameLayout container findViewById(R.id.board_container); gobangView new GobangView(this); container.addView(gobangView); Button btnUndo findViewById(R.id.btn_undo); Button btnRestart findViewById(R.id.btn_restart); btnUndo.setOnClickListener(v - gobangView.undo()); btnRestart.setOnClickListener(v - gobangView.resetGame()); } }5. 常见问题与排查技巧实录在实际开发中你肯定会遇到各种各样的问题。下面是我在实现和教学过程中总结的一些典型坑点和解决方案。5.1 画面不刷新或刷新异常问题描述落子后棋盘没有出现新棋子或者棋子闪烁、残留。排查思路检查invalidate()确保在每次修改了board数组或游戏状态后都调用了invalidate()或postInvalidate()来请求重绘。invalidate()必须在UI线程调用。检查onDraw逻辑在onDraw中打印日志确认是否被调用以及board数组数据是否正确。确保绘制棋子的循环覆盖了整个数组0到14。检查坐标计算确认boardPadding、cellSize、boardSize在onSizeChanged中计算正确并且在onDraw和onTouchEvent中使用的是一致的值。有时在onDraw中重复计算这些值会导致偏差。解决方案在onDraw开始处添加Log.d(“Gobang”, “onDraw called”)在placePiece后添加Log.d(“Gobang”, “place at ” row “,” col)。通过日志流观察程序执行顺序和数据变化。5.2 触摸落子位置不准问题描述点击交叉点附近棋子却下在了别处。排查思路打印坐标在onTouchEvent中打印touchX,touchY,exactRow,exactCol,row,col看转换过程哪一步出了问题。检查吸附逻辑确认你的“吸附”算法Math.round是否符合预期。有时候因为boardPadding计算有误导致整个坐标系的偏移。检查棋盘绘制区域确认onDraw中绘制的棋盘网格其起始点(boardPadding, boardPadding)和总大小boardSize与onTouchEvent中判断触摸区域和计算索引时使用的值完全一致。建议将这些值作为成员变量只在onSizeChanged中计算一次。解决方案在屏幕上绘制出触摸点的位置和最终转换出的索引进行可视化调试。可以在onDraw的最后添加// 调试用绘制最后一次触摸的像素坐标和逻辑坐标 if (lastTouchX 0) { Paint debugPaint new Paint(); debugPaint.setColor(Color.BLUE); debugPaint.setTextSize(30); canvas.drawText(“Touch: (“ lastTouchX “,” lastTouchY “)”, 50, 50, debugPaint); canvas.drawText(“Index: (“ lastRow “,” lastCol “)”, 50, 90, debugPaint); }5.3 胜负判断逻辑错误问题描述五子连珠了没判赢或者没连成五子却判赢了。排查思路边界检查这是最常见的错误。在countDirection函数中一定要先检查数组索引是否越界再访问数组元素。while (r 0 r 15 c 0 c 15 board[r][c] color)这个顺序不能错。方向向量检查四个方向向量{0,1},{1,0},{1,1},{1,-1}是否正确它们必须覆盖水平、垂直和两条对角线。计数逻辑确认count的初始值是1代表中心子并且正反两个方向的计数相加。可以用一个简单的测试用例比如在棋盘中央手动设置一个五连珠然后单步调试checkWinner函数。游戏状态更新确保在判定获胜后正确更新了gameState并且阻止了后续的落子操作在placePiece开头检查gameState。解决方案编写单元测试。在Android项目中可以创建本地单元测试JUnit来单独测试checkWinner函数。准备几个预设的棋盘状态如刚形成五连珠、四连珠被堵住等断言函数的返回结果是否符合预期。这能极大提升逻辑的可靠性。5.4 性能与内存问题问题描述在低端设备上感觉卡顿或者长时间运行后内存增长。排查思路避免在onDraw中创建对象onDraw会被频繁调用任何在这里new Paint(),new Path()的操作都会产生大量临时对象触发GC导致卡顿。所有Paint对象都应在init()中初始化并复用。棋盘数据模型对于15x15的棋盘用int[][]完全没问题。但如果想支持超大棋盘比如30x30可以考虑使用SparseArray等稀疏结构来存储已落子的位置但会稍微增加判断某点是否有子的复杂度。视图层级我们的例子中只有一个自定义View性能很好。但如果界面复杂要避免过度绘制和复杂的视图层级。解决方案使用Android Profiler工具监控CPU和内存使用情况。确保onDraw方法执行时间短理想情况小于16ms以维持60fps。5.5 横竖屏切换导致状态丢失问题描述旋转屏幕后棋盘清空了游戏状态重置。原因默认情况下屏幕旋转会导致Activity销毁重建成员变量会丢失。解决方案需要保存和恢复状态。有两种主要方式使用onSaveInstanceState和onRestoreInstanceState在GobangView中重写这两个方法保存board数组、isBlackTurn、gameState和moveHistory。Override protected Parcelable onSaveInstanceState() { Parcelable superState super.onSaveInstanceState(); Bundle savedState new Bundle(); savedState.putParcelable(“superState”, superState); savedState.putSerializable(“board”, board); // 注意int[][]需要转换 savedState.putBoolean(“isBlackTurn”, isBlackTurn); savedState.putInt(“gameState”, gameState); // moveHistory 需要自己实现序列化 return savedState; } Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle savedState (Bundle) state; board (int[][]) savedState.getSerializable(“board”); isBlackTurn savedState.getBoolean(“isBlackTurn”); gameState savedState.getInt(“gameState”); state savedState.getParcelable(“superState”); } super.onRestoreInstanceState(state); invalidate(); }使用ViewModel推荐这是更现代的做法。将游戏状态棋盘数据、回合等放在一个继承自ViewModel的类中。这样即使Activity因配置更改重建ViewModel也会保留。这需要结合LiveData或StateFlow使用稍微复杂但架构更清晰。把这个五子棋项目从头到尾实现一遍你会对Android的自定义View、事件处理、游戏逻辑有一个非常扎实的掌握。它就像一块很好的敲门砖敲开了Android应用开发实战的大门。之后你可以在此基础上添加更多功能比如AI人机对战这又是一个有趣的算法挑战、网络联机、更炫的动画效果等等每一个扩展都能带你深入一个新的技术领域。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。