植物大战僵尸的制作(不定时更新)

叱咤月海鱼鱼猫 / 2024-11-13 / 原文

植物大战僵尸的制作

[点击直达github](1zero0/PlantVSZombies: 学习制作植物大战僵尸并熟悉Unity)

1.创建项目

点击右上角新项目

image-20241011163848560

选择2D(built-In Render Pipeline)(红色箭头),修改自己项目的名字(蓝色箭头),选择自己想要的地址(绿色箭头)

image-20241011163920078

2.项目

将资源包(红色箭头)拖入到Assets(蓝色箭头)中并等待一段时间。

image-20241011164343956

2.1 导入素材

如下图中,Audio就是音效Images就是相关的所有图片,其中Music中包含的就是背景音乐,随后点击右下角Import

image-20241011164456596

导入后就会在这里生成一个Audio文件夹(红色箭头)和Images文件夹(蓝色箭头)

image-20241011205018263

随后点开Audio中的MusicSound文件夹,会看到导入的音频,随便选一个,然后在右边可以点击播放(红色箭头)

:如果右边没有如下图的ch 1ch 2可以点击AssetBundle上的bgm就会弹出如图所示的样子

image-20241011205138490

3.项目制作

3.1 导入背景

再Background中找到这个白天场景(红色箭头)

image-20241014153145327

Pixels Per Unit指的是图片导入到unity中的一个单位转换,60像素转换为一米。(再unity中一个格子代表一米)

image-20241014153244104

随后将场景拖入到左侧的Scene中

image-20241014155927338

随后点击左上角Scene右侧的Game(游戏视角),并选择Full HD(1920x1080)

image-20241014160454560

随后可以通过右边的X和Y进行场景的位置改动

image-20241014161727941

随后导入卡片,以便于种植,植物再Card中

image-20241014161921870

因为植物卡片需要卡片槽(用于放置植物卡片),这里先找到卡片槽,在UI中

image-20241014162207123

  1. 随后在上面Hierarchy中右键
  2. 选择UI
  3. 选择Image

image-20241014162430279

  1. (先双击Canvas聚焦)先点击Image
  2. 选择右侧Image中的Source Image,

image-20241014162742999

在弹出的窗口中选择卡片槽

image-20241014162827306

随后点击Image中的Set Native Size

image-20241014163023406

随后将卡片槽拖拽到左上角的位置

image-20241014163046787

然后通过Game来确认它的最终位置,还是通过右侧Pos X和Pox Y进行左右和上下的移动

image-20241014163122844

  1. 选择定位
  2. 选择左上角

image-20241014163345298

3.2 植物卡片

先像上面一样创建一个Image

image-20241014163553181

随后将新建的图层拉到卡片槽的位置,并找到向日葵(可种植状态)

image-20241014163748337

随后复制一个图层并选择灰色的向日葵(不可种植状态)

image-20241014164021464

随后再次复制一个图层,并选择黑色图层(冷却状态中)

image-20241014164107731

  1. 选择Color
  2. 选择透明度

image-20241014164247413

随后在Image Type中选择Simple

image-20241014164343010

随后在选择Fill Method中的Vertical(上下)(Horizontal:水平,Radial 360:圆圈型360度,其余两个同理)

image-20241014164534844

随后选择Fill Origin中将Bottom(从下往上)改为Top(从上往下)

image-20241014164705155

随后将这三张图片分到一个组中

image-20241014165032931

并将其命名为CardTemplate,并将下面的卡片分别命名为CardLight(可种植),CardGray(不可种植),CardMask(冷却中)

image-20241014165215615

随后我们要创建一个文件夹来存放这些卡片,即将CardTemplate文件拖入到我们新建的Prefabs中

image-20241014165526556

随后将卡槽和植物卡片共同组建一个父类(也可以将植物卡片放到卡槽下),并起名为CardListUI

image-20241014165716774

这里要记得将CardListUI也定位于左上角

image-20241014165820092

3.3 为植物卡片添加脚本使之达到各种状态

先创建一个新的文件夹Scripts用于存放脚本

image-20241014170034284

  1. 点击CardTemplate
  2. 点击Add Component
  3. 输入Card,回车(两次,随后等待创建好)

image-20241014170143150

随后将新建的Card脚本拖入到Scripts中

image-20241014170312428

