你的生命是人类的货币——用Unity实现《Seed》(三)

作者:Yumir
哈喽大家好我是Yumir。
不出意外这是本系列的最后一篇文章啦,感兴趣的同学可以从头开始看哦。传送门如下:
本次使用了状态模式实现了简单的AI,代码也比较多说的比较细,有条件的同学可以试着自己实现一发。
传送门:你的生命是人类的货币——用Unity实现《Seed》(一)
我们今天的目标是完成人类部分的功能,因为人类这边逻辑上都是一样的(只有贴图差别),所以只实现一个类型的人类。

我们需要实现的功能有:
1、人类出生之后会寻找树并砍树。
2、将树砍倒后,要有一个人把树送回人类的房子,其他人继续砍树。
3、将木材送到人类的房子,增加货币购买新的人类。
4、人会变老(顺便实现被植物攻击的方法)。
----------------------------开始准备预制体----------------------------------
首先我们来实现人类的预制体,每个类型的人类都是四个动画,动画的制作方法之前已经讲过:用Unity做一个萌萌哒游戏(附资源)

动画的状态机如图所示,再在人类身上添加一个控制人类行为的脚本,一个人类的预制体就制作完成啦,比树的预制体简单很多很多。

----------------------------预制体准备完毕----------------------------------
接下来是人的AI行为,对需求进行分析,首先我们需要实现一个让人类移向目标的方法,熟悉unity的同学此时就很开心了,这多简单啊,求出一个指向目标点的向量,让物体向着这个方向移动就可以了。确实如此,再复杂的建筑也是用建材和粘合剂组合起来的。
人物的移动方法很简单,但是还没到分享代码的时候,还记得文章开头我说了会用到状态模式么?
在看看我们的需求,人类有:找树、砍树、回家交货、死亡,这四个状态,实际上还有一个状态是原游戏的“原始人”特有的(原游戏的原始人没有工具可以砍树),也可以用于场景内没有树的时候,人类会随机走动。
当人类找树的时候,人类需要以树为目标寻路;
当人类在砍树的时候,人类不需要寻路;
当人类回家交货的时候,人类需要以“家”为目标寻路;
当人类死亡的时候,人类不会移动;
也就是说我们可以根据当前人类的状态设置目标点。
switch (humanState){
case HumanState.GoNone:
target.position = new Vector3(Random.Range(0, 100), Random.Range(0, 100), Random.Range(0, 100));
break;
case HumanState.GoHome:
target = GameManager.instance.humanHome.transform;
break;
case HumanState.GoTree:
target = GameManager.instance.GetHumanTargetTree(transform);
break;
case HumanState.Attack:
break;
case HumanState.Die:
target = transform;
break;}
那么场景中的树那么多,人类怎么知道自己要跑向哪颗树呢?

如果你刚刚认真看了上面的代码就会发现我写了一个方法,在“GoTree”状态下调用。
我在GameManager中声明了一个树的list,当树长出来的时候就会把自己加到这个list里面,当树的生命值小于等于零时将自己从该list删除并播放树死亡的动画,这个list就是下面的“TreeGOList”。这个方法返回了一颗离当前人类最近的树作为人类的目标。
public Transform GetHumanTargetTree(Transform humanTransform)
{
Transform targetTrans = humanTransform;
float direction = 2000;
foreach (GameObject item in TreeGOList)
{
if (Vector3.Distance(item.transform.position, humanTransform.position) < direction)
{
direction = Vector3.Distance(item.transform.position, humanTransform.position);
targetTrans = item.transform;
}
}
return targetTrans;
}
现在人类有了自己的目标,我们来想象一下:
游戏开始,人类出现开始找树,他跑到树的面前了,我们看到了,可是“人类”并不知道,他还在不停的寻路。
所以我们让他在距离目标点还有一段距离的时候停下来,进入砍树状态,但是目标点也不一定就是树啊,所以我们还要判断嘛!当人类往回走的时候需要转身,总不能退着回家嘛!所以我们需要通过比较目标点和人类自身的x轴大小改变人类的“flipX”的值。
人类可真是麻烦,但是没关系,这个麻烦我帮你们搞定了,来看代码。
curenPosition = this.transform.position;
moveDirection = target.position - curenPosition;
moveDirection.Normalize();
if (curenPosition.x > target.position.x)
{
GetComponent<SpriteRenderer>().flipX = true;
}
else if (curenPosition.x < target.position.x)
{
GetComponent<SpriteRenderer>().flipX = false;
}
if (Mathf.Abs(curenPosition.x - target.position.x) < 35f&& Mathf.Abs(curenPosition.y - target.position.y) < 15)
{
transform.position = new Vector3(transform.position.x, transform.position.y, target.position.z);
if (target.tag == "Tree" && humanState == HumanState.GoTree)
{
humanState = HumanState.Attack;
animator.SetBool("attack", true);
target.GetComponent<Tree>().AddAttacker(gameObject);
}
else if (target.tag == "Home" && humanState == HumanState.GoHome)
{
//把身上携带的木材交给管理器,然后把自身携带的木材清零
GameManager.instance.AddWood(wood);
wood = 0;
animator.SetBool("gohome", false);
humanState = HumanState.GoTree;
}
}
else
{
transform.Translate(moveDirection * speed * Time.deltaTime);
}
人类找到了目标之后还有什么操作呢,一个是砍树,另一个就是交货啦。
先说砍树,在上一篇文章里我们在树的脚本中写了被攻击方法,那么我们要怎么获取到他呢?
欸!你已经在上面的代码看到了!对的,我们已经知道目标是哪个树了,直接Get他!但是不是上面那句代码哦,是这段:
private void Attack()
{
target.GetComponent<Tree>().BeAttack(attack);
//音效
}
这句代码在哪里调用呢?答案是在帧事件调用哒!在人类的刀砍中树的那一帧加入帧事件,调用这个方法就可以啦~
那么之前提到的代码,为什么要在寻路碰到树的时候调用呢,这就要说到我们的第二个目标:将树砍倒后,要有一个人把树送回人类的房子,其他人继续砍树。

