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);
}

也许这样的转换在实质上跟使用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;
	            }
	            else
	                state = 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

猜你喜欢

转载自blog.csdn.net/sdhexu/article/details/128120292