随后双击点开Card,在内部写入代码

enum CardState
{
    Cooling,
    WaitingSun,
    Ready
}

public class Card : MonoBehaviour
{
    // 冷却 可以被点击 不可用
    private CardState cardState = CardState.Cooling;
    
    //要控制植物卡片的状态,就要先获取三种状态
    public GameObject cardLight;
    public GameObject cardGary;
    public Image cardMask;

    private void Update()
    {
        switch (cardState)
        {
            case CardState.Cooling:
                CoolingUpdate();
                break;
            case CardState.WaitingSun:
                WaitingSunUpdate();
                break;
            case CardState.Ready:
                ReadyUpdate();
                break;
            default:
                break;
        }
    }

    // 卡片等待动作,首先要确定卡片的时间,即冷却时间,通过计时器来实现,通过剩余时间(冷却时间-计时器时间)/一个冷却时TransitionToWaitingSun()间生成的一个比例来给FillAmount达到一个控制效果,这样就可以使得FillAmount达到一个逐步地减少。当冷却时间到了之后就会执行TransitionToWaitingSun()转换到一个等待阳光的状态,然后就会转到WaitingSunUpdate()状态
    void CoolingUpdate()
    {
        cdTimer += Time.deltaTime;

    cardMask.fillAmount = (cdTime - cdTimer) / cdTime;  // 剩余时间的比例

    if (cdTimer >= cdTime)
    {
        TransitionToWaitingSun();
    }
    }
    void WaitingSunUpdate()
    {

    }
    void ReadyUpdate()
    {

    }
    
     void TransitionToWaitingSun()
 {
     cardState = CardState.WaitingSun;   // 先改变植物卡片的状态(冷却状态改为灰色状态)

         // 植物状态的启用和禁用
     cardLight.SetActive(false);     // 将植物卡片亮禁用
     cardGary.SetActive(true);       // 植物卡片灰启用
     cardMask.gameObject.SetActive(false);   // 将等待状态禁用
 }
}

保存之后回到unity,将三种状态拖入到对应的地方(如图中红色箭头)

image-20241020201148351

3.4 阳光的计数

因为植物的种植需要阳光,所以先创建一个Empty,并改名为Manager

image-20241021200905517

随后点击右侧Add Component然后添加一个Sun Manager脚本,并双击点开

image-20241021200940111

随后先将SunManager拖入到Scripts中,随后新建文件夹Manager,并将SunManager拖入其中

image-20241021201038695

随后编写SunManager的代码,

public static SunManager Instance { get; private set; }
private void Awake()	// 进行赋值
{
    Instance = this;
}
[SerializeField]	// 加上一个序列化的标签,以便于可以随时改变
private int sunPoint;	// 使用私有不会让值在外面随便改变
public int SunPoint		// 用来获取阳光值
{
    get { return sunPoint; }
}

然后回到card中,将时间私有化,以免随便被换掉并创建序列化标签便于改变。

[SerializeField]
private  float cdTime = 2;    // 冷却时间
private  float cdTimer = 0;   // 计时器,从零开始(可以从零增加到2 ,也可以从最大减少到零)

设置一个植物的需要的阳光的值

[SerializeField]
private int needSunPoint = 50;

再回到如果需要的阳光值小于已有的阳光值,则植物卡片状态变为准备状态

void WaitingSunUpdate()
{
    if (needSunPoint <= SunManager.Instance.SunPoint)
    {
        TransitionToReady();
    }
}

编写转换为准备状态的代码(高亮改为启用,等待阳光状态改为禁用,等待冷却时间保持不变)

void TransitionToReady()
{
    cardState = CardState.Ready;  

    
    cardLight.SetActive(true);     
    cardGary.SetActive(false);       
    cardMask.gameObject.SetActive(false);
}

3.5 准备状态(等待点击状态)

因为在准备状态可能会因为种植别的植物而使得阳光不够,所以要先确定阳光是否充足,先将阳光不够时候的代码写出

void ReadyUpdate()
{
    if (needSunPoint > SunManager.Instance.SunPoint)
    {
        TransitionToWaitingSun();
    }
}

接下来给高亮时候添加一个button

image-20241022195054877

随后在card中添加一个方法用来处理自身被点击的事件,

public void Onclick()
 {

 }

并回到unity中将Card Template拖入到Select Object中

