我们是第一次接触unity,也是第一次尝试制作一个比较大的游戏项目,所以游戏制作初期我跟随教程进行了尝试,Unity2D植物大战僵尸PVZ_Unity实战课程(合P版)_哔哩哔哩_bilibili是我们学习的教程,在此阶段我对于游戏开发有了一定的了解。
首先关于我们的专业想要从零开始制作是不可能的,所以游戏使用的素材只能使用从网上找,那么混合使用不同素材的结果就是成品会显得有些违和。
其次,关于直接上手unity,我的最大感想是很方便,这个游戏引擎可以看作是一个低代码开发平台,关于物理效果的设置及调整都可以通过可视化界面来操作,所以我们要做的就是搭建场景以及通过c#脚本来实现一些具体的需求。
接下来是游戏开发过程:
首先是我们每个人都要跟随教程学习的内容
第一步,导入素材,生成预制体,例如,阳光、植物、僵尸,等等。这些预制体有一定的动画效果。
第二步,场景搭建,对于2D游戏的开发,最大的好处是只有两个维度,那么背景就是一张贴图,摆放好位置之后在背景上添加一些组件,例如图像、动画预制体等等,效果如下:
第三步,给游戏物体添加脚本。例如在游戏中阳光会从天上落下,还会从太阳花中产出,无论那种情况,都要给它限制在一定区域后再进行移动,还有点击的时候要让其飞到计数器的位置再消失,所以要给它添加一个碰撞。那么我们就需要写一个脚本来管理阳光,并将其添加给游戏管理器。而关于游戏UI也要单独添加一个脚本来使其可以被操作。所以,我们要做的就是通过脚本实现不同的功能。
其次是我自己制作的内容:
我负责的是花园的制作,包括植物的种植、施肥浇水、金币收获等;
因为教程未提供完整的素材,所以大部分素材都是网上找到的,场景搭建初步完成后效果如下:
通过网格对花盆进行布局并存储相应的信息,即花盆并无任何实际操作。
网格脚本(功能等同于花盆):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Gird
{
public static Gird Instance;
public Vector2 point;//坐标点
public Vector2 position;//世界坐标
public bool havePlant;//是否有植物
public plantBase curPlantBase;//当前植物
public bool haveReq;//当前是否有需求
public int curReq;//当前需求
public requirement req1;//需求的预制体1肥料
public requirement req2;//需求的预制体2水
public Gird(Vector2 point, Vector2 position, bool havePlant)
{
this.point = point;
this.position = position;
this.havePlant = havePlant;
}
private void Awake()
{
Instance = this;
curReq = Random.Range(0, 2);
}
void Update()
{
}
public plantBase CurPlantBase { get => curPlantBase;
set
{
curPlantBase = value;
if (curPlantBase == null)
{
havePlant = false;
}
else
{
havePlant = true;
}
}
}
private int waterTime = 3;
private int feTime = 3;
//当次数减少时销毁原有需求,建立新需求,并产出金币,当所有需求满足后认为植物成熟,销毁植物(卖出)
public int WaterTime
{
get => waterTime;
set
{
waterTime = value;
if (waterTime >= 0)
{
curReq = Random.Range(0, 2);//0是请求肥料;1是请求水
req2.delereq();
haveReq = false;
Gold gold = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.gold, position, Quaternion.identity, FlowerPotManager.Instance.transform).GetComponent<Gold>();
gold.jump();
}
else
{
curReq = 0;
req2.delereq();
Gold gold = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.gold, position, Quaternion.identity, FlowerPotManager.Instance.transform).GetComponent<Gold>();
gold.jump();
if (waterTime < 0 && feTime < 0)
{
haveReq = false;
curPlantBase.delePlant();
CurPlantBase = null;
waterTime = 3;
feTime = 3;
FlowerPotManager.Instance.GoldNum += 100;
}
else
{
haveReq = false;
}
}
}
}
//同上
public int FeTime
{
get => feTime;
set
{
feTime = value;
if (feTime >= 0)
{
curReq = Random.Range(0, 2);//0是请求肥料;1是请求水
req1.delereq();
haveReq = false;
Gold gold = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.gold, position, Quaternion.identity, FlowerPotManager.Instance.transform).GetComponent<Gold>();
gold.jump();
}
else
{
curReq = 1;
req1.delereq();
Gold gold = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.gold, position, Quaternion.identity, FlowerPotManager.Instance.transform).GetComponent<Gold>();
gold.jump();
if (waterTime < 0 && feTime < 0)
{
haveReq = false;
curPlantBase.delePlant();
CurPlantBase = null;
waterTime = 3;
feTime = 3;
FlowerPotManager.Instance.GoldNum += 100;
}
else
{
haveReq = false;
}
}
}
}
}
花盆管理器:因为网格位置不是规律分布的,所以只能分块处理
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class FlowerPotManager : MonoBehaviour
{
public static FlowerPotManager Instance;
public GameObject fp;//花盆预制体
public Text GoldSumtext;//金币数量
private int goldNum;
public int GoldNum
{
get => goldNum;
set
{
goldNum = value;
UpSunNum(goldNum);
}
}
public List<Gird> pointlist = new List<Gird>();
private void Awake()
{
Instance = this;
GoldNum = 0;
}
// Start is called before the first frame update
void Start()
{
initPot();
InvokeRepeating("RandomCreatReq", 1,1);
InvokeRepeating("RandomCreateGold", 3,15);
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector2 vc = Camera.main.ScreenToWorldPoint(Input.mousePosition);
//Debug.Log(getGirdByMouse(vc).position);
}
}
//初始化花盆及其位置
void initPot()
{
for(int i = 0; i < 8; i++)
{
Vector2 start = new Vector2(-8.44f, 3.07f);
GameObject.Instantiate<GameObject>(fp, start-new Vector2(-2.45f * i,0), Quaternion.identity, transform);
pointlist.Add(new Gird(new Vector2(0, i), start - new Vector2(-2.45f * i, 0), false));
}
for (int i = 0; i < 8; i++)
{
if (i < 4)
{
Vector2 start = new Vector2(-8.84f, 3.07f);
GameObject.Instantiate<GameObject>(fp, start - new Vector2(-2.45f * i, 2.38f), Quaternion.identity, transform);
pointlist.Add(new Gird(new Vector2(1, i), start - new Vector2(-2.45f * i, 2.38f), false));
}
else
{
Vector2 start = new Vector2(-8.24f, 3.07f);
GameObject.Instantiate<GameObject>(fp, start - new Vector2(-2.45f * i, 2.38f), Quaternion.identity, transform);
pointlist.Add(new Gird(new Vector2(1, i), start - new Vector2(-2.45f * i, 2.38f), false));
}
}
for (int i = 0; i < 8; i++)
{
if (i < 4)
{
Vector2 start = new Vector2(-9.44f, 3.07f);
GameObject.Instantiate<GameObject>(fp, start - new Vector2(-2.45f * i, 2.38f * 2), Quaternion.identity, transform);
pointlist.Add(new Gird(new Vector2(2, i), start - new Vector2(-2.45f * i, 2.38f*2), false));
}
else
{
Vector2 start = new Vector2(-8.0f, 3.07f);
GameObject.Instantiate<GameObject>(fp, start - new Vector2(-2.45f * i, 2.38f * 2), Quaternion.identity, transform);
pointlist.Add(new Gird(new Vector2(2, i), start - new Vector2(-2.45f * i, 2.38f * 2), false));
}
}
for (int i = 0; i < 8; i++)
{
if (i < 4)
{
Vector2 start = new Vector2(-9.44f, 3.07f);
GameObject.Instantiate<GameObject>(fp, start - new Vector2(-2.45f * i, 2.38f * 3), Quaternion.identity, transform);
pointlist.Add(new Gird(new Vector2(3, i), start - new Vector2(-2.45f * i, 2.38f * 3), false));
}
else
{
Vector2 start = new Vector2(-8.0f, 3.07f);
GameObject.Instantiate<GameObject>(fp, start - new Vector2(-2.45f * i, 2.38f * 3), Quaternion.identity, transform);
pointlist.Add(new Gird(new Vector2(3, i), start - new Vector2(-2.45f * i, 2.38f * 3), false));
}
}
}
//通过鼠标点击位置获取最近的坐标点
public Vector2 getPointByMouse()
{
float dis = 1000;
Vector2 point = new Vector2();
for (int i = 0; i < pointlist.Count; i++)
{
if (Vector2.Distance(Camera.main.ScreenToWorldPoint(Input.mousePosition), pointlist[i].position) < dis)
{
dis = Vector2.Distance(Camera.main.ScreenToWorldPoint(Input.mousePosition), pointlist[i].position);
point = pointlist[i].position;
}
}
return point;
}
//通过鼠标点击位置获取最近的网格
public Gird getGirdByMouse(Vector2 vec)
{
float dis = 1000;
Gird gird = null;
for (int i = 0; i < pointlist.Count; i++)
{
if (Vector2.Distance(vec, pointlist[i].position) < dis)
{
dis = Vector2.Distance(vec, pointlist[i].position);
gird = pointlist[i];
}
}
return gird;
}
//随机时间后创建植物需求
private void RandomCreatReq()
{
int n = Random.Range(0, 32);
Gird g = pointlist[n];
if (g.CurPlantBase != null && g.haveReq == false)
{
g.haveReq = true;
creatReq(g);
}
}
//随机时间后创建金币
private void RandomCreateGold()
{
int n = Random.Range(0, 32);
Gird g = pointlist[n];
if (g.CurPlantBase != null)
{
Gold gold = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.gold,g.position, Quaternion.identity, transform).GetComponent<Gold>();
gold.jump();
}
}
//创建植物需求
private void creatReq(Gird gird)
{
/*int m = Random.Range(0, 2);
if (gird.WaterTime > 0&&gird.FeTime>0)
{
gird.curReq = m;
}else if (gird.WaterTime > 0 && gird.FeTime < 0)
{
gird.curReq = 1;
}
else if (gird.WaterTime < 0 && gird.FeTime > 0)
{
gird.curReq = 0;
}*/
switch (gird.curReq)
{
case 0:
gird.req1 = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.reqfer, gird.position, Quaternion.identity, transform).GetComponent<requirement>();
gird.req1.jump();
break;
case 1:
gird.req2 = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.reqwate, gird.position, Quaternion.identity, transform).GetComponent<requirement>();
gird.req2.jump();
break;
default:
gird.req1 = GameObject.Instantiate<GameObject>(GameManager.Instance.GameConf.reqwate, gird.position, Quaternion.identity, transform).GetComponent<requirement>();
gird.req1.jump();
break;
}
}
//更新金币数量
public void UpSunNum(int num)
{
GoldSumtext.text = num.ToString();
}
}
然后是植物的种植,因为是非关卡场景所以植物的作用只有需求水和肥料并产出金币,对此我简单处理了一下,植物的需求其实也可以认为是花盆(网格)的需求,同样也可以是花盆的需求满足后产出金币,那么这些行为都交由网格管理即可,也便于我管理这些操作。对于需求的创建仅仅是制作了一段动画。
public void jump()
{
StartCoroutine(Dojump());
}
private IEnumerator Dojump()
{
bool isleft = UnityEngine.Random.Range(0, 2) == 0;
transform.localScale = new Vector3(0.5f,0.5f,1);
Vector3 nowPos = transform.position;
if (isleft)
{
while (transform.position.y <= nowPos.y + 1)
{
yield return new WaitForSeconds(0.005f);
transform.Translate(new Vector3(-0.01f, 0.05f, 0));
}
}
else
{
while (transform.position.y <= nowPos.y + 1)
{
yield return new WaitForSeconds(0.005f);
transform.Translate(new Vector3(0.01f, 0.05f, 0));
}
}
}
对于肥料和浇水壶,可将其视为具有不同反馈的同一工具,当对其进行点击后会跟随鼠标移动,当花盆上有植物且该植物有需求时会在该花盆的位置产生工具的虚影表示可以使用该工具,点击即可执行对应操作,如果植物的需求得到满足后销毁需求并产生一枚金币,金币产生动画与需求相同点击金币会将其收集;经过若干次浇水施肥后,植物成熟可以卖出,但是由于没有相应的动画素材,所以它会直接消失。
void Update()
{
if (NedPlace && plant != null)
{
Vector3 pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
plant.transform.position = new Vector3(pos.x, pos.y, 0);
Gird gird = FlowerPotManager.Instance.getGirdByMouse(pos);
if (gird.haveReq && Vector2.Distance(pos, gird.position) < 1)
{
if (plantGird == null)
{
plantGird = GameObject.Instantiate<GameObject>(plant.gameObject, gird.position, Quaternion.identity, CardManager.Instance.transform).GetComponent<Tool>();
}
else
{
plantGird.transform.position = gird.position;
}
//左键点击进入施肥状态
if (Input.GetMouseButtonDown(0))
{
if (gird.curReq == 0 && CardType == GardenCardType.Fertilizer)
{
gird.FeTime--;
}
if (gird.curReq == 1 && CardType == GardenCardType.water)
{
gird.WaterTime --;
}
if(plantGird!=null) Destroy(plantGird.gameObject);
plantGird = null;
//播放一段动画后
NedPlace = false;
}
}
else
{
if (plantGird != null)
{
if (plantGird != null) Destroy(plantGird.gameObject);
plantGird = null;
}
if (Input.GetMouseButtonDown(0))
{
if (plant != null) Destroy(plant.gameObject);
plant = null;
NedPlace = false;
}
}
}
//右键点击取消放置
if (Input.GetMouseButtonDown(1))
{
if (plant != null) Destroy(plant.gameObject);
plant = null;
NedPlace = false;
if (plantGird != null) Destroy(plantGird.gameObject);
plantGird = null;
}
}
然后是蜗牛可以在场景中循环移动自动收集金币,实现上通过给蜗牛和金币添加碰撞器和刚体来检测碰撞进行收集。unity关于碰撞的要求是两个物体必须均为碰撞体且其中一个物体同时为刚体,所以我设置蜗牛为刚体,使其在整个地图循环移动。为了移动平滑,降低了函数执行间隔。
void Start()
{
InvokeRepeating("move", 1, 0.01f);
}
最后就是植物种子的添加,这个需要在关卡中击杀僵尸来获取植物种子并将其添加到花园的背包中,种植操作基本与关卡中一致。目前设置为一个按钮,点击即可在物品栏中增加一个种子,可以通过点击时的卡片类型设置种子类型。