拼棋派对开发实战系列封面

同一屏幕上两个人拼图,每次点击应该只改变本人盘面,但整场比赛又需要共同开始和结束。《拼棋派对》把两份玩家状态与一份比赛状态分开保存,让图片块、选中位置、步数和进度各自独立。

这一篇复用前一篇的图片块模型,重点讲操作分发、计时条件与结算规则。本次在HarmonyOS7 API26手机模拟器上使用山湖4×4互换模式,观察玩家A的选块、交换和完成过程。

本文目录

两份玩家状态与一场比赛

PuzzleSideState 保存tiles、selectedIndex、steps、elapsed、completed和done。A、B分别持有一个对象,比赛共享battleStarted、battleFinished和battleWinner。

状态玩家独立还是比赛共享用途
tiles、selectedIndex每人一份本人的图片位置与待交换图片
steps、completed每人一份本人的操作次数与归位数
elapsed、done每人一份用时与是否完成
started、finished、winner共同一份描述整场比赛的阶段
图片主题、阶数、模式共同一份双方使用相同玩法配置

重开时,页面以当前时间生成seed,A用seed+11,B用seed+23分别创建盘面。这保证数组独立,但不保证两人的开局难度相同。相同主题、相同阶数与相同排列是不同概念,当前实现属于前两项相同。

若要改为严格的同题竞速,可以对双方使用同一seed,分别调用工厂产生两个数组;不能把同一个可变数组同时赋给A、B。后续交换其中一份时,也需要继续保持复制后更新的习惯。

从玩家身份分发点击

页面事件携带player与index,handleBattleTileTap 只更新对应玩家。A的处理分支如下,省略提示更新与B分支:

const previous = this.battleA;
this.battleA = this.applyBattleTap(this.battleA, index, player);
this.updateBattleTapNotice(player, previous, this.battleA);
if (this.battleA.completed === this.puzzleTotal()) {
  this.finishBattle('玩家A');
}

applyBattleTap 返回新的玩家对象。它处理本人的选块和交换,页面再决定是否结束整场比赛。规则函数不直接给A或B赋值,可以复用到不同朝向与布局中。

手机面对面布局让B位于上方、A位于下方。朝向变化属于显示层,不改变tiles内部的逻辑顺序。若通过屏幕像素推断玩家身份,折叠与旋转后容易把操作分给另一方;事件显式传入身份更可靠。

交换两块图片怎样计步

互换模式有三个分支:没有选块时记录index;点击同一块取消;点击第二块时交换并增加一步。第一次选择不能算作已完成交换。

const tiles = this.swapTiles(side.tiles, side.selectedIndex, index);
const completed = countCompleted(tiles);
return {
  tiles,
  selectedIndex: -1,
  steps: side.steps + 1,
  elapsed: side.elapsed,
  completed,
  done: completed === this.puzzleTotal()
};

选中第一块的分支保留tiles、steps、elapsed与completed,只修改selectedIndex。点击同一块取消也保留步数。这样进度和成绩反映实际交换结果,而不是点击次数。

滑块模式则进入 applyBattleSlidingTap。不相邻的点击不会加步,会提示应移动的相邻图片;有效移动交换图片与空格。两种模式共享同一份玩家状态结构,但计步触发点不同。

计时条件与暂停语义

页面每秒的计时回调只在对局页、当前玩法为双人拼图、已开始且未结束时更新双方用时。tickPuzzleSide 对尚未完成的一方增加一秒:

private tickPuzzleSide(side: PuzzleSideState): PuzzleSideState {
  return {
    tiles: side.tiles,
    selectedIndex: side.selectedIndex,
    steps: side.steps,
    elapsed: side.done ? side.elapsed : side.elapsed + 1,
    completed: side.completed,
    done: side.done
  };
}

当前显示的是前台玩法计时,不是用开始时间与系统时间差计算的绝对比赛时长。离开对局页后不继续累计,不能据此宣称后台计时已经完成。

“开始”会设置battleStarted,“暂停”会清掉这个标志。不过点击拼块时,处理函数发现尚未开始会自动调用startBattle。因此当前暂停允许玩家通过再次操作恢复比赛,并不是冻结盘面、必须先点继续的强暂停。若产品希望暂停期间完全禁止操作,应在点击入口增加独立的暂停状态判断。

本次实操保留了交换后的计时与暂停画面;后台、切页和长时间暂停仍需要另行验证。

完成获胜与手动结算

当一方的completed等于总块数,页面立即结束整场比赛。finishBattle 把started设为false、finished设为true,保存winner,停止音乐并写入战绩。这里是“先完成者胜”的竞速路径。

“结算”按钮还有一条提前结束路径。当前 finishBattleByScore 先比较归位数,再比较步数:归位多者胜;归位相同,步数较少者胜。

if (this.battleA.completed > this.battleB.completed) {
  this.finishBattle('玩家A');
  return;
}
if (this.battleB.completed > this.battleA.completed) {
  this.finishBattle('玩家B');
  return;
}
if (this.battleA.steps <= this.battleB.steps) {
  this.finishBattle('玩家A');
} else {
  this.finishBattle('玩家B');
}

注意最后的 <=:归位数与步数都相同时,当前会判A胜,并没有平局分支,也没有进一步比较用时。本次文章如实说明该实现;如果希望双人竞争更公平,建议明确完全相同成绩时平局,或在产品规则中约定用时作为第三条件。

未开始且双方均无操作、无用时的时候,按钮只提示先开局。提前结算也不等于图片已拼完,战绩会保存双方当前归位数,介绍结果时应区分这两条路径。

实际操作与后续改进

从双人拼图打开“选图”,选择山湖4×4互换并返回对局。点击A的第一块时出现选中边框,步数仍是0;再点击A的第二块,两块交换,A变为1步,B仍为0步。动图展示从选块、交换到全部归位的过程。

玩家A选择、交换、暂停后继续拼图,15步完成16块并获胜

接着点击暂停,保留当前双方盘面;继续交换后,A在第15步完成16块归位,页面显示玩家A获胜,B仍为0步。

这次验证覆盖A的选块、交换、独立步数与完成获胜。B保持原始盘面;双人滑块模式、切页计时和同成绩手动结算没有在本次完整实测,相关说明来自源码核对。

接下来最值得优化的是竞赛公平性:同题开局可以减少随机难度差,明确平局能避免完全相同成绩偏向A,独立暂停状态可以让暂停行为更清楚。这些改进与页面有多大无关,却直接影响两个人对结果是否认可。

源码入口:GameModels.ets 的 PuzzleSideState;GameLogic.ets 的 createPuzzleSide;Index.ets 的 handleBattleTileTap、applyBattleTap、tickPuzzleSide 与 finishBattleByScore。相关开发资料见HarmonyOS官方文档。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