如同养育一个婴儿,父母总会一步步引领孩子成长,从蹒跚学步到来去如风;我们对游戏功能的开发,也无疑应当从走出第一步棋开始。现在,我们已经构建出了棋盘、棋子等基本的游戏逻辑对象;那么是时候编写功能,让棋子在棋盘上移动了。
4.1 选中棋子准备开始下棋!首先,第一个问题出现:棋怎么下?
如果你在QQ游戏、联众等在线棋牌平台玩过象棋、军棋等棋类,或者玩过《文明6》等回合制战棋游戏,那么对于棋类游戏的基本操作方式一定不会陌生。
假设轮到玩家A走棋——
A点击一个棋子,选中这个棋子;
A点击棋盘上的一个格子,将自己的棋子移动到棋盘上的另一位置。这个过程会吃掉目标位置上的敌方棋子(如果有的话)。
要想实现上述玩法,我们需要实现一个重要的功能:选中。
创建脚本SelectCore.cs,来实现选中棋子的功能。
using System.Collections;using System.Collections.Generic;using UnityEngine;public class SelectCore : MonoBehaviour{public static SelectCore Get = null;[SerializeField]private Chessman selection;public static Chessman Selection => Get.selection;private void Awake(){Get = this;}public static void TrySelect(Chessman chessman){Get.selection = chessman;}public static void DropSelect(){Get.selection = null;}}SelectCore是一个全局唯一组件,它能够记录一个被选中的棋子;使用TrySelect方法会试图选中一个棋子,而DropSelect方法则会取消选中。
在物体GameCtrl上创建一个新物体SelectCore,然后挂载此组件。
然后,我们在棋子(Chessman.cs)的被点击事件中使用TrySelect方法,使得每个棋子在被点击时,会被SelectCore所选定。
修改Chessman.cs中的OnChessmanClicked方法,内容如下所示:
private void OnChessmanClicked(){SelectCore.TrySelect(this);}运行游戏,用鼠标左键单击任意一个棋子;此时你将在SelectCore组件的Inspector视图中,发现变量Selection已经变成了刚刚被点击的棋子。这说明该棋子已被选中,如图。
(再单击别的棋子,则选中的对象也会随之切换。)
不过,目前在游戏视图中,我们无法看出哪一个棋子被选中了,这显然不是合格的游戏体验。为了解决这个问题,还需要实现一个简单的边框效果,以此标出被选中的棋子。
创建脚本SelectEffect.cs。
using UnityEngine;public class SelectEffect : MonoBehaviour{public Material GLRectMat;public Color GLRectColor(Camp camp){if (camp == Camp.Blue){return Color.green;}else if (camp == Camp.Red){return new Color(1f, 0.5f, 0.75f);//Color.red太丑了,需要自己换个颜色}else{return Color.white;}}void OnPostRender(){var selection = SelectCore.Selection;if (!selection){return;}selection.TryGetComponent(out RectTransform rectTransform);var center = Camera.main.WorldToScreenPoint(rectTransform.position);GL.PushMatrix();//GL入栈GLRectMat.SetPass(0);//启用线框材质rectMatGL.LoadPixelMatrix();//设置用屏幕坐标绘图for (int radius = 46; radius = 0; i--){all[i].ExitFromBoard();}}/// /// 依照坐标查询,找到位于相应坐标上的棋子。/// /// /// public static Chessman GetChessman(Location location){foreach (var chessman in All()){if (chessman.location.Equals(location)){return chessman;}}return null;}/// /// 棋子所在的方格。/// public Square Square => ChessBoard.Get[location];/// /// 初始化棋子/// public void Start(){if (camp == Camp.Neutral){Debug.LogError("棋子阵营不能为中立。");return;}MoveTo(location, false);GetComponent().onClick.AddListener(OnChessmanClicked);}public bool IsRat => animal == Animal.Rat;//棋子是否为鼠public bool IsElephant => animal == Animal.Elephant;//棋子是否为象public bool CanJump => animal == Animal.Tiger || animal == Animal.Lion;//棋子是否具有跳河能力(狮或虎)public int Attack => (int)animal;//棋子的强度(己方走棋时的攻击力)public int Defence//棋子的强度(对方走棋时的防御力,会受到陷阱的虚弱效果影响){get{if (IsTrapped){return 0;}return Attack;}}public bool IsTrapped => Square.type == SquareType.Trap && Square.camp != camp;//棋子是否处于对方陷阱中/// /// 使棋子移动到指定坐标。这会删除目标位置上的另一个棋子。/// public void MoveTo(Location target, bool swapPlayers = true){try{Square square = ChessBoard.Get[target.x, target.y];//定位目标棋盘格if (square.Chessman != this){square.RemoveChessman();//删除目标位置上已有的棋子}location = target;//修改自身坐标为新的坐标transform.DOMove(square.transform.position, 0.35f);//执行移动//transform.position = square.transform.position;//无DOTween时以此替代上一行if (swapPlayers){PlayerManager.Tik();}}catch (Exception ex){Debug.LogError($"移动棋子失败.{ex.Message}");}}private void OnChessmanClicked(){SelectCore.TrySelect(this);}/// /// 使这个棋子退场。/// public void ExitFromBoard(){Destroy(gameObject);}}运行游戏来测试。点击棋子蓝鼠,将它移动到另一方格,此时会发现对蓝鼠的选中解除了,且PlayerManager组件上的currentPlayer字段由Blue变成了Red,说明行棋权已经正确切换。再点击棋子红狮,将它移动到另一方格。这一过程可以重复循环下去。
唔~这样下棋的手感很不错!
4.4 流畅行棋功能到这里是否完成了呢?
重新开始游戏,我们使用一些非法操作,对刚刚完成的玩法进行压力测试。
测试1:点击棋子蓝鼠,将它移动到另一方格,然后再点击蓝鼠。
这样做的结果是,你又一次成功选中了蓝鼠——甚至可以在行棋玩家被标注为Red时移动它。
测试2:先用蓝方随便走一步棋,然后点击棋子红狮,再点击蓝鼠来试图将其吃掉。
这样做的结果是,如果你点击的是蓝鼠的棋子本体,则红狮毫无反应,而蓝鼠则在棋盘上被选中了;
如果你点击的是蓝鼠所在方格的外缘区域(即点击方格),则红狮会吃掉蓝鼠。
乱套了,全都乱套了!
为什么会出现这样的问题呢?原因很简单,现有的代码并未对任何异常情况作出处理,例如玩家重复走棋、玩家试图选中对方的棋子,等等。玩家在棋盘上可以执行许多种行为,这些行为未必都是合乎规则的;而我们的棋局必须能够正确响应玩家的各种不合规操作。这意味着我们需要考虑这样一个问题。
玩家在棋盘上有可能作出哪些行为?这些行为出现后,游戏分别应当如何响应?
在开始研究这个问题之前,受到先前测试2的启发,我们不妨先对玩家的点击行为进行一次简化,以防止玩家在点击【棋子】和【棋子下方的方格区域】时出现行为歧义。
玩家在【点击棋子】和【点击棋子下方的棋盘格子区域】时,所表达的意愿是完全一致的。如果你有在网络上下棋的经验,那么一定会了解到这一点。
为了体现上述效果,我们对Chessman.cs中的棋子被点击事件进行修改。修改后,点击一个棋子将不再被视为一种独特的事件进行处理,而是等效于该棋子所在的方格受到点击。
修改Chessman.cs中的OnChessmanClicked方法。
private void OnChessmanClicked(){Square.OnSquareClicked();}接下来,我们便可以暂时抛开代码,用自然语言来描述——
当玩家在不同状态下点击棋盘上的不同物件时,其真实意愿分别是什么?
总共有6种不同的情况,表达的不同意愿有3种,如下所示。
1.当玩家手里有子时
(1)玩家点击空方格,表明玩家试图将选中的棋子向点击的位置移动;(意愿:移动)
(2)玩家点击有己方棋子的方格,说明玩家试图取消对当前棋子的选中,并选中新点击的棋子;(意愿:选中)
(3)玩家点击有对方棋子的方格,说明玩家试图将选中的棋子向点击的位置移动,并吃掉对方的棋子。(意愿:移动)
2.当玩家手里无子时
(4)玩家点击空方格,这不表示任何含义,棋局应无事发生;(意愿:无)
(5)玩家点击有己方棋子的方格,说明玩家试图选中该棋子;(意愿:选中)
(6)玩家点击有对方棋子的方格,这不表示任何含义,棋局应无事发生。(意愿:无)
这里要注意的是,上述的移动行为,本质上都是试图移动。根据斗兽棋具体游戏规则的约束,玩家试图移动棋子的意愿可能有效,也可能无效。例如,试图用猫吃掉对方不在陷阱内的狮子,或者试图让鼠以外的兽潜入水中,都是无效的行棋请求,将会被拒绝执行。详细的规则模块将在第5章中编写,在那之前,我们暂且认为所有的行棋请求都是合法的。
新建脚本CommandCenter.cs,用来集中接收和处理玩家的行棋指令。
执行GameOrder方法,表示执行一条行棋指令;该指令会试图将一个棋子移动到目标位置。
*进阶提示
这里加入了两个特殊参数ignorePlayerColor和swapPlayersAfterMovement,用于决定一条行棋指令是否可以无视当前行棋玩家的限制,以及是否在行棋后交换行棋权。在常规情况下,上述答案显而易见,这两个参数并无必要;但加入这两个参数能够在日后带来功能上的可扩展性,例如让子棋、让步棋和自由摆棋的实现。
using System.Collections;using System.Collections.Generic;using UnityEngine;public class CommandCenter : MonoBehaviour{public static void GameOrder(Chessman chessman, Location target, bool ignorePlayerColor = false, bool swapPlayersAfterMovement = true){if (!ignorePlayerColor){if (chessman.camp != PlayerManager.Get.currentPlayer){return;}}chessman.MoveTo(target, swapPlayersAfterMovement);}}修改Square.cs中的OnSquareClicked方法,依照前面归纳的结论,在玩家点击方格之后正确判定玩家的意愿,并根据玩家意愿,发出【选中】或【行棋】的指令。
public void OnSquareClicked(){var selection = SelectCore.Selection;//玩家手里有子if (selection){//点击空方格if (!Chessman){CommandCenter.GameOrder(selection, location);}//点击有己方棋子的方格else if (Chessman.camp == PlayerManager.Get.currentPlayer){SelectCore.TrySelect(Chessman);}//点击有对方棋子的方格else{CommandCenter.GameOrder(selection, location);}}//玩家手里无子else{//点击空方格if (!Chessman){return;}//点击有己方棋子的方格else if (Chessman.camp == PlayerManager.Get.currentPlayer){SelectCore.TrySelect(Chessman);}//点击有对方棋子的方格else{return;}}}完成以下修改后执行游戏,并交替使用蓝方和红方来走棋。
与上一次测试相比,你会发现这一次的棋局表现有了极大的改进;它能够有效地应对各种奇怪、非法的操作,从而防止棋局的进程出现异常。诸如:
·无法再选中非行棋玩家的棋子;
·点击棋子下方的格子区域和点击棋子的本体的效果总是相同的,不会再产生不同的响应。
此外,在手中有子时点击对方棋子,已经能够被正确判定为是试图吃掉对方棋子。因为现在没有规则限制,所以你可以随心所欲地吃掉对方的棋子,例如让蓝鼠在开局时千里奔袭,吃掉红方的猫:
1.选中蓝鼠
2.点击远处的红猫以将其吃掉
(壮起鼠胆,把猫打翻!)
开发进行到这里,我们在没有写入具体规则的情况下,完整实现了双方玩家轮流行棋的功能;同时,我们有效地建立了游戏操作的容灾能力,实现了流畅而完整的行棋手感。
现在不妨多测试几次,体验一下这盘随心所欲,毫无约束的“耍赖版斗兽棋”。是不是有着别样的乐趣?
接下来,我们就可以编写规则模块,让这盘棋的游戏体验变得“认真”起来啦!