image-20241022195501219

随后点击右侧No Function选择Card中的Onclick

image-20241022195602677

随后回到代码中,因为在点击前我们可能会遇到阳光不够的情况,所以先做一个判断,如果阳光不够则直接返回,不做任何处理

public void Onclick()
 {
     if (needSunPoint > SunManager.Instance.SunPoint) return;
 }

再写一个转换为冷却时间的方法即Cooling方法

 void TransitionCooling()
 {
     cardState = CardState.Cooling;

     cdTimer = 0;
     cardLight.SetActive(false);
     cardGary.SetActive(true);
     cardMask.gameObject.SetActive(true);
 }

随后将此方法放入到点击事件中

public void Onclick()
{
    if (needSunPoint > SunManager.Instance.SunPoint) return;

    // TODO:消耗阳光值,并进行种植
    TransitionCooling();

}

随后回到unity中并将CardTemplate复制两个,并将其分别改名为CardSunFlower和CardPeaShooter

image-20241022200714884

随后选择CardLight,点击右侧的Source Image选择peashooter的图片,CardGary同理,CardMask不做改动

image-20241022200847143

3.6 开发阳光值的消耗和更新

在CardListUI中创建一个Text-TextMeshPro(之后改名为SunPointText)

image-20241022203642114

随后选中Text(TMP)并将其拖到适合位置并改变大小

image-20241022203729089

随后点击Text(TMP)改变其中字体大小并悬着上下左右居中

image-20241022203613698

将里面的内容改为300

image-20241022203808511

改变字体颜色为黑色,并改变字体

image-20241022203937359

呈现如下效果

image-20241022204009868

随后在SunManager代码中增加public TextMeshProUGUI sunPointText;代码,并在unity中进行手动拖拽将SunPointText拖进去

image-20241022204555821

随后在SunManager代码块中增加更新阳光的代码

public void UpdateSunPointText()
{
    sunPointText.text = sunPoint.ToString();
}

随后在上面编写一个阳光开始的代码,并将UpdateSunPointText()放入其中

private void Start()
{
    UpdateSunPointText();
}

编写一个阳光减少并更新数值的代码

public void SubSun(int point) 
{
    sunPoint -= point;
    UpdateSunPointText();
}

随后进入Cards中,在点击(Onclick)方法中增加方法

public void Onclick()
{
    if (needSunPoint > SunManager.Instance.SunPoint) return;

    // TODO:消耗阳光值,并进行种植
    SunManager.Instance.SubSun(needSunPoint);


    TransitionCooling();

}

3.7 创建第一个植物-向日葵

首先双击背景

image-20241023170506955

随后将中间的摄像头变小一点

image-20241023170539770

随后点开Images中的Plants,再点开SunFLower,就可以看到两种状态的向日葵图层

image-20241023172933819

选中所有的高亮向日葵并拖拽到左边Sense中

image-20241023173309494

先创建一个新的文件夹Animations,并修改文件名为Sunflower_Idle

image-20241023173423047

随后选中新出现的两个文件并放入到Animations中

image-20241023173544900

随后点击上面的Sunflower进行大小修改,这里大小合适,不做修改

image-20241023173721026

随后点击运行,这就就能看到向日葵的动态图

image-20241023173806797

注意:如果这里的向日葵会一闪一闪的,可以点击上面的Sunflower后在右侧的Additional Settings中将Order in Layer的0修改为1

image-20241023173853042

在Scripts中创建一个Plant(用于控制全部植物)的脚本

image-20241023175046763

将它拖入到Sunflower中

image-20241023175134741

3.8 关于植物不同状态的处理

在新建脚本Plant中创建枚举PlantState,里面只有两种状态Disable和Enable

enum PlantState
{
    Disable,
    Enable
}

在下面编写一个更新方法

private void Update()
{
    switch (plantState)
    {
        case PlantState.Disable:
            DisableUpdate();
            break;
        case PlantState.Enable:
            EnableUpdate();
            break;
        default:
            break;
    }
}

并编写Disable方法和Enable方法

void DisableUpdate()
{
    
}
void EnableUpdate()
{

}

3.9开发卡片点击后的植物生成和跟随

首先要先将Sunflower拖入到Prerabs

imgage

随后进入到Card脚本中,添加一个新的enum,以确认我们所种植的植物是什么类型

