Unity复刻骑砍中的帝国象棋(一)

起因和简介

这两天从一款游戏中发现了这么个棋类小游戏,觉得挺有意思,没错,就是下面这个:

作为程序员的我,一下就想到复刻它一下。这个棋类小游戏,我并不知道它确切的名字,好像叫帝国象棋??好吧,这个不重要,重要的是我要实现它,顺便说一下,这个应该不会侵权吧,我可是真的只是为了学习,毕竟就算100%实现了,也不是以赚钱为目的的。先来说说它的规则吧,其实这个游戏我并没有玩,规则是我从其他人的攻略文章里搜来的:

  1. 双方分为攻击方和防守方,中间摆成十字的棋子,是防守方,四周的4组品字形棋子为攻击方。防守方中有一个棋子是王,就是正中间那个头上有皇冠图标的那个棋子。
  2. 所有的棋子,全部走直线,类似中国象棋里的“车”的走法。但是棋盘正中间的那个位置,任何棋子都不可以进入,也不可以越过,即便是“王”,离开之后就不能再回到中间去。
  3. 两个己方棋子,如果能“夹住”一个敌方棋子,那么就可以把它吃掉。水平或者垂直方向均可。棋盘中间的位置可以视作任何一方的己方棋子。一方棋子如果主动走到两边具有敌对棋子的位置,并不会“自杀”,对方如果想要杀死该棋子,只能从另一个方向夹击,或原夹击形式的棋子离开后重新回到原位置,重新形成夹击才能杀死该棋子。
  4. 防守方的“王”,如果能行走到任意方向上的棋盘的边缘,则防守方胜利。攻击方如果能杀死“王”,则攻击方胜利。

规则描述虽然文字挺多,但是实际上很简单。

开发计划和实现

目前写这篇文章之时,已经实现了走棋规则的判断、胜负的判断,就是说可以实现人和人的对战了。下一步,如果有时间的话,将会再完善一下这个小工程,主要是:①完善UI,实现开始、退出的对话框,以及胜利、失败的提示等。②如果还有时间,打算再实现一个AI功能,思路跟我之前写过的一片棋类游戏AI相差不多。有兴趣的可以一观:四字连珠。
目前的演示视频如下:

Unity复刻骑砍游戏中的帝国象棋(一)

具体实现

数据描述

首先是棋盘的数据描述,这是整个游戏最重要的数据结构,这个游戏的棋盘是个9*9的方格,很显然,最适合的就是用数组描述,比较直观的就是2维数组,但是二维数组在访问频率较高的场合,会稍稍影响性能,如果你用的Rider,它会给你提示:

上图的意思是,使用多维数组效率很低,建议使用一维数组代替。

基于Rider给予的忠告,这里我使用的是一维数组。但是为了方便,还是以2维的形式去访问,然后写一个2维转1维的索引转换函数:

private static int CoordToIndex(int x, int y)
{return Mathf.Clamp(x * 9 + y, 0, 80);
}

也许这样的转换在实质上跟使用2维数组是性能等价的,但Rider不提示性能问题了,作为有点强迫症的可以心安理得,当然,如果你用的是VS,那就无所谓了,就算2维数组性能不如一维,这种性能的差异也可以忽略不计。

那么问题来了,数组里面放什么呢?如果想搞的稍微复杂一些,可以专门写一个棋子类,然后用数组作为容器,去盛放这些棋子。但是我并没有这么做,主要是感觉没大有必要,于是我用了2个数组,来描述这个棋盘。

private enum ChessType
{None,        // 表示该位置没有棋子Offensive,    // 表示该位置是攻击方的棋子Defense,   // 表示该位置是防守方的棋子King      // 表示该位置是防守方的王
}private readonly ChessType[] boardMap = new ChessType [81];
private readonly GameObject[] chesses = new GameObject[81];

上面代码,定义了一个ChessType,其实就是这个棋盘上每个方格的状态。然后用一个9*9=81长度的数组来表示整个棋盘,还有一个一样的数组,用来盛放棋子的GameObject,方便进行位置调整和被杀后的剔除。

