找回密码
 注册帐号

扫一扫,访问微社区

士郎 Unity教程 | 从零开始制作一款三消游戏

136
回复
3237
查看
  [ 复制链接 ]
排名
1
昨日变化

7846

主题

8404

帖子

3万

积分

Rank: 16

UID
1231
好友
186
蛮牛币
11088
威望
30
注册时间
2013-7-29
在线时间
4025 小时
最后登录
2019-6-19

活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

2018-12-11 11:48:33 显示全部楼层 阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
三消类游戏一直是游戏市场上经久不衰的休闲游戏,该类型也是源自于经典的俄罗斯方块玩法的一部分。三消游戏需要交换游戏中相邻的两个方格,以让3个或更多相同的方格连成直线,一旦连线成功,则消除这些连成线的同色方格,并使用新的方格进行填充,填充后如果还存在连线就可以达成Combo或多倍加分!

本教程就为大家分享如何在Unity中制作这样一款三消游戏的完整过程,从创建底板填充方格开始,到统计步数并计算游戏得分,来自己做一款三消游戏。

准备工作



下载项目初始资源(请回复本帖哦~):
游客,如果您要查看本帖隐藏内容请回复




将项目初始资源导入Unity项目,资源目录如下:

1.jpg


其中分别包含要用于游戏的动画、音效、字体、预制件、场景、脚本及图片资源。

创建游戏底板

打开Game场景,新建空游戏对象命名为BoardManager,该对象将用于生成游戏底板,并填充方格。然后将Scripts/Board and Grid文件夹下的BoardManager脚本拖拽至刚刚创建的BoardManager游戏对象上:

2.jpg


BoardManager脚本代码如下:



[AppleScript] 纯文本查看 复制代码
public static BoardManager instance;     // 1
    public List<Sprite> characters = new List<Sprite>();     // 2
    public GameObject tile;      // 3
    public int xSize, ySize;     // 4
 
    private GameObject[,] tiles;      // 5
 
    public bool IsShifting { get; set; }     // 6
 
    void Start () {
        instance = GetComponent<BoardManager>();     // 7
 
        Vector2 offset = tile.GetComponent<SpriteRenderer>().bounds.size;
        CreateBoard(offset.x, offset.y);     // 8
    }
 
    private void CreateBoard (float xOffset, float yOffset) {
        tiles = new GameObject[xSize, ySize];     // 9
 
        float startX = transform.position.x;     // 10
        float startY = transform.position.y;
 
        for (int x = 0; x < xSize; x++) {      // 11
            for (int y = 0; y < ySize; y++) {
                GameObject newTile = Instantiate(tile, new Vector3(startX + (xOffset * x), startY +                                                                 (yOffset * y), 0), tile.transform.rotation);
                tiles[x, y] = newTile;
            }
        }
    }




  • BoardManager脚本声明了一个单例名为instance,便于其它脚本访问该脚本。
  • characters是方格需要用到的图片列表。
  • tile是用于初始化方格底板的预制件。
  • xSize及ySize是底板横向及纵向的数量。
  • tiles是保存底板方格的二维数组。
  • IsShifting函数用于检测是否需要填充方格。
  • Start函数用于初始化BoardManager脚本实例。
  • CreateBoard函数用于创建底板,参数为方初始化格图片的宽度及高度,根据此前定义的底板方格数量及底板方格预制件,来初始化整个底板。



在层级视图中选中BoardManager对象,然后在检视视图中将BoardManager脚本的Characters元素数量设为7,然后将Sprites/Characters文件夹下的图片绑定到数组元素。最后将Prefabs文件夹下的Tile预制件绑定到脚本的Tile字段,将BoardManager脚本的X Size与Y Size分别设为8、12。完成后如下图:

3.jpg


然后运行场景,可以看到底板能够正常创建,但出现了偏移:

4.jpg


