Unity实现简单卡牌游戏框架

Unity卡牌游戏教程(一)简单框架

前言

项目的初衷是以项目形式串起unity各种零散知识,语言风格较详细(啰嗦)

需求

先来看需要实现什么东西

构建卡牌对象

简单的UI系统

UI和场景的简单交互

前期准备

打开unity,新建2D项目

左键点击Main Camera,在Inspector面板中点击Background修改背景颜色,同时将size修改为**屏幕像素高度/100f/2),比如你的屏幕像素是1920x1080,那么size就应该设置为5.4,这样屏幕上一个像素对应图像中一个像素(所谓Pixel-Perfect)

导入资源。在Project新建文件夹存放我们需要的资源,然后直接把文件(这个项目用到的全是图片)拖进Project面板里。我们新建的是2D项目,所以可以看到在Inspector面板里纹理类型是Sprite。需要注意的是Pixels Per Unit这一项,该数值为屏幕上的一个单元对应原图片的多少像素点,简单理解,该值越大,图片就显示得越小

(PS:导入图片时最好确保所有同一用途的图片大小以及Pixels Per Unit都保持一致,图片大小可以在点击图片后在Inspector面板右下角查看,如果大小不一建议在导入前用PhotoShop的裁剪工具处理,尤其是你素材是自己随便截下来的时候

构建卡牌对象

在Hierarchy窗口右键:2D Object->Sprite 新建一个空白物体,顺手在Inspector面板将他的Position改成(0,0,1)

(PS:由于Unity使用左手坐标系,xyz轴的正方向分别是右、上、前,离我们越远的物体Z值越大,因此个人习惯将摄像机的Z值设置为0,将其他物体Z值设置为1)

将图片拖拽到Sprite Renderer下Sprite处(下文简称为“槽”),完成“图片赋值”

但还没完!一张卡牌应该有卡背。在刚刚创建的对象下用同样的步骤新建一个子物体,将卡背图片拖拽,但这次将Position设置为(0,0,-0.1)此时的位置是与父物体的相对位置,为了保持神秘感,我们先将卡牌翻面,因此将Z值设为-0.1(理由见同上

是时候敲点代码了!在Project中Create一个脚本(希望你已经建好了一个用于存放脚本的文件夹),并将它拖给你的卡牌

//	BasicCard.cs
public class BasicCard : MonoBehaviour
{
    private GameObject Card_Back;
    
    public void OnMouseDown()
	{
        if (Card_Back.activeSelf)
            Card_Back.SetActive(false);
        else
            Card_Back.SetActive(true);
	}
	void Start()
    {
        //  查找子物体,获得卡背
        Card_Back = transform.Find("Card_Back").gameObject;
    }
}

transform.Find()函数能让你获取该物体下的子物体,是父子物体通信的有效手段,我们接下来还会遇到物体与物体间通信的问题

重写了鼠标响应事件函数之后,我们现在可以点击卡牌使它翻面了…吗?好像还不行,因为只有带有碰撞器组件的游戏物体才能对鼠标做出反应。于是我们给BasicCard添加一个Box Collider2D(点Add Component)
现在我们有卡牌了!
还差最后一步。我们不希望每次创建卡牌都要重复一次上述过程,因此可以用刚刚创建好的BasicCard创建一个预置体

将BasicCard从Hierarchy视图拖到Project视图中,你会发现Hierarchy视图中的箱子图标变蓝了。此时删除原来的BasicCard,对预置体没有影响

简单的UI系统

Unity有两种GUI系统,一种是直接模式(immedia mode),在每帧显示地发出绘制命令,完全基于代码,它的核心是OnGUI()方法,会在渲染完场景中所有物体后调用。它可以实现一些很简单的东西(比如FPS游戏中的准心),但缺点显而易见(顺便一提,可能是我匆匆忙忙学艺不精,感觉QT的UI逻辑就是这种,十分折磨)

所以我们选择另一种保留模式(retained mode),只需一次定义后系统自动每帧绘制,无需重新定义;而且能在编译器里工作,方便直观(最常用)

右键->UI->Canvas,我们创建了一个画布对象,之后所有的UI图像和操作都附加在上面。它缩放的很大,但不用害怕,因为场景中1个单位相当于UI上1个像素。你会发现自动创建了个EventSystem,暂时不管它

(P.S:Canvas组件下的Render Mode选项有三个模式,默认Overlay会将UI渲染在摄像机顶部;Camera则在前者基础上加上可以进行透视效果的旋转;World Space则将Canvas对象放置场景中,可以用该选项提高UI的浸入感,比如光环中将武器剩余子弹数显示在武器上)

新建一个Text和Button对象(自动变成Canvas的子物体),自行设置图片字体大小之类的(按照设置sprite的方法直接拖就行),之后分别放在右上角和左上角。为了在游戏画面缩放时能自适应屏幕大小,我们分别设置好对应的锚点(相对位置)

用脚本控制卡牌生成

在整合UI和场景之前,我们需要一些准备工作。我们需要一个SenceController帮我们自动化生成卡牌,以代替目前在inspect面板手动添加的方式

新建一个空物体,再写一个脚本,命名为SceneController

//	SceneController.cs
public class SceneController : MonoBehaviour
{
    [SerializeField] private GameObject CardPrefab;
    [SerializeField] private int MaxCardsNumber = 8;    //  最大总卡牌数
    [SerializeField] private int colNum = 4;            //  每行最大卡牌数
    [SerializeField] private float offset = 2.5f;       //  相邻卡牌间隔
    											    //	卡牌初始位置
    private Vector3 firstPos = new Vector3(-2.5f, -2.5f, 0.0f);
    private int CardsNum = 0;						  //  记录卡牌数
    void createCards()
    {
        while (CardsNum < MaxCardsNumber)
        {
            GameObject newCard = Instantiate(CardPrefab);
            //	控制卡牌按间隔分布
            if (CardsNum >= colNum)
                newCard.transform.position = 
                new Vector3((CardsNum - colNum) * offset, offset * 2, 1) + firstPos;
            else
                newCard.transform.position = 
                new Vector3(CardsNum * offset, 0, 1) + firstPos;
                
            ++CardsNum;
        }
    }
    void Start()
    {
        createCards();
    }
}

SerializeField意为序列化,简单理解就是能在inspect面板里面修改脚本中的属性(注意当你序列化变量之后会以inspect上的值为准,此时你光修改代码是无法改变变量的值的,必须在inspect作出修改)当然,将变量属性设置为public能达到同样的效果,但这就破坏了所谓封装性。所以SerializeField是实用性和封装性之间的一个小小妥协

另一个新东西是Instantiate()方法。该方法返回一个原对象的克隆值,新对象的激活状态和原对象保持一致。我们用刚刚创建好的卡牌预置体来生成新卡牌

将预置体拖入“组件槽”中,运行游戏,如无意外你会看到八张整齐排列的卡牌:
感觉有哪里不对...
好像还不戳,但你逐一点开之后发现八张都是一样的(和预制体保持一致)。想生成不同的卡牌有两种办法:

一、将所有不同种类的卡牌制成预置体

二、不预设图片,生成卡牌对象时在动态“赋值”

这里我们选择第二种方法,原因主要是不同的卡牌只有图片一个属性是不一样的,可以通过SceneController动态生成一个数组来给不同的卡牌“赋值”;其次生成不同预置体后还要一个个拖到“槽”上“赋值”,十分麻烦(其实可以用GameObject数组来管理优化,不过现阶段没必要这么麻烦,如无必要勿增实体

(P.S:不妨设想什么情况下第一种方法是更好的选择)

首先确定需求。我们需要一个存放Sprite的数组用于“赋值”(总比GameObject数组好,不是吗);接着修改代码,让其能随机地生成卡牌(的封面)

我们首先将卡牌预置体中的Sprite属性重新设置为None;接着修改脚本

//	SceneController.cs
//	之前已存在且无需修改的部分用...表示
public class SceneController : MonoBehaviour
{				...
	[SerializeField] private Sprite[] Card_Front;
}				...

保存后看inspect面板,在size一行输入你想要的大小,再将将对应sprite拖到“槽”上赋值

还需要稍微修改生成卡牌的代码,产生0—3的随机数给图片赋值

//	SceneController.cs
void createCards()
{
	while (CardsNum < MaxCardsNumber)
        {
             ...
            if (CardsNum >= colNum)
			...	
            else
             ...
             newCard.GetComponent<SpriteRenderer>().sprite = 									Card_Front[Random.Range(0,4)];
            ...
        }
}

GetComponent<>()可以获取本游戏物体上的组件;Random.Range(int min,int max)则返回一个范围内的随机int(包括min但不包括max)

(P.S.:浮点数重载的Random.Range(float min,float max)的范围是包括min也包括max,使用时要注意)
现在的效果是这样的

消息系统

unity中父子物体间通信可以用transform.Find()解决,那不同物体间的通信呢?unity其实提供了自带的消息系统(还记得我们创建Canvas对象时自动创建的那个EventSystem吗)其核心方法是FindObjectOfType().SendMessage("methodName",参数),FindObjectOfType()可以返回第一个类型为 T 的已加载的激活对象的游戏物体;SendMessage()则调用此游戏对象中的每个 [MonoBehaviour](file:///D:/Unity/ScriptReference/MonoBehaviour.html) 上名为 methodName 的方法,同时可以传递参数;结合这两个方法可以实现不同物体间(单对单)通信。优点是简单,同时能保证通信必定成功(计网中的面向连接可靠通信)因为发出的消息没被接受会报错…

缺点是只能实现单对单通信,虽然有FindObjectsOfType()版本能返回一个符合条件的对象数组,但挺麻烦(要写很多个SendMessage());另外FindObjectOfType().的效率很低,不建议每帧都使用。

但还有另一种选择,观察者模式。有通信需求的游戏物体挂载一个监听器,消息发出者向全体监听者广播一条消息,监听者发现这条消息是针对自己的,就做出响应(如果不是,那么就丢弃不管)这样就轻松实现了一对多通信

为了教学目的,这个例子中我们用第一种方法(其实是因为简单

UI和场景的简单交互

有来有回才算是交互!我们需要让UI响应场景的变化,同时也能通过在UI层的操作改变场景

先看需求:我们希望Text能显示场上的卡牌数;按Button可以生成卡牌

先实现第一部分:场景->UI

在Canvas下新建一个脚本,命名为UIController

//	UIController.cs
public class UIController : MonoBehaviour
{ 
    void showCardsNum(int num)
    {
        //	应该不陌生了
        Text t = transform.Find("Text").GetComponent<Text>();
        t.text = "目前场上卡牌数:" + num.ToString();
    }
}

然后在SceneController(下面简称SCer)给UIController(简称UICer)发信息(谁取的名字啊,又臭又长.jpg

//	SceneController.cs
void createCards()
{
    while (CardsNum < MaxCardsNumber)
    {
        ...
        //  unity自带的消息系统
        FindObjectOfType<Canvas>().SendMessage("showCardsNum", CardsNum);
    }
}

再实现第二部分:UI->场景

button想发送消息十分简单,点开button的inspect面板,发现有一个On Click()条目,左上角的“槽”设置响应模式(编译器下或是游戏运行时),点右下角的小加号,将SceneController对象拖到右下角的“槽”;右上角的“槽”可以选择调用该游戏物体下的组件中的方法,这里我们选SceneController->SendMessage(string),接着在文本输入域内写你想调用的方法名,我们写createCards

(P.S:OnClick()是按钮组件暴露给外部的唯一事件,但我们也有对所有类型的游戏物体都适用的,响应各种不同交互方式的办法,答案是使用EventTrigger组件;添加组件后点Add New Event Type,选择你想响应的活动。接下来的操作就和上面一样了)
甚至贴心写出能响应的事件

打开SCer,将createCards()从Start()删去

Congratulations!现在能看到完整效果了

在这里插入图片描述

回顾一下我们实现了什么东西

构建卡牌对象:有不同封面和默认卡背,能自动生成的卡牌

简单的UI系统:按钮和文本框

UI和场景的简单交互:按钮生成卡牌;文本框显示卡牌数

这节涉及的前置知识较多,代码量和操作难度其实不大,但有了基本框架后,我们可以做一些比较cool的东西了

下期预告:
小游♂戏

猜你喜欢

转载自blog.csdn.net/LuLou_/article/details/115191300