然后就是棋盘的初始化:
private void PutChess(int x, int y, ChessType ct)
{GameObject obj;switch (ct){case ChessType.Defense:obj = Instantiate(defenseChessPrefab, transform);break;case ChessType.King:obj = Instantiate(kingChessPrefab, transform);break;case ChessType.Offensive:obj = Instantiate(offensiveChessPrefab, transform);break;default:return;}obj.transform.localPosition = CoordToLocal(x, y);int index = CoordToIndex(x, y);boardMap[index] = ct;chesses[index] = obj;
}
private void ResetChessBoard()
{for (int i = 0; i < 81; ++i){boardMap[i] = ChessType.None;if (chesses[i] != null){Destroy(chesses[i]);chesses[i] = null;}}PutChess(3, 0, ChessType.Offensive);PutChess(4, 0, ChessType.Offensive);PutChess(5, 0, ChessType.Offensive);PutChess(4, 1, ChessType.Offensive);PutChess(0, 3, ChessType.Offensive);PutChess(0, 4, ChessType.Offensive);PutChess(0, 5, ChessType.Offensive);PutChess(1, 4, ChessType.Offensive);PutChess(8, 3, ChessType.Offensive);PutChess(8, 4, ChessType.Offensive);PutChess(8, 5, ChessType.Offensive);PutChess(7, 4, ChessType.Offensive);PutChess(3, 8, ChessType.Offensive);PutChess(4, 8, ChessType.Offensive);PutChess(5, 8, ChessType.Offensive);PutChess(4, 7, ChessType.Offensive);PutChess(2, 4, ChessType.Defense);PutChess(3, 4, ChessType.Defense);PutChess(4, 2, ChessType.Defense);PutChess(4, 3, ChessType.Defense);PutChess(4, 5, ChessType.Defense);PutChess(4, 6, ChessType.Defense);PutChess(5, 4, ChessType.Defense);PutChess(6, 4, ChessType.Defense);PutChess(4, 4, ChessType.King);
}
棋盘坐标转换

只要在游戏开始的时候,调用ResetChessBoard方法,就可以重新摆一盘棋了。这里面有个函数CoordToLocal,用于根据棋盘坐标来计算棋子的局部坐标:

private static Vector3 CoordToLocal(int x, int y)
{return new Vector3(x * deltaCellSize - 0.47f + 0.0522222f, (8 - y) * deltaCellSize - 0.47f + 0.0522222f, 0.05f);
}

实际上,这个函数写的并不太优雅,原因是它跟模型的尺寸和棋盘的纹理位置是相关的,这就意味着如果你更换一个棋盘模型,这个函数就得重写。当然,能够通用的方法也有很多,为了简单,同时也为了性能,这里就这么写死了。

同时,对应的,也要有鼠标棋盘后,把点击位置转换为棋盘坐标的方法:

if (Physics.Raycast(mainCamera.ScreenPointToRay(Input.mousePosition), out RaycastHit hit, 500f))
{int x = Mathf.FloorToInt((hit.point.x + 0.47f) / deltaCellSize);int y = Mathf.FloorToInt((hit.point.z + 0.47f) / deltaCellSize);// x, y就是棋盘坐标
}
游戏逻辑

整个游戏过程,可以用状态机来描述不同的状态,代码和意义如下:

private enum PlayState
{GameOver,                // 游戏结束状态,或者游戏尚未开始状态ComputerThink,         // 计算机正在思考PlayerThink,            // 玩家正在思考PlayerTakeChess,     // 玩家举起了一枚棋子(选定某棋子,但尚未决定如何行走)PlayerPutChess         // 玩家将举起的棋子放到了指定位置(决定了如何行走)
}

针对不同的状态,就可以写不同的逻辑,然后在各个状态中进行切换:

思考状态 PlayerThink
case PlayState.PlayerThink:if (Input.GetMouseButtonDown(0)){if (Physics.Raycast(mainCamera.ScreenPointToRay(Input.mousePosition), out RaycastHit hit, 500f)){int x = Mathf.FloorToInt((hit.point.x + 0.47f) / deltaCellSize);int y = Mathf.FloorToInt((hit.point.z + 0.47f) / deltaCellSize);if (TryGetChessByCoord(x, y, out GameObject chess) && TakeupChess(x, y)){chess.transform.localPosition += takeChessOffset;currentTakedChess = chess;takedChessCoord.x = x;takedChessCoord.y = y;state = PlayState.PlayerTakeChess;}}}break;

如上,在玩家思索状态下,如果检测到了鼠标左键被按下,那么就发射一条射线,判断是否点击了棋盘上的己方棋子,如果是己方棋子,则继续判断这个棋子是不是可以被拿起:如果棋子四周有其他棋子,也就是说这枚棋子实际上不能进行移动,那么这枚棋子就不能被拿起,比如说这一枚:

只有点击的是能够被拿起的棋子,那么就拿起这枚棋子,并且将状态切换到PlayState.PlayerTakeChess