这是因为底板方格从左下角开始最先生成,而首个方格坐标为BoardManager对象坐标。下面调整BoardManager对象的坐标为(-2.66, -3.83, 0),让BoardManager坐标位于屏幕左下角。

5.jpg


随机生成底板

打开BoardManager脚本,在CreateBoard方法中新增以下代码:

[AppleScript] 纯文本查看 复制代码
newTile.transform.parent = transform; // 1
Sprite newSprite = characters[Random.Range(0, characters.Count)]; // 2
newTile.GetComponent<SpriteRenderer>().sprite = newSprite; // 3




以上代码的作用是将所有底板方格的父对象均设置为BoardManager,保持层级视图干净整洁,并从之前定义的数组中随机选取一张图片来初始化方格。现在运行游戏,效果如下:

6.jpg


上面生成的方格还有些小问题,就是一开始就出现了连续的可消除方格,下面就来解决这个问题。

避免初始化重复方格

底板方格按从下到上从左到右的顺序创建,所以在创建新方格前要对相邻的方格进行判断。

7.jpg


上图所示的循环会从左下方开始遍历方格,每次迭代都会获取当前方格左侧及下方的方格,然后通过随机选取这两个方格,来保证不会在初始化底板时出现3个及以上相连的同一方格。更改CreateBoard方法代码为如下:



[AppleScript] 纯文本查看 复制代码
 private void CreateBoard (float xOffset, float yOffset) {
            tiles = new GameObject[xSize, ySize];
 
    float startX = transform.position.x;
            float startY = transform.position.y;
 
            Sprite[] previousLeft = new Sprite[ySize]; // Add this line
            Sprite previousBelow = null; // Add this line
 
            for (int x = 0; x < xSize; x++) {
                    for (int y = 0; y < ySize; y++) {
                            GameObject newTile = Instantiate(tile, new Vector3(startX + (xOffset * x), startY + (yOffset * y), 0), tile.transform.rotation);
                            tiles[x, y] = newTile;
                            newTile.transform.parent = transform; // Add this line
 
                            List<Sprite> possibleCharacters = new List<Sprite>();
                            possibleCharacters.AddRange(characters);
 
                            possibleCharacters.Remove(previousLeft[y]);
                            possibleCharacters.Remove(previousBelow);
 
                            Sprite newSprite = possibleCharacters[Random.Range(0, possibleCharacters.Count)];
                            newTile.GetComponent<SpriteRenderer>().sprite = newSprite;
                            previousLeft[y] = newSprite;
                            previousBelow = newSprite;
                    }
    }
}




运行游戏,不会出现重复相连的3个方格了:

8.jpg


交换方格

下面来实现选中并交换相邻的方格。打开Tile脚本,其中Select方法用于选中方格后替换方格图片并播放选中音效,Deselect方法用于恢复选中方格的图片,并提示当前未选中任意方格。SwapSprite方法用于交换两个相邻方格,即替换两个Sprite的纹理,然后播放交换音效。这里通过按下鼠标左键来操作方格,代码如下:

[AppleScript] 纯文本查看 复制代码
void Awake() {
                render = GetComponent<SpriteRenderer>();
    }
 
        private void Select() {
                isSelected = true;
                render.color = selectedColor;
                previousSelected = gameObject.GetComponent<Tile>();
                SFXManager.instance.PlaySFX(Clip.Select);
        }
 
        private void Deselect() {
                isSelected = false;
                render.color = Color.white;
                previousSelected = null;
        }
 
        void OnMouseDown() {
                // Not Selectable conditions
                if (render.sprite == null || BoardManager.instance.IsShifting) {
                        return;
                }
 
                if (isSelected) { // Is it already selected?
                        Deselect();
                } else {
                        if (previousSelected == null) { // Is it the first tile selected?
                                Select();
                        } else {
                                if (GetAllAdjacentTiles().Contains(previousSelected.gameObject)) { // Is it an adjacent tile?
                                        SwapSprite(previousSelected.render);
                                        previousSelected.ClearAllMatches();
                                        previousSelected.Deselect();
                                        ClearAllMatches();
                                } else {
                                        previousSelected.GetComponent<Tile>().Deselect();
                                        Select();
                                }
                        }
                }
        }
 
        public void SwapSprite(SpriteRenderer render2) {
                if (render.sprite == render2.sprite) {
                        return;
                }
 
                Sprite tempSprite = render2.sprite;
                render2.sprite = render.sprite;
                render.sprite = tempSprite;
                SFXManager.instance.PlaySFX(Clip.Swap);
                GUIManager.instance.MoveCounter--; // Add this line here
        }




