植物大战僵尸的制作(不定时更新)
植物大战僵尸的制作
[点击直达github](1zero0/PlantVSZombies: 学习制作植物大战僵尸并熟悉Unity)
1.创建项目
点击右上角新项目
选择2D(built-In Render Pipeline)(红色箭头),修改自己项目的名字(蓝色箭头),选择自己想要的地址(绿色箭头)
2.项目
将资源包(红色箭头)拖入到Assets(蓝色箭头)中并等待一段时间。
2.1 导入素材
如下图中,Audio就是音效Images就是相关的所有图片,其中Music中包含的就是背景音乐,随后点击右下角Import
导入后就会在这里生成一个Audio文件夹(红色箭头)和Images文件夹(蓝色箭头)
随后点开Audio中的Music和Sound文件夹,会看到导入的音频,随便选一个,然后在右边可以点击播放(红色箭头)
注:如果右边没有如下图的ch 1和ch 2可以点击AssetBundle上的bgm就会弹出如图所示的样子
3.项目制作
3.1 导入背景
再Background中找到这个白天场景(红色箭头)
Pixels Per Unit指的是图片导入到unity中的一个单位转换,60像素转换为一米。(再unity中一个格子代表一米)
随后将场景拖入到左侧的Scene中
随后点击左上角Scene右侧的Game(游戏视角),并选择Full HD(1920x1080)
随后可以通过右边的X和Y进行场景的位置改动
随后导入卡片,以便于种植,植物再Card中
因为植物卡片需要卡片槽(用于放置植物卡片),这里先找到卡片槽,在UI中
- 随后在上面Hierarchy中右键
- 选择UI
- 选择Image
- (先双击Canvas聚焦)先点击Image
- 选择右侧Image中的Source Image,
在弹出的窗口中选择卡片槽
随后点击Image中的Set Native Size
随后将卡片槽拖拽到左上角的位置
然后通过Game来确认它的最终位置,还是通过右侧Pos X和Pox Y进行左右和上下的移动
- 选择定位
- 选择左上角
3.2 植物卡片
先像上面一样创建一个Image
随后将新建的图层拉到卡片槽的位置,并找到向日葵(可种植状态)
随后复制一个图层并选择灰色的向日葵(不可种植状态)
随后再次复制一个图层,并选择黑色图层(冷却状态中)
- 选择Color
- 选择透明度
随后在Image Type中选择Simple
随后在选择Fill Method中的Vertical(上下)(Horizontal:水平,Radial 360:圆圈型360度,其余两个同理)
随后选择Fill Origin中将Bottom(从下往上)改为Top(从上往下)
随后将这三张图片分到一个组中
并将其命名为CardTemplate,并将下面的卡片分别命名为CardLight(可种植),CardGray(不可种植),CardMask(冷却中)
随后我们要创建一个文件夹来存放这些卡片,即将CardTemplate文件拖入到我们新建的Prefabs中
随后将卡槽和植物卡片共同组建一个父类(也可以将植物卡片放到卡槽下),并起名为CardListUI
这里要记得将CardListUI也定位于左上角
3.3 为植物卡片添加脚本使之达到各种状态
先创建一个新的文件夹Scripts用于存放脚本
- 点击CardTemplate
- 点击Add Component
- 输入Card,回车(两次,随后等待创建好)
随后将新建的Card脚本拖入到Scripts中
随后双击点开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,将三种状态拖入到对应的地方(如图中红色箭头)
3.4 阳光的计数
因为植物的种植需要阳光,所以先创建一个Empty,并改名为Manager
随后点击右侧Add Component然后添加一个Sun Manager脚本,并双击点开
随后先将SunManager拖入到Scripts中,随后新建文件夹Manager,并将SunManager拖入其中
随后编写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
随后在card中添加一个方法用来处理自身被点击的事件,
public void Onclick()
{
}
并回到unity中将Card Template拖入到Select Object中
随后点击右侧No Function选择Card中的Onclick
随后回到代码中,因为在点击前我们可能会遇到阳光不够的情况,所以先做一个判断,如果阳光不够则直接返回,不做任何处理
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
随后选择CardLight,点击右侧的Source Image选择peashooter的图片,CardGary同理,CardMask不做改动
3.6 开发阳光值的消耗和更新
在CardListUI中创建一个Text-TextMeshPro(之后改名为SunPointText)
随后选中Text(TMP)并将其拖到适合位置并改变大小
随后点击Text(TMP)改变其中字体大小并悬着上下左右居中
将里面的内容改为300
改变字体颜色为黑色,并改变字体
呈现如下效果
随后在SunManager代码中增加public TextMeshProUGUI sunPointText;
代码,并在unity中进行手动拖拽将SunPointText拖进去
随后在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 创建第一个植物-向日葵
首先双击背景
随后将中间的摄像头变小一点
随后点开Images中的Plants,再点开SunFLower,就可以看到两种状态的向日葵图层
选中所有的高亮向日葵并拖拽到左边Sense中
先创建一个新的文件夹Animations,并修改文件名为Sunflower_Idle
随后选中新出现的两个文件并放入到Animations中
随后点击上面的Sunflower进行大小修改,这里大小合适,不做修改
随后点击运行,这就就能看到向日葵的动态图
注意:如果这里的向日葵会一闪一闪的,可以点击上面的Sunflower后在右侧的Additional Settings中将Order in Layer的0修改为1
在Scripts中创建一个Plant(用于控制全部植物)的脚本
将它拖入到Sunflower中
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
随后进入到Card脚本中,添加一个新的enum,以确认我们所种植的植物是什么类型
public enum PlantType
{
Sunflower,
PeaShooter
}
随后创建一个公开方法public PlantType plantType = PlantType.Sunflower;
这里设置成public是为了方便通过Inspecter面板进行修改,随后回到unity中我们就可以通过右侧的Planttype来修改自己它的属性
因为我们在种植的时候也需要知道植物的属性,所以回到Plant代码中,在代码中增加植物类型的方法
public PlantType plantType = PlantType.Sunflower;
种植的的时候,我们会先点击卡片,随后植物会随着我们的鼠标移动,直到我们将植物放到格子上点击后才算完成一次种植。在Manager再次创建一个HandManager
先将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拖入其中
因为在我们点击卡片的时候需要我们实例化出来一个植物,所以回到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中,我们运行程序并点击向日葵会发现生成了向日葵,只是两颗向日葵叠加在了一起。
而在点击豌豆射手时则会弹出“要种植的植物不存在”的字样
而我们需要的是植物跟随我们的鼠标,所以回到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轴不为零。
回到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中,再次运行,这次我们就可以看到植物随着鼠标在进行运动