另外,举起棋子要做的事情是:枚举这枚棋子所有可能的走位,然后在所有的走位上打上标记,就是生成一个小圆点,表示可以走到的位置。这里用到了一个简单的对象池,而且只要想一下就不难发现,因为棋盘大小是9*9,所以,所有可能的走位不可能超过9+9-1=17个,因此这个对象池可以实现实例化17个这样的标记预制体,然后根据实际需要来摆放。

举起棋子的状态 PlayerTakeChess
  • ① 如果玩家点了鼠标右键,那么表示玩家改变了主意,表示要放弃刚刚选择的棋子,要重新选择棋子,这时候要把已经举起的棋子放下,状态重新切换为思考状态。
  • ② 如果玩家点了鼠标左键,首先判断所点的位置是不是原棋子的位置,如果是,那就同①,表示要放弃该棋子;如果所点的是另一个棋子,并且这枚棋子可以被举起,那么同样表示玩家改变了主意,此时需要放下原来举起的棋子,然后举起新选择的棋子,状态不改变;如果玩家点击的是所举起的棋子可以到达的空位,那么就标记该位置为目标位置,并将状态切换为“放置棋子”。
case PlayState.PlayerTakeChess:if (Input.GetMouseButtonDown(1)){ClearChessPoints();state = PlayState.PlayerThink;currentTakedChess.transform.localPosition -= takeChessOffset;currentTakedChess = null;break;}if (Input.GetMouseButtonDown(0)){if (Physics.Raycast(mainCamera.ScreenPointToRay(Input.mousePosition), out RaycastHit hit, 500f)){int x = Mathf.FloorToInt((hit.point.x + 0.47f) / deltaCellSize);int y = Mathf.FloorToInt((hit.point.z + 0.47f) / deltaCellSize);if (takedChessCoord.x == x && takedChessCoord.y == y){ClearChessPoints();currentTakedChess.transform.localPosition -= takeChessOffset;currentTakedChess = null;state = PlayState.PlayerThink;break;}if (IsPutable(x, y)){ClearChessPoints();tickLerp = 0;originChessPosition = currentTakedChess.transform.localPosition;targetChessPosition = CoordToLocal(x, y);int index = CoordToIndex(takedChessCoord.x, takedChessCoord.y);takedChessType = boardMap[index];boardMap[index] = ChessType.None;chesses[index] = null;takedChessCoord.x = x;takedChessCoord.y = y;state = PlayState.PlayerPutChess;break;}if (TryGetChessByCoord(x, y, out GameObject chess)){int curindex = CoordToIndex(x, y);if (IsFriend(curindex, boardMap[CoordToIndex(takedChessCoord.x, takedChessCoord.y)])){ClearChessPoints();currentTakedChess.transform.localPosition -= takeChessOffset;}if (TakeupChess(x, y)){chess.transform.localPosition += takeChessOffset;currentTakedChess = chess;takedChessCoord.x = x;takedChessCoord.y = y;state = PlayState.PlayerTakeChess;}elsestate = PlayState.PlayerThink;}}}break;
放置棋子状态 PlayerPutChess
case PlayState.PlayerPutChess:if (tickLerp < 0.99f){tickLerp += 2.5f * Time.deltaTime;currentTakedChess.transform.localPosition =Vector3.Lerp(originChessPosition, targetChessPosition, tickLerp);}else{currentTakedChess.transform.localPosition = targetChessPosition;int index = CoordToIndex(takedChessCoord.x, takedChessCoord.y);boardMap[index] = takedChessType;chesses[index] = currentTakedChess;currentTakedChess = null;if (IsWin() || KillChess() || NoEnemyChessCanMove()){GameOver();break;}isPlayerOffensive = !isPlayerOffensive;if (isPlayWithPlayer){state = PlayState.PlayerThink;}else{state = PlayState.ComputerThink;}}break;

这个状态的逻辑就比较简单些,主要是完成棋子移动的动画,并且,移动完成后,进行是否赢了的判断、是否杀死了对方的棋子,如果是杀死的是防御方的王,也要判定赢了;还有,如果敌人虽然仍有棋子,但全部无法移动,比如被堵到角落,也需要判定胜利。如果判断一方胜利,那么就结束游戏。然后将状态切换到GameOver,否则的话,就视情况将状态切换到玩家思考或者电脑思考状态,取决于游戏模式是人机对战还是跟人人对战。
这里重要的方法有两个,判断输赢和判断吃子的方法,请参考代码注释:

// 如果王已经逃走,判定防御方胜利
private bool IsWin()
{// 如果操作的棋子是王,那么判断是否移动到了棋盘的任意方向的边界if (takedChessType == ChessType.King){if (takedChessCoord.x is 0 or 8 || takedChessCoord.y is 0 or 8)return true;}return false;
}
// 判断杀死棋子,如果杀死的是王,那么返回true,表示攻击方胜利
private bool KillChess()
{bool result = false;if (takedChessCoord.x >= 2){if (IsFriend(CoordToIndex(takedChessCoord.x - 2, takedChessCoord.y))){int index = CoordToIndex(takedChessCoord.x - 1, takedChessCoord.y);if (IsEnemy(index)){result |= RemoveChess(index);}}}if (takedChessCoord.y >= 2){if (IsFriend(CoordToIndex(takedChessCoord.x, takedChessCoord.y - 2))){int index = CoordToIndex(takedChessCoord.x, takedChessCoord.y - 1);if (IsEnemy(index)){result |= RemoveChess(index);}}}if (takedChessCoord.x <= 6){if (IsFriend(CoordToIndex(takedChessCoord.x + 2, takedChessCoord.y))){int index = CoordToIndex(takedChessCoord.x + 1, takedChessCoord.y);if (IsEnemy(index)){result |= RemoveChess(index);}}}if (takedChessCoord.y <= 6){if (IsFriend(CoordToIndex(takedChessCoord.x, takedChessCoord.y + 2))){int index = CoordToIndex(takedChessCoord.x, takedChessCoord.y + 1);if (IsEnemy(index)){result |= RemoveChess(index);}}}return result;
}// 移除被吃掉的棋子,如果是王,返回true
private bool RemoveChess(int index)
{Destroy(chesses[index]);chesses[index] = null;bool result = boardMap[index] == ChessType.King;boardMap[index] = ChessType.None;return result;
}// 判断指定位置的棋子是否为tp类型棋子的友方,如果tp未指定,判断依据为当前所举棋子
private bool IsFriend(int index, ChessType tp = ChessType.None)
{// 棋盘正中的位置,可以视作任何方的乙方单位if (index == 4 * 9 + 4)return true;if (tp == ChessType.None)tp = takedChessType;return tp switch{ChessType.Offensive => boardMap[index] == ChessType.Offensive,ChessType.King or ChessType.Defense => boardMap[index] == ChessType.King ||boardMap[index] == ChessType.Defense,_ => false};
}// 判断指定位置是否为当前方的敌对棋子
private bool IsEnemy(int index)
{return takedChessType switch{ChessType.Offensive => boardMap[index] == ChessType.King || boardMap[index] == ChessType.Defense,ChessType.King or ChessType.Defense => boardMap[index] == ChessType.Offensive,_ => false};
}// 敌人是否没有任何棋子可以移动
private bool NoEnemyChessCanMove()
{if (isPlayerOffensive)return !boardMap.Where((t, i) => t is ChessType.King or ChessType.Defense && IsChessMoveable(i)).Any();return !boardMap.Where((t, i) => t == ChessType.Offensive && IsChessMoveable(i)).Any();
}// 判定一个棋子是否可以移动
private bool IsChessMoveable(int index)
{int x = index / 9;int y = index % 9;int t = x - 1;if (t >= 0 && (y != 4 || t != 4) && boardMap[CoordToIndex(t, y)] == ChessType.None)return true;t = x + 1;if (t <= 8 && (y != 4 || t != 4) && boardMap[CoordToIndex(t, y)] == ChessType.None)return true;t = y - 1;if (t >= 0 && (x != 4 || t != 4) && boardMap[CoordToIndex(x, t)] == ChessType.None)return true;t = y + 1;return t <= 8 && (x != 4 || t != 4) && boardMap[CoordToIndex(x, t)] == ChessType.None;
}

尚未开发的功能

  • UI
  • AI

源码下载

https://download.csdn.net/download/sdhexu/87222722