这里还需要保证仅相邻的方格才能进行交换,在Tile脚本中添加以下两个方法:


[AppleScript] 纯文本查看 复制代码
private GameObject GetAdjacent(Vector2 castDir) {
        RaycastHit2D hit = Physics2D.Raycast(transform.position, castDir);
        if (hit.collider != null) {
                return hit.collider.gameObject;
        }
        return null;
}
 
private List<GameObject> GetAllAdjacentTiles() {
        List<GameObject> adjacentTiles = new List<GameObject>();
        for (int i = 0; i < adjacentDirections.Length; i++) {
                adjacentTiles.Add(GetAdjacent(adjacentDirections[ i ]));
        }
        return adjacentTiles;
}




其中GetAdjacent方法用于检测某个固定方向是否存在方格,如果有,则返回此方格。GetAllAdjacentTiles方法则调用GetAdjacent来生成围绕当前方格的列表,该循环将遍历各个方向与当前方格相邻的方格,并返回列表,以保证方格仅能与其相邻方格进行交换。

保存代码后运行场景,效果如下:

1.1.gif


检测相同方格进行消除

消除可以拆解为几个步骤,首先判断是否出现3个及以上相连的同样方格,如果有,则消除已匹配的方格,并填充新方格。然后重复此步骤直至没有有效匹配。

在Tile脚本中新增以下代码:



[AppleScript] 纯文本查看 复制代码
private List<GameObject> FindMatch(Vector2 castDir) {
        List<GameObject> matchingTiles = new List<GameObject>();
        RaycastHit2D hit = Physics2D.Raycast(transform.position, castDir);
        while (hit.collider != null && hit.collider.GetComponent<SpriteRenderer>().sprite == render.sprite) {
                matchingTiles.Add(hit.collider.gameObject);
                hit = Physics2D.Raycast(hit.collider.transform.position, castDir);
        }
        return matchingTiles;
}
 
private void ClearMatch(Vector2[] paths) {
        List<GameObject> matchingTiles = new List<GameObject>();
        for (int i = 0; i < paths.Length; i++) { matchingTiles.AddRange(FindMatch(paths[i])); }
        if (matchingTiles.Count >= 2) {
                for (int i = 0; i < matchingTiles.Count; i++) {
                        matchingTiles[i].GetComponent<SpriteRenderer>().sprite = null;
                }
                matchFound = true;
        }
}
 
private bool matchFound = false;
public void ClearAllMatches() {
        if (render.sprite == null)
                return;
 
        ClearMatch(new Vector2[2] { Vector2.left, Vector2.right });
        ClearMatch(new Vector2[2] { Vector2.up, Vector2.down });
        if (matchFound) {
                render.sprite = null;
                matchFound = false;
                StopCoroutine(BoardManager.instance.FindNullTiles()); //Add this line
                StartCoroutine(BoardManager.instance.FindNullTiles()); //Add this line
                SFXManager.instance.PlaySFX(Clip.Clear);
        }
}




FindMatch方法接收一个Vector2参数,用于表示所有射线投射的方向,新建GameObject列表来保存所有匹配条件的方格,从方格朝参数方向投射射线,直至射线未碰撞到任何方格或与当前方格不一致时停止,然后返回匹配条件的Sprite列表。