也就是我们需要让人知道自己的目标树已经去世这个消息,而且只有一个人可以获得这棵树的遗产,我们要怎么做到呢。
其实砍树的人群就是若干的“观察者”,而被砍的树就是“通知者”,这棵树在完全去世之前还要先向每个人类都说一句“嘿,我被你打败了啦,快去砍别的树吧”并且把自己的遗产交给其中一个人类,让他回家。
说得花里胡哨的,其实就是在每棵树的身上也有一个list,这个list是用来存储当前砍伐自己的人类的,在人类的脚本上写了人类的状态转换方法,当树死亡时通过foreach遍历调用就行了。人类开始砍树时加入该list,死亡时将自己从该list上删除。
if (attacker.Count != 0)
{
foreach (GameObject item in attacker)
{
item.GetComponent<Human>().GoTree();
}
attacker[0].GetComponent<Human>().GoHome(wood);
}
再来是回家交货,树通知了砍树的人并且将自己的遗产交给了其中一个人类,这个树的“wood”就赋值到了人类的“wood”,当人类到达根据地的时候就会将这个“wood”传递给管理器,这段代码在前面已经出现过了,那么问题是管理器中发生了什么。

游戏中的木材是“人类世界”的货币,可以用来购买新的人类,新的人类又会去砍树,反复循环。但是如果是交货的同时购买新的人类的话游戏会仿佛没有获得过货币,所以这里我用了一个延时将购买新的人类的操作延时触发。
public void AddWood(int num)
{
wood += num;
woodText.text = wood.ToString();
Invoke("BuyHuman", 0.5f);
}
每次购买的人类数量是所有货币的数量除以当前人类的价格的商(不同时代的人类价格可以改变),为了人类生成的时候不是堆在一起的,给他的坐标加上一个随机数。
public void BuyHuman()
{
if (wood / humanPrice != 0)
{
int num = wood / humanPrice;
for (int i = 0; i < num; i++)
{
Instantiate(humanPrefab, new Vector3(humanHome.transform.position.x + Random.Range(20, 60), humanHome.transform.position.y + Random.Range(20, 60), humanHome.transform.position.z), Quaternion.identity);
}
wood = wood % humanPrice;
woodText.text = wood.ToString();
}
}
这样一来我们的前三个目标都已经完成了,剩下最后一个目标:人类会死亡。

这是一个很简单的操作,回忆一下上一篇文章,有一种植物会让人类加速死亡,我们可以一起做了,我是这样实现的——增加一个老化速度,每次被大王花攻击的时候增加这个速度,这个速度的初始值为1。
private void HumanOld()
{
if (life > 0)
{
life -= Time.deltaTime*oldVelocity;
}
else if(humanState != HumanState.Die)
{
animator.SetTrigger("die");
humanState = HumanState.Die;
if(humanState == HumanState.Attack)
{
target.GetComponent<Tree>().RemoveAttacker(gameObject);
}
}
}
最后一个功能是对应了我上一篇做的植物预制体中的歌唱家,他的能力是概率使人类迷惑,也就是暂停人类的行动。这个功能可以通过“animator.speed”来实现,我用协程来控制这个迷惑的时间,并且改变了贴图的颜色,使效果更明显。

以上就是该游戏所有的功能实现啦,有了这些功能已经足够同学自主补充完整个游戏了,当然我们也不一定要完全把原来的游戏完完整整的做出来,学习嘛,学习的事情,也是要劳逸结合的。

——分割线——
欢迎加入游戏开发群欢乐搅基:869551769
有意向参与线下游戏开发学习的读者可戳这里进一步了解:http://levelpp.com/