public enum PlantType
{
    Sunflower,
    PeaShooter
}

随后创建一个公开方法public PlantType plantType = PlantType.Sunflower;这里设置成public是为了方便通过Inspecter面板进行修改,随后回到unity中我们就可以通过右侧的Planttype来修改自己它的属性

image3.9.1

因为我们在种植的时候也需要知道植物的属性,所以回到Plant代码中,在代码中增加植物类型的方法

public PlantType plantType = PlantType.Sunflower;

种植的的时候,我们会先点击卡片,随后植物会随着我们的鼠标移动,直到我们将植物放到格子上点击后才算完成一次种植。在Manager再次创建一个HandManager

image3.9.2

先将Hand Mager拖入到Manager中,再打开HandManger后先将它设置成单例模式

public static HandManager instance { get; private set; }

随后在Awake中赋个值

private void Awake()
{
    instance = this;
}

因为我们在点击植物卡片时需要植物在鼠标上,所以我们在HandManager中新建一个方法,可以使得植物跟随鼠标进行移动

public void AddPlant(PlantType plantType)
{

}

随后将新方法添加到Card中

HandManager.Instance.AddPlant(plantType);

回到HandManager中,我们知道植物有很多,所以我们创建一个集合来保存植物的Prefabs

public List<Plant> plantPrefabsList;

随后我们回到unity中,会发现在Manager右侧的HandManager中多了一个Plant Prefabs List的栏位,随后将Sunflower拖入其中

Image3.9.3

因为在我们点击卡片的时候需要我们实例化出来一个植物,所以回到Hand Manager,通过植物的类型,将植物的实例化从植物列表中取出来,所以先写一个新的方法用来得到集合中的植物。

private Plant GetPlantPrefab(PlantType plantType)
{
    foreach (Plant plant in plantPrefabsList) 
    { 
        if(plant.plantType == plantType)
        {
            return plant;
        }
    }
    return null;
}

随后在AddPlant方法中添加代码

 public void AddPlant(PlantType plantType)
 {
     Plant plantPrefab = GetPlantPrefab(plantType);	// 得到plant
     if (plantPrefab == null) 		// 判断是否存在该植物
     {
         print("要种植的植物不存在");
     }
     GameObject.Instantiate(plantPrefab);	// 实例化植物
 }

实例化植物后需要引用,所以再写一个类用来代表当前要种植的植物:private Plant currentPlant;,并在Add Plant中修改:

public void AddPlant(PlantType plantType)
{
    Plant plantPrefab = GetPlantPrefab(plantType);
    if (plantPrefab == null) 
    {
        print("要种植的植物不存在");return;
    }
    currentPlant = GameObject.Instantiate(plantPrefab);
}

这样就拿到了一株植物。

然而此时回到unity中,我们运行程序并点击向日葵会发现生成了向日葵,只是两颗向日葵叠加在了一起。

Image3.9.4

而在点击豌豆射手时则会弹出“要种植的植物不存在”的字样

Image3.9.6

而我们需要的是植物跟随我们的鼠标,所以回到Hand Manager中,添加一个新方法,使得植物能够跟随鼠标进行移动

private void Update()
{
    FollowCursor();
}

然后我们添加一个新的方法FollowCursor

void FollowCursor()
{
    if (currentPlant == null) return;   // 先判断鼠标是否有拿取植物卡片

    Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);    // 将屏幕坐标转换为世界坐标
    currentPlant.transform.position = mouseWorldPosition; // 将鼠标坐标传递给植物

}

但在回到unity并运行后会发现植物确实生成了,但是没有显示,这时暂停一下运行并点击Sense,将2D图层转换为3D,我们就会发现植物确实是生成了,只不过不在同一个图层,看右边的坐标会发现生成的植物坐标z轴不为零。

Image3.9.7

回到Hand Manager中,将鼠标坐标的z轴设置为0

void FollowCursor()
{
    if (currentPlant == null) return;   // 先判断鼠标是否有拿取植物卡片

    Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);    // 将屏幕坐标转换为世界坐标
    mouseWorldPosition.z = 0;
    currentPlant.transform.position = mouseWorldPosition; // 将鼠标坐标传递给植物

}

随后回到unity中,再次运行,这次我们就可以看到植物随着鼠标在进行运动

image3.9.8