ClearMatch方法会按照给定路径寻找相同的方格,并相应消除所有匹配的方格。即判断FindMatch方法返回的列表中,是否有相连为直线的3个及以上相同方格。如果有,则将matchFound设为True。ClearAllMatch方法会在找到满足条件的匹配后,删除所有匹配的方格。

运行游戏,效果如下:

1.2.gif


填充空白方格

在消除方格后,还需要为其填充新的方格。在BoardManager脚本中加入以下代码:



[AppleScript] 纯文本查看 复制代码
public IEnumerator FindNullTiles() {
        for (int x = 0; x < xSize; x++) {
                for (int y = 0; y < ySize; y++) {
                        if (tiles[x, y].GetComponent<SpriteRenderer>().sprite == null) {
                                yield return StartCoroutine(ShiftTilesDown(x, y));
                                break;
                        }
                }
        }
 
        for (int x = 0; x < xSize; x++) {
                for (int y = 0; y < ySize; y++) {
                        tiles[x, y].GetComponent<Tile>().ClearAllMatches();
                }
        }
}
 
private IEnumerator ShiftTilesDown(int x, int yStart, float shiftDelay = .03f) {
        IsShifting = true;
        List<SpriteRenderer> renders = new List<SpriteRenderer>();
        int nullCount = 0;
 
        for (int y = yStart; y < ySize; y++) {
                SpriteRenderer render = tiles[x, y].GetComponent<SpriteRenderer>();
                if (render.sprite == null) {
                        nullCount++;
                }
                renders.Add(render);
        }
 
        for (int i = 0; i < nullCount; i++) {
                GUIManager.instance.Score += 50; // Add this line here
                yield return new WaitForSeconds(shiftDelay);
                for (int k = 0; k < renders.Count - 1; k++) {
                        renders[k].sprite = renders[k + 1].sprite;
                        renders[k + 1].sprite = GetNewSprite(x, ySize - 1);
                }
        }
        IsShifting = false;
}
 
private Sprite GetNewSprite(int x, int y) {
        List<Sprite> possibleCharacters = new List<Sprite>();
        possibleCharacters.AddRange(characters);
 
        if (x > 0) {
                possibleCharacters.Remove(tiles[x - 1, y].GetComponent<SpriteRenderer>().sprite);
        }
        if (x < xSize - 1) {
                possibleCharacters.Remove(tiles[x + 1, y].GetComponent<SpriteRenderer>().sprite);
        }
        if (y > 0) {
                possibleCharacters.Remove(tiles[x, y - 1].GetComponent<SpriteRenderer>().sprite);
        }
 
        return possibleCharacters[Random.Range(0, possibleCharacters.Count)];
}




其中FindNullTiles方法用于查找是否存在空的方格,如果有,则调用ShiftTilesDown方法将周围的方格填充进来,该方法有三个参数,分别是X索引,Y索引以及延迟时间,X、Y决定了哪一块方格需要移动,这里仅实现向下填充,所以X值是固定了,仅Y值会变。GetNewSprite方法将生成新的方块来填满整个底板。

1.3.gif


连击

新填充的方格可能会再次出现符合条件的匹配,所以新填充底板后要再次进行判断。再找到匹配后再次匹配成功,就是一次连击。所以在上面的FindNullTiles方法中,通过以下代码循环判断是否出现匹配:



[AppleScript] 纯文本查看 复制代码
for (int x = 0; x < xSize; x++) {
                for (int y = 0; y < ySize; y++) {
                        tiles[x, y].GetComponent<Tile>().ClearAllMatches();
                }
        }




现在运行游戏,效果如下


1.4.gif

添加计步器与分数

下面实现玩家步数记录,并统计游戏分数。打开Scripts/Managers文件夹下的GUIManager脚本,该脚本用于管理游戏UI,显示步数及分数文本。脚本代码如下:

[AppleScript] 纯文本查看 复制代码
public static GUIManager instance;
 
        public GameObject gameOverPanel;
        public Text yourScoreTxt;
        public Text highScoreTxt;
 
        public Text scoreTxt;
        public Text moveCounterTxt;
 
        private int score, moveCounter;
 
        void Awake() {
                instance = GetComponent<GUIManager>();
                moveCounter = 99;
        }
 
        // Show the game over panel
        public void GameOver() {
                GameManager.instance.gameOver = true;
 
                gameOverPanel.SetActive(true);
 
                if (score > PlayerPrefs.GetInt("HighScore")) {
                        PlayerPrefs.SetInt("HighScore", score);
                        highScoreTxt.text = "New Best: " + PlayerPrefs.GetInt("HighScore").ToString();
                } else {
                        highScoreTxt.text = "Best: " + PlayerPrefs.GetInt("HighScore").ToString();
                }
 
                yourScoreTxt.text = score.ToString();
        }
 
        public int Score {
                get {
                        return score;
                }
 
                set {
                        score = value;
                        scoreTxt.text = score.ToString();
                }
        }
 
        public int MoveCounter {
                get {
                        return moveCounter;
                }
 
                set {
                        moveCounter = value;
                        if (moveCounter <= 0) {
                                moveCounter = 0;
                                StartCoroutine(WaitForShifting());
                        }
                        moveCounterTxt.text = moveCounter.ToString();
                }
        }
 
        private IEnumerator WaitForShifting() {
                yield return new WaitUntil(() => !BoardManager.instance.IsShifting);
                yield return new WaitForSeconds(.25f);
                GameOver();
        }




在Awake中获取脚本引用,并初始化步数。Score及MoveCounter函数用于在每次更新分数值或步数时,UI界面上的文本也会同时更新。当步数减少至0时,游戏结束。此时会通过WaitForShifting协程在等待0.25秒后调用GameOver方法,并在GameOver方法中显示游戏结束面板。这里的等待是为了确保所有连击都被计算在总分内。

1.5.gif


总结

到这里本篇教程就结束了,当然大家还可以在理解游戏机制后添加更多的玩法,包括限时结算模式、增加不同关卡与底板类型、连击的积分计算规则,或是为消除方格添加一些酷炫的粒子效果等等。后面就留给大家自行扩展与发挥了!

下载完整项目(请回复本帖哦~):



游客,如果您要查看本帖隐藏内容请回复




Via:unity论坛
回复

使用道具 举报

5熟悉之中
883/1000
排名
3817
昨日变化

0

主题

130

帖子

883

积分

Rank: 5Rank: 5

UID
246100
好友
0
蛮牛币
994
威望
0
注册时间
2017-9-26
在线时间
359 小时
最后登录
2019-5-30
2018-12-11 13:36:17 显示全部楼层
厉害了,大佬
回复

使用道具 举报

6蛮牛粉丝
1120/1500
排名
2867
昨日变化

0

主题

108

帖子

1120

积分

Rank: 6Rank: 6Rank: 6

UID
108091
好友
0
蛮牛币
240
威望
0
注册时间
2015-6-14
在线时间
500 小时
最后登录
2019-6-18
2018-12-11 13:48:51 显示全部楼层
学习学习
回复

使用道具 举报

7日久生情
1595/5000
排名
1995
昨日变化

1

主题

217

帖子

1595

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
150319
好友
0
蛮牛币
1088
威望
0
注册时间
2016-5-30
在线时间
697 小时
最后登录
2019-6-19
2018-12-11 14:04:10 显示全部楼层
xxx学习学习学习学习
回复 支持 反对

使用道具 举报

4四处流浪
440/500
排名
7194
昨日变化

0

主题

132

帖子

440

积分

Rank: 4

UID
267358
好友
0
蛮牛币
595
威望
0
注册时间
2018-2-1
在线时间
128 小时
最后登录
2019-6-13
2018-12-11 14:22:29 显示全部楼层
学习
回复

使用道具 举报

