开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:1811

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1515|回复: 38

[蛮牛干货] 从零开始制作一款三消游戏

[复制链接]  [移动端链接]
排名
3
昨日变化

5312

主题

5761

帖子

2万

积分

Rank: 16

UID
1231
好友
183
蛮牛币
1445
威望
30
注册时间
2013-7-29
在线时间
2524 小时
最后登录
2017-8-17

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

发表于 2017-8-4 10:59:35 | 显示全部楼层 |阅读模式

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

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

x
  三消类游戏一直是游戏市场上经久不衰的休闲游戏,该类型也是源自于经典的俄罗斯方块玩法的一部分。三消游戏需要交换游戏中相邻的两个方格,以让3个或更多相同的方格连成直线,一旦连线成功,则消除这些连成线的同色方格,并使用新的方格进行填充,填充后如果还存在连线就可以达成Combo或多倍加分!
  本教程就为大家分享如何在Unity中制作这样一款三消游戏的完整过程,从创建底板填充方格开始,到统计步数并计算游戏得分,来自己做一款三消游戏。
  准备工作

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

1.jpg


  其中分别包含要用于游戏的动画、音效、字体、预制件、场景、脚本及图片资源。
  创建游戏底板
  打开Game场景,新建空游戏对象命名为BoardManager,该对象将用于生成游戏底板,并填充方格。然后将Scripts/Board and Grid文件夹下的BoardManager脚本拖拽至刚刚创建的BoardManager游戏对象上:
2.jpg


BoardManager脚本代码如下:

[C#] 纯文本查看 复制代码
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方法中新增以下代码:

[C#] 纯文本查看 复制代码
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方法代码为如下:

[C#] 纯文本查看 复制代码
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的纹理,然后播放交换音效。这里通过按下鼠标左键来操作方格,代码如下:

[C#] 纯文本查看 复制代码
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脚本中添加以下两个方法:

[C#] 纯文本查看 复制代码
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来生成围绕当前方格的列表,该循环将遍历各个方向与当前方格相邻的方格,并返回列表,以保证方格仅能与其相邻方格进行交换。
  保存代码后运行场景,效果如下:
9.gif


  检测相同方格进行消除
  消除可以拆解为几个步骤,首先判断是否出现3个及以上相连的同样方格,如果有,则消除已匹配的方格,并填充新方格。然后重复此步骤直至没有有效匹配。
  在Tile脚本中新增以下代码:

[C#] 纯文本查看 复制代码
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方法会在找到满足条件的匹配后,删除所有匹配的方格。
  运行游戏,效果如下:
10.gif


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

[C#] 纯文本查看 复制代码
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方法将生成新的方块来填满整个底板。
11.gif


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

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

  现在运行游戏,效果如下:

12.gif


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

[C#] 纯文本查看 复制代码
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方法中显示游戏结束面板。这里的等待是为了确保所有连击都被计算在总分内。


13.gif

14.jpg

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

评分

参与人数 1鲜花 +2 收起 理由
qq1186351245 + 2 很给力!

查看全部评分


跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

2初来乍到
142/150
排名
13192
昨日变化
317

0

主题

69

帖子

142

积分

Rank: 2Rank: 2

UID
224879
好友
0
蛮牛币
225
威望
0
注册时间
2017-6-2
在线时间
33 小时
最后登录
2017-8-17
发表于 2017-8-4 11:59:38 | 显示全部楼层
谢谢分享。。。。。。。。。。。。。

回复

使用道具 举报

排名
3377
昨日变化
21

18

主题

131

帖子

876

积分

Rank: 9Rank: 9Rank: 9

UID
6728
好友
7
蛮牛币
3720
威望
0
注册时间
2013-10-30
在线时间
349 小时
最后登录
2017-8-17

专栏作家社区QQ达人

发表于 2017-8-4 15:32:09 | 显示全部楼层
学习了

回复

使用道具 举报

4四处流浪
458/500
排名
4764
昨日变化
29

0

主题

104

帖子

458

积分

Rank: 4

UID
131010
好友
1
蛮牛币
538
威望
0
注册时间
2015-12-6
在线时间
134 小时
最后登录
2017-8-17
发表于 2017-8-4 17:18:07 | 显示全部楼层
阔以!!!

回复

使用道具 举报

7日久生情
1680/5000
排名
16568
昨日变化
15

2

主题

1424

帖子

1680

积分

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

UID
185807
好友
0
蛮牛币
2166
威望
0
注册时间
2016-11-22
在线时间
230 小时
最后登录
2017-8-16
发表于 2017-8-4 18:17:03 | 显示全部楼层
赞,学习了,谢谢分享

回复 支持 反对

使用道具 举报

7日久生情
2996/5000
排名
80
昨日变化

0

主题

309

帖子

2996

积分

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

UID
27214
好友
0
蛮牛币
10446
威望
0
注册时间
2014-5-29
在线时间
675 小时
最后登录
2017-8-17

VIP

QQ
发表于 2017-8-5 09:08:41 | 显示全部楼层
赞,支持干货!

回复

使用道具 举报

排名
13522
昨日变化
331

1

主题

43

帖子

99

积分

Rank: 2Rank: 2

UID
233944
好友
0
蛮牛币
226
威望
0
注册时间
2017-7-24
在线时间
22 小时
最后登录
2017-8-17
发表于 2017-8-5 10:16:00 | 显示全部楼层
谢谢谢谢寻寻寻寻寻寻寻寻寻   分享

回复 支持 反对

使用道具 举报

3偶尔光临
274/300
排名
9378
昨日变化
121

3

主题

150

帖子

274

积分

Rank: 3Rank: 3Rank: 3

UID
85086
好友
2
蛮牛币
270
威望
0
注册时间
2015-3-26
在线时间
41 小时
最后登录
2017-8-17
发表于 2017-8-5 13:01:05 | 显示全部楼层
qqqqqqqqqqqqqqq

回复 支持 反对

使用道具 举报

3偶尔光临
274/300
排名
9378
昨日变化
121

3

主题

150

帖子

274

积分

Rank: 3Rank: 3Rank: 3

UID
85086
好友
2
蛮牛币
270
威望
0
注册时间
2015-3-26
在线时间
41 小时
最后登录
2017-8-17
发表于 2017-8-5 13:04:11 | 显示全部楼层
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

回复 支持 反对

使用道具 举报

3偶尔光临
274/300
排名
9378
昨日变化
121

3

主题

150

帖子

274

积分

Rank: 3Rank: 3Rank: 3

UID
85086
好友
2
蛮牛币
270
威望
0
注册时间
2015-3-26
在线时间
41 小时
最后登录
2017-8-17
发表于 2017-8-5 14:16:37 | 显示全部楼层
aaaaaaaaaaaaaaaaaaaaaaaaaaa

回复 支持 反对

使用道具 举报

3偶尔光临
274/300
排名
9378
昨日变化
121

3

主题

150

帖子

274

积分

Rank: 3Rank: 3Rank: 3

UID
85086
好友
2
蛮牛币
270
威望
0
注册时间
2015-3-26
在线时间
41 小时
最后登录
2017-8-17
发表于 2017-8-5 14:20:07 | 显示全部楼层
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

回复 支持 反对

使用道具 举报

排名
49357
昨日变化
196

0

主题

13

帖子

59

积分

Rank: 2Rank: 2

UID
157365
好友
0
蛮牛币
135
威望
0
注册时间
2016-7-17
在线时间
44 小时
最后登录
2017-8-7
发表于 2017-8-5 14:20:20 | 显示全部楼层
好像没有资源包吗

回复 支持 反对

使用道具 举报

2初来乍到
148/150
排名
12642
昨日变化
258

2

主题

56

帖子

148

积分

Rank: 2Rank: 2

UID
232569
好友
0
蛮牛币
314
威望
0
注册时间
2017-7-24
在线时间
46 小时
最后登录
2017-8-18
发表于 2017-8-5 20:09:55 | 显示全部楼层
只能学习思路吗,没找到素材,不能跟着学

回复 支持 反对

使用道具 举报

排名
12909
昨日变化
9

0

主题

6

帖子

86

积分

Rank: 2Rank: 2

UID
177120
好友
0
蛮牛币
148
威望
0
注册时间
2016-10-20
在线时间
40 小时
最后登录
2017-8-14
发表于 2017-8-5 21:53:31 | 显示全部楼层
補上網上找到的
资源包: https://koenig-media.raywenderlich.com/uploads/2017/01/Match-3-Game-Starter.zip
不知道你們能不能下載

回复 支持 反对

使用道具 举报

3偶尔光临
264/300
排名
6869
昨日变化
68

0

主题

74

帖子

264

积分

Rank: 3Rank: 3Rank: 3

UID
149061
好友
0
蛮牛币
162
威望
0
注册时间
2016-5-18
在线时间
56 小时
最后登录
2017-8-17
发表于 2017-8-6 07:25:50 | 显示全部楼层
谢谢分享

回复

使用道具 举报

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

本版积分规则

快速回复 返回顶部 返回列表