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


    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);
    }
    
    • 1
    • 2
    • 3
    • 4

    也许这样的转换在实质上跟使用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];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上面代码,定义了一个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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    棋盘坐标转换

    只要在游戏开始的时候,调用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);
    }
    
    • 1
    • 2
    • 3
    • 4

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

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

    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就是棋盘坐标
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    游戏逻辑

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

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

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

    思考状态 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如上,在玩家思索状态下,如果检测到了鼠标左键被按下,那么就发射一条射线,判断是否点击了棋盘上的己方棋子,如果是己方棋子,则继续判断这个棋子是不是可以被拿起:如果棋子四周有其他棋子,也就是说这枚棋子实际上不能进行移动,那么这枚棋子就不能被拿起,比如说这一枚:
    不应该能够被拿起
    只有点击的是能够被拿起的棋子,那么就拿起这枚棋子,并且将状态切换到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;
    	            }
    	            else
    	                state = PlayState.PlayerThink;
    	        }
    	    }
    	}
    	
    	break;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    放置棋子状态 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这个状态的逻辑就比较简单些,主要是完成棋子移动的动画,并且,移动完成后,进行是否赢了的判断、是否杀死了对方的棋子,如果是杀死的是防御方的王,也要判定赢了;还有,如果敌人虽然仍有棋子,但全部无法移动,比如被堵到角落,也需要判定胜利。如果判断一方胜利,那么就结束游戏。然后将状态切换到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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    尚未开发的功能
    • UI
    • AI
    源码下载

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

  • 相关阅读:
    php表单提交并自动发送邮件给某个邮箱(示例源码下载)
    Windows10系统下C#部署Paddlex模型
    [Linux打怪升级之路]-秒懂进程地址空间
    【Mysql】内置函数
    MySQL锁详解
    如何查找特定基因集合免疫基因集 炎症基因集
    半车(前后、左右)、整车悬架模型仿真分析
    图解机器学习 | 降维算法详解
    BAT023:将当前目录同名文件(不包括扩展名)整理到以其命名的文件夹内
    NOI2022游记
  • 原文地址:https://blog.csdn.net/sdhexu/article/details/128120292