8常驻蛮牛
5497/10000
排名
7
昨日变化

0

主题

313

帖子

5497

积分

Rank: 8Rank: 8

UID
981
好友
0
蛮牛币
17952
威望
0
注册时间
2013-7-18
在线时间
1502 小时
最后登录
2019-6-19

活力之星

2018-12-11 14:53:08 显示全部楼层
下来,空闲时间研究下
回复 支持 反对

使用道具 举报

5熟悉之中
820/1000
排名
5758
昨日变化

9

主题

320

帖子

820

积分

Rank: 5Rank: 5

UID
285521
好友
0
蛮牛币
1444
威望
0
注册时间
2018-6-12
在线时间
239 小时
最后登录
2019-6-13
2018-12-11 14:54:47 显示全部楼层
666666666666666666666666666
回复 支持 反对

使用道具 举报

7日久生情
4439/5000
排名
167
昨日变化

19

主题

325

帖子

4439

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
67323
好友
1
蛮牛币
14239
威望
0
注册时间
2015-1-10
在线时间
1817 小时
最后登录
2019-6-19
2018-12-11 15:09:28 显示全部楼层
三消的项目示例
回复

使用道具 举报

4四处流浪
474/500
排名
39859
昨日变化

1

主题

255

帖子

474

积分

Rank: 4

UID
160799
好友
0
蛮牛币
15
威望
0
注册时间
2016-8-5
在线时间
212 小时
最后登录
2019-5-13
2018-12-11 15:50:01 显示全部楼层
66666666666666666
回复 支持 反对

使用道具 举报

4四处流浪
388/500
排名
10343
昨日变化

0

主题

185

帖子

388

积分

Rank: 4

UID
248391
好友
0
蛮牛币
263
威望
0
注册时间
2017-10-12
在线时间
93 小时
最后登录
2019-6-19
2018-12-11 15:50:06 显示全部楼层
厉害厉害
回复

使用道具 举报

4四处流浪
312/500
排名
7799
昨日变化

3

主题

68

帖子

312

积分

Rank: 4

UID
254117
好友
4
蛮牛币
1740
威望
0
注册时间
2017-11-13
在线时间
91 小时
最后登录
2019-3-15
2018-12-11 15:50:48 显示全部楼层
不错不错,谢谢分享
回复 支持 反对

使用道具 举报

7日久生情
2290/5000
排名
1393
昨日变化

0

主题

709

帖子

2290

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
135463
好友
0
蛮牛币
21
威望
0
注册时间
2016-1-23
在线时间
691 小时
最后登录
2019-6-19
2018-12-11 16:24:01 显示全部楼层
hhhhhhhhhhhhhhhhh6666666666666666666
回复 支持 反对

使用道具 举报

3偶尔光临
164/300
排名
11372
昨日变化

0

主题

29

帖子

164

积分

Rank: 3Rank: 3Rank: 3

UID
51884
好友
0
蛮牛币
24
威望
0
注册时间
2014-10-29
在线时间
47 小时
最后登录
2019-6-17
2018-12-11 16:59:05 显示全部楼层
12312312312312312323
回复 支持 反对

使用道具 举报

5熟悉之中
808/1000
排名
3744
昨日变化

0

主题

45

帖子

808

积分

Rank: 5Rank: 5

UID
226917
好友
0
蛮牛币
2345
威望
0
注册时间
2017-6-15
在线时间
361 小时
最后登录
2019-6-6
2018-12-11 16:59:48 显示全部楼层
学习学习....
回复

使用道具 举报

5熟悉之中
549/1000
排名
5141
昨日变化

0

主题

47

帖子

549

积分

Rank: 5Rank: 5

UID
281427
好友
0
蛮牛币
909
威望
0
注册时间
2018-5-16
在线时间
222 小时
最后登录
2019-6-18
2018-12-11 17:02:07 显示全部楼层
学习学习,感谢大佬分享心得
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则