教程效果演示
最近摩尔庄园这个游戏很火啊,在里面种种菜,开开荒还是不错的。
不过种田虽爽,也还会思考如果是要你实现这些功能,你会怎么做呢?稍微思考了一下,其实里面的功能也没那么复杂,今天就以开垦、播种、洒水和收获这四个功能为目标,进行一些简单的实现。
首先新建一个unity项目,随便写一个名字
我们先看一下摩尔庄园里面的按钮布局,下方是工具或者物品选项栏,右边是选项操作按钮,选中某一个工具或物品,再点击按钮就可以使用该工具。
在此之前我们先导入四个unity免费资源包,在商店可以找到
在unity里面新建一个场景,创建一个Plane地面,更改名字为GroundPlane,删除原有的Mesh Collider,添加Box Collider。
找到导入的MaleFreeSimpleMovement1预制体,拖入场景中,作为我们要控制的人物,此时运行,可以控制人物移动,但是摄像机不会跟着动,所以我们还需要对摄像机进行设计跟随
新建一个脚本CameraFollow,挂在摄像机上
脚本代码如下吗(注意脚本挂上去之后,要添加MaleFreeSimpleMovement1为FollowTarget):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CamFollow : MonoBehaviour
{
//要跟随的目标
public Transform FollowTarget;
//相对位置
private Vector3 relPos;
void Start()
{
//初始化相对位置
if (FollowTarget != null)
{
relPos = transform.position - FollowTarget.position;
}
}
// Update is called once per frame
void Update()
{
//根据相对位置赋值
if (FollowTarget != null)
{
transform.position = relPos + FollowTarget.position;
}
}
}
做完上面这一步,我们开始设计界面的UI,如下图所示是我已经设计好的整体,GamePanel是一个Panel面板,里面有两个组件,一个是Scroll View,一个是ActionBtn。ScrollView主要负责下方的各种工具和物品呈现,ActionBtn是Button组件,负责处理物品或者工具的点击事件。而MessInfoPanel主要是打印一些操作信息。
打开ScrollView,将ScrollView的锚点设置在下方
给Content添加Horizontal Layout Group和Toggle Group组件,主要是控制里面物品的布局,设置如下
接下来就开始设计Content下面的物体,这里面的物体主要是使用Toggle来实现点击切换,如下图所示为具体的结构,新建一个Toggle然后去修改,在Background下面新建一个Image,改名为toolsprite,toolsprite的图片设置为工具或者物品的图片。
修改Background的图片,作为物品背景框
修改Checkmark的颜色和透明度
完成一个修改之后,就可以进行Ctrl+D复制了,千万不要一个一个制作。ActionBtn只要进行图片样式修改就可以,改成颜色为白色的圆形图案,并且把Text删掉,新建一个Image在下面,Image设置图片为工具,这里可以随便先放一张图片。就是一个是背景,一个是工具图。
接下来,我们开始进行功能的实现,首先为了实现一块一块开荒的功能,我们需要预先初始化土地块,开荒的时候只要点击了开荒,土地块就出现,所以就需要生成一大片的土地块。新建一个box,调整y轴的缩放,添加一个黄色土地的材质球,起名为Ground。为其添加一个脚本Ground,然后拖回Assets文件夹作为预制体。
Ground脚本里面的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum FamVege
{
None,
白菜,
胡萝卜
}
public class Ground : MonoBehaviour
{
/// <summary>
/// 是否已经开垦
/// </summary>
public bool HasOpened;
/// <summary>
/// 是否种植了作物
/// </summary>
public bool HasFamed;
/// <summary>
/// 当前这块地种植的作物
/// </summary>
public ToolAndThingsType CurrentFamVege;
void Start()
{
}
void Update()
{
}
/// <summary>
/// 开垦土地
/// </summary>
public void OpenGround()
{
GetComponent<MeshRenderer>().enabled = true;
HasOpened = true;
}
/// <summary>
/// 种植作物
/// </summary>
public void PlantVege(ToolAndThingsType toolAndThings)
{
CurrentFamVege = toolAndThings;
HasFamed = true;
}
}
场景中新建一个空物体,起名为CreatGroundMan,新建一个CreatGroundMan脚本添加上去,脚本代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CreateGroundMan : MonoBehaviour
{
public GameObject groundPrefab;
public Transform groundplane;
public List<Ground> grounds = new List<Ground>();
void Start()
{
InitGround();
}
// Update is called once per frame
void Update()
{
}
/// <summary>
/// 初始化土地
/// </summary>
private void InitGround()
{
//计算生成的高度,贴合地面即可
float heightY = groundPrefab.transform.localScale.y / 2+groundplane.position.y;
for (float i =0; i < 50; i+=1.2f)
{
for (float j = 0; j < 50; j+=1.2f)
{
Vector3 pos = new Vector3(i, heightY, j);
GameObject groundObj = Instantiate(groundPrefab, pos, Quaternion.identity);
groundObj.transform.SetParent(transform);
grounds.Add(groundObj.GetComponent<Ground>());
}
}
}
}
生成完成了,下一步是实现侦测附近土地块的功能,实现起来主要会使用到Physics.OverlapSphere这个unity的API,即球形射线检测功能,以个位置为中心点,某一个半径为大小检测碰到的物体,返回一个Collider数组。在MaleFreeSimpleMovement1下方新建一个球体,缩小调整位置,删除Sphere Collider组件,修改名字为OverLapSphere,新建一个脚本名为ObjectDetect为其添加,脚本完整内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectDetect : MonoBehaviour
{
public static ObjectDetect _instance;
private void Awake()
{
_instance = this;
}
void Start()
{
}
// Update is called once per frame
void Update()
{
}
/// <summary>
/// 获取到附近未开垦土地
/// </summary>
/// <returns></returns>
public Ground GetNearByUnOpenGround()
{
//第一个参数是中心点,第二个参数是半径,第三个参数是层级
Collider[] cols = Physics.OverlapSphere(transform.position, 3f, 1 << LayerMask.NameToLayer("ground"));
if (cols.Length > 0)
{
foreach (Collider col in cols)
{
Ground g = col.transform.GetComponent<Ground>();
if (g != null)
{
if (!g.HasOpened)
{
MessInfoPanel._instance.ShowMess("获取到土地信息");
Debug.Log(col);
return g;
}
}
}
}
return null;
}
/// <summary>
/// 获取到已开垦,但是未种植的土地信息
/// </summary>
/// <returns></returns>
public Ground GetNearByUnPlantGround()
{
Collider[] cols = Physics.OverlapSphere(transform.position, 3f, 1 << LayerMask.NameToLayer("ground"));
if (cols.Length > 0)
{
foreach (Collider col in cols)
{
Ground g = col.transform.GetComponent<Ground>();
if (g != null)
{
if (g.HasOpened&&(!g.HasFamed))
{
MessInfoPanel._instance.ShowMess("获取到土地信息");
Debug.Log(col);
return g;
}
}
}
}
return null;
}
/// <summary>
/// 获取到需要浇水的作物
/// </summary>
/// <returns></returns>
public Vegetable GetNearByNeedWaterVege()
{
Collider[] cols = Physics.OverlapSphere(transform.position, 3f, 1 << LayerMask.NameToLayer("vege"));
if (cols.Length > 0)
{
foreach (Collider col in cols)
{
Vegetable g = col.transform.GetComponent<Vegetable>();
if (g != null)
{
if (g.needWater)
{
//MessInfoPanel._instance.ShowMess("获取到土地信息");
Debug.Log(col);
return g;
}
}
}
}
return null;
}
/// <summary>
/// 获取到可以采摘的蔬菜信息
/// </summary>
/// <returns></returns>
public Vegetable GetNearByMatureVege()
{
Collider[] cols = Physics.OverlapSphere(transform.position, 3f, 1 << LayerMask.NameToLayer("vege"));
if (cols.Length > 0)
{
foreach (Collider col in cols)
{
Vegetable g = col.transform.GetComponent<Vegetable>();
if (g != null)
{
if (g.IsMatured)
{
Debug.Log(col);
return g;
}
}
}
}
return null;
}
private void OnDrawGizmos()
{
Gizmos.DrawWireSphere(this.transform.position, 3);
}
}
在上一步中,我们会用到两个层级,所以需要在Layers里面添加两个层级
下一步我们制作农作物的预制体,如下图所示找到两种萝卜已做好的预制体,拖入场景中取消预制体连接。然后拖入我们自己的存放预制体的文件夹。
新建一个脚本为Vegetable,添加给上一步的农作物预制体,完成代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Vegetable : MonoBehaviour
{
/// <summary>
/// 是否需要洒水
/// </summary>
public bool needWater;
/// <summary>
/// 是否成熟
/// </summary>
public bool IsMatured;
/// <summary>
/// 需要的成熟时间
/// </summary>
public float NeedMatureTime;
public GameObject waterPrefab;
public GameObject lanziPrefab;
private GameObject c_water;
private GameObject c_lanzi;
void Start()
{
///随机一个成熟时间
NeedMatureTime = Random.Range(10, 20);
needWater = true;
///初始化水滴和篮子
Vector3 pos = new Vector3(transform.position.x, transform.position.y + 0.9f, transform.position.z);
c_water = GameObject.Instantiate(waterPrefab, pos, Quaternion.identity);
c_lanzi = GameObject.Instantiate(lanziPrefab, pos, Quaternion.identity);
c_water.transform.SetParent(transform);
c_lanzi.transform.SetParent(transform);
}
/// <summary>
/// 成熟的函数
/// </summary>
public void MatureEvent()
{
if (!needWater)
{
if (!IsMatured)
{
NeedMatureTime -= Time.deltaTime;
if (NeedMatureTime <= 0)
{
IsMatured = true;
}
}
}
if (needWater)
{
c_water.SetActive(true);
}
else
{
c_water.SetActive(false);
}
if (IsMatured)
{
c_lanzi.SetActive(true);
}
else
{
c_lanzi.SetActive(false);
}
}
void Update()
{
MatureEvent();
}
/// <summary>
/// 进行浇水
/// </summary>
public void WaterV()
{
needWater = false;
MessInfoPanel._instance.ShowMess("浇水成功!!!");
}
public void PickVege()
{
MessInfoPanel._instance.ShowMess("采摘成功!!!");
Ground g=transform.parent.GetComponent<Ground>();
if (g != null)
{
g.HasFamed = false;
}
Destroy(gameObject, 1.0f);
}
}
农作物身上的脚本Vegetable主要负责控制作物的成熟、洒水和收获的功能。在摩尔庄园手游里面我们看到当需要洒水的时候,农作物上方会有一个水滴,当成熟了要采摘的时候,农作物上方会有一个篮子或者是其他标志,所以我们在脚本里面定义了waterPrefab和lanziPrefab两个预制体,这两个预制体也需要进行预先设计,然后放入蔬菜预制体的Vegetable参数里面。
水滴预制体:
篮子预制体(随便做的椭圆代替):
设置蔬菜预制体参数:
新建一个脚本GamePanel,作为存储数据和部分UI控制的功能,以下是完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//工具物品类型枚举
public enum ToolAndThingsType
{
None,
PA,
SHUIHU,
LANZI,
白萝卜,
小萝卜
}
/// <summary>
/// 工具枚举类型对应的图片
/// </summary>
[System.Serializable]
public class ToolThingTexture
{
public Sprite sprite;
public ToolAndThingsType type;
}
public class GamePanel : MonoBehaviour
{
/// <summary>
/// 当前选中的是哪个
/// </summary>
public ToolAndThingsType currenttoolandthings;
/// <summary>
/// 按钮
/// </summary>
public ActionBtn actionBtn;
/// <summary>
/// 预先要设置的链表
/// </summary>
public List<ToolThingTexture> toolThingTextures = new List<ToolThingTexture>();
/// <summary>
/// 初始化成字典
/// </summary>
public Dictionary<ToolAndThingsType, Sprite> toolthingsSprite = new Dictionary<ToolAndThingsType, Sprite>();
/// <summary>
/// 单例模式
/// </summary>
public static GamePanel _instance;
private void Awake()
{
_instance=this;
}
void Start()
{
InitGameData();
}
// Update is called once per frame
void Update()
{
}
/// <summary>
/// 初始化参数
/// </summary>
public void InitGameData()
{
foreach (var ttt in toolThingTextures)
{
toolthingsSprite.Add(ttt.type, ttt.sprite);
}
}
/// <summary>
/// 根据选择的工具或者物品,更新操作按钮的行为
/// </summary>
public void UpdateActionBtn(ToolAndThingsType toolAndThingsType)
{
currenttoolandthings = toolAndThingsType;
actionBtn.CurrenttoolthingsType = currenttoolandthings;
transform.Find("ActionBtn/Image").GetComponent<Image>().sprite = toolthingsSprite[currenttoolandthings];
}
}
在完成上面代码之后,记得要进行参数的设置,特别是类型对应图片的设置
新建一个名为ToolsAndThings的脚本,放置在以下物品中,用来区分你当前选择的工具或者物品是什么。
其他几个同理设置,完整的代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ToolsAndThings : MonoBehaviour
{
/// <summary>
/// 当前的类型
/// </summary>
public ToolAndThingsType toolthingsType;
/// <summary>
/// 是否开启
/// </summary>
public bool Ison;
void Start()
{
}
// Update is called once per frame
void Update()
{
}
/// <summary>
/// 绑定的动态函数
/// </summary>
/// <param name="ison"></param>
public void ToggleValueChange(bool ison)
{
Ison = ison;
GamePanel._instance.UpdateActionBtn(toolthingsType);
}
}
然后回到ActionBtn,为其添加一个脚本,起名为ActionBtn,主要负责处理事件,完整代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[System.Serializable]
/// <summary>
/// 蔬菜类型对应的游戏物体
/// </summary>
public struct VegetablePrefab
{
public ToolAndThingsType panletype;
public GameObject plantObj;
}
public class ActionBtn : MonoBehaviour
{
/// <summary>
/// 当前的工具或者选择类型
/// </summary>
public ToolAndThingsType CurrenttoolthingsType;
public List<VegetablePrefab> vegetablePrefabs = new List<VegetablePrefab>();
public Dictionary<ToolAndThingsType, GameObject> toolandObjects = new Dictionary<ToolAndThingsType, GameObject>();
private Button actionBtn;
void Start()
{
actionBtn = GetComponent<Button>();
actionBtn.onClick.AddListener(ActionButtonClick);
foreach (var vp in vegetablePrefabs)
{
toolandObjects.Add(vp.panletype, vp.plantObj);
}
}
// Update is called once per frame
void Update()
{
}
/// <summary>
/// 按钮事件
/// </summary>
private void ActionButtonClick()
{
switch (CurrenttoolthingsType)
{
case ToolAndThingsType.None:
break;
case ToolAndThingsType.PA:
PA_ToolEvent();
break;
case ToolAndThingsType.SHUIHU:
SHUIHU_ToolEvent();
break;
case ToolAndThingsType.LANZI:
LANZI_ToolEvent();
break;
case ToolAndThingsType.小萝卜:
XIAOLUOBO_PlantEvent();
break;
case ToolAndThingsType.白萝卜:
BAILUOBO_PlantEvent();
break;
default:
break;
}
}
/// <summary>
/// 篮子工具
/// </summary>
public void LANZI_ToolEvent()
{
Vegetable v = ObjectDetect._instance.GetNearByMatureVege();
if (v != null)
{
v.PickVege();
}
}
/// <summary>
/// 水壶工具
/// </summary>
public void SHUIHU_ToolEvent()
{
Vegetable v = ObjectDetect._instance.GetNearByNeedWaterVege();
if (v != null)
{
v.WaterV();
}
}
/// <summary>
/// 杷工具使用
/// </summary>
public void PA_ToolEvent()
{
Ground g =ObjectDetect._instance.GetNearByUnOpenGround();
if (g != null)
{
g.OpenGround();
}
}
/// <summary>
/// 小萝卜种植
/// </summary>
public void XIAOLUOBO_PlantEvent()
{
Ground g = ObjectDetect._instance.GetNearByUnPlantGround();
if (g != null)
{
///设置基本信息
Vector3 pos = g.transform.position;
GameObject obj = Instantiate(toolandObjects[CurrenttoolthingsType], pos, Quaternion.identity);
obj.transform.SetParent(g.transform);
g.PlantVege(CurrenttoolthingsType);
MessInfoPanel._instance.ShowMess("种植"+CurrenttoolthingsType+"成功");
}
}
/// <summary>
/// 白萝卜种植
/// </summary>
public void BAILUOBO_PlantEvent()
{
Ground g = ObjectDetect._instance.GetNearByUnPlantGround();
if (g != null)
{
Vector3 pos = g.transform.position;
GameObject obj = Instantiate(toolandObjects[CurrenttoolthingsType], pos, Quaternion.identity);
obj.transform.SetParent(g.transform);
g.PlantVege(CurrenttoolthingsType);
MessInfoPanel._instance.ShowMess("种植" + CurrenttoolthingsType + "成功");
}
}
}
最后还是要回到unity中设置相应的参数
最后,就是为MessInfoPanel这个游戏物品添加一个脚本,起名为MessInfoPanel,主要是显示一些信息:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MessInfoPanel : MonoBehaviour
{
public static MessInfoPanel _instance;
private void Awake()
{
_instance = this;
}
public Text messText;
void Start()
{
messText = GetComponentInChildren<Text>();
GetComponent<Image>().CrossFadeAlpha(0, 1,true);
messText.gameObject.SetActive(false);
}
// Update is called once per frame
void Update()
{
}
IEnumerator showMessIE(string info)
{
GetComponent<Image>().CrossFadeAlpha(1, 1, true);
messText.gameObject.SetActive(true);
messText.text = info;
yield return new WaitForSeconds(1f);
GetComponent<Image>().CrossFadeAlpha(0, 1, true);
messText.gameObject.SetActive(false);
}
public void ShowMess(string info)
{
StartCoroutine(showMessIE(info));
}
}
以上,就是本教程的基本所有内容了。