Unity复刻骑砍中的帝国象棋(一)相关推荐

  1. [技美CG]Unity3D复刻UnityShader 之 ShaderToy - Bubbles

    Unity3D复刻UnityShader 之 ShaderToy - Bubbles 背景: 官方地址/参考资料: ShaderToy-Bubbles原始代码: Unity复刻开始 核心显示类: 核心 ...

  2. 波士顿动力致敬经典!灵魂复刻40年前「滚石」热舞,动作不差分毫

    点击 机器学习算法与Python学习 ,选择加星标 精彩内容不迷路   新智元报道   来源:Youtube 波士顿动力又放大招!昨日,官方账号发布了对滚石乐队1981年热门歌曲「Start Me U ...

  3. unity简单复刻无敌破坏王

    使用unity简单复刻无敌破坏王总结 国庆突发奇想想简单复刻一下无敌破坏王,这里写一些总结,希望也能对大家有帮助 游戏截图: 基本思路: 时间有限,也只是简单复刻,所以只实现破坏方块的功能,画面算是广 ...

  4. Unity黑魂复刻经典教程心得(三)-CameraController

    CameraController 1.根据角色的位置来计算camera的位置 targetLookAt = new GameObject("targetLookAt").trans ...

  5. 【Mib自看】黑魂复刻Unity脚本

    [课程地址]B站傅老师Unity课程学习记录,仅代表个人理解. [自看]黑魂复刻Unity脚本 1.移动脚本 设计思路 2.动画 动画脚本:ActorController + 输入脚本:PlayerI ...

  6. zk4元年拆解_科比zk4复刻前掌没有zoom zk4选秀日复刻中底拆解测评

    科比zk4的复刻回归本来是一件很兴奋的事情,但是网上就有很多小伙伴一直在讨论zk4复刻是否和元年的配置一样,有的小伙伴希望是元年的配置,有的希望是全掌zoom,然而最近拆解图出来了,说实话小编看了还是 ...

  7. 复刻 Unity编辑器 移动的方式

    复刻 Unity编辑器 移动的方式 第一人称移动 自定义键值补充 代码搭载 老规矩,直接上代码: 第一人称移动 using System.Collections; using System.Colle ...

  8. 复刻一篇论文中蛋白质结构预测过程1

    根据"黑曲霉 α-L-鼠李糖苷酶 Rha1 的二级结构预测及三级结构模拟"一文,复刻由氨基酸序列预测蛋白质结构的过程 1. 获取氨基酸序列数据 https://www.ncbi.n ...

  9. Unity黑魂复刻经典教程心得(一)

    b站上傅老师的黑魂复刻教程,是比较好的,傅老师风情幽默,值得种草 https://www.bilibili.com/video/BV1gW411T7yb?p=55

最新文章

  1. maven的配置-2019-4-13
  2. 你还在从零搭建项目 ?
  3. 数据仓库—数据仓库—Sybase IQ 介绍
  4. 如何创建一个FeatureClass,IFeatureWorkspace 接口漫谈
  5. Mendix发布全球低代码报告,中国软件与低代码发展远超全球
  6. flutter能开发游戏吗_不用 H5,闲鱼 Flutter 如何玩转小游戏?-阿里云开发者社区...
  7. 吃冻梨对人会有什么好处?
  8. 2017CCPC哈尔滨 H:A Simple Stone Game
  9. CryptoTab 服务器_宁畅AI服务器X640 首登MLPerf 斩获30项世界第一
  10. Adobe Photoshop CC2014 安装过程
  11. 如何导出专业的工程图纸(附工图模板)
  12. 两台计算机之间的远程连接
  13. python简单图片识别_用Python进行简单的图片识别(1)
  14. Apple Watch使用指南:所有Apple Watch图标和符号含义
  15. flash助手推荐怎么删除
  16. lv6 网络编程(9)-网络编程扩展下
  17. 指挥计算机工作的程序集,主互操作程序集(PIA)是否要求在计算机中安装Microsoft Office才能工作...
  18. IBM SPSS Statistics 与用户自定义 Python 模块的集成及分析
  19. mac的键位说明——⌘、⌥、⇧、⌃、⎋代表哪个键
  20. QQ分组控件的简单实现

热门文章

  1. Mulesoft 开发前工作
  2. L1-047 装睡 java语言
  3. 《静儿的服务治理私房菜》网络模型的分类和职业规划思考
  4. tensorflow保存和加载npy文件
  5. u盘插在电脑上灯亮没有反应_U盘插上灯亮,但是状态栏不显示,我的电脑里 – 手机爱问...
  6. MongoDB原生脚本 - 检测order表LiveSource字段两侧包含空格的数据,并trim掉两端的空格*
  7. 钓鱼比赛(平均概率公式:1 - (1-p)^ t)----百度2016研发工程师在线编程题
  8. 【路径规划】贝塞尔曲线平滑路径
  9. Vue keep-alive的使用方法
  10. pdf文件加密码怎么设置?