Unity实战篇 | 游戏中控制 地图无限自动化生成 的方法,进一步优化项目

  • 🎬 博客主页:https://xiaoy.blog.csdn.net

  • 🎥 本文由 呆呆敲代码的小Y 原创 🙉

  • 🎄 学习专栏推荐:Unity系统学习专栏

  • 🌲 游戏制作专栏推荐:游戏制作

  • 🌲Unity实战100例专栏推荐:Unity 实战100例 教程

  • 🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

  • 📆 未来很长,值得我们全力奔赴更美好的生活✨

  • ——————❤️分割线❤️————————-


目录


请添加图片描述


📢 前言

  • 在一个实际的的游戏项目中,地图和地形基本上是不可或缺的一部分。
  • 假设在游戏中有很多地形的时候,我们就需要对地形的存在做一个可控的管理,不然玩游戏的时候设备就要吃苦了!
  • 对地形管理的方法也有很多种,本篇文章就来介绍一个地图自动化生成的方法。
  • 本文只用到了一个脚本进行管理,如果有这样的需求可以直接跳转到文末复制代码到自己的项目中就可以使用了!

🎬 Unity实战篇 | 游戏中控制 地图自动化生成 的方法,进一步优化项目

在这里插入图片描述

上面是具体效果的一个简单展示

我们要做的就是让我们控制的角色在使用该地形时,该地形进行显示,在角色离开该地形时,则取消地形的显示。

这样做有两个很大的好处:

  • 一个是可以对场景中的地形进行优化显示,避免所有的地形都在场上,但是却一直用不到,以至于白白对性能造成一些浪费。
  • 另一个则是可以无限控制地图的自动化生成。我们可以预先配置几个初始场景,然后剩下的就可以让程序自动生成,当角色走到该地域时生成一个地块,离开时则消失显示。既可以做到无限地图的一个效果,又避免了性能的浪费!

下面一起看看怎样做出这样一个效果的吧~


❤️ 第一步:准备好需要用到的地形预制体

既然是自动化生成一块,那么我们需要提前准备好可能需要用到的地形场景。

这里的地块可以准备多个,也可以只准备一个,这要看我们是不是有一个生成随机地块的需求啦!

打开Unity新建一个场景,并拖入一个Plane并在上面随便添加一些3D的游戏对象!

在这里插入图片描述
我这里简单搭建了一个测试场景,并使用不同的颜色做区别,实际项目中则根据具体的地形使用即可。

制作完地形后记得将模型拖入Project中变成预制体!


🧡 第二步:生成初始九宫格地形

在刚开始运行时我们需要显示一个初始的地块,这里让他显示周围九宫格地形,也就是玩家周围一圈。

直接上代码看一下,代码中对重点部分已做注释:

	public Transform _terrParent;//生成地形的父对象
	public GameObject _terrainObj;//地块预制体
	public GameObject _player;//玩家

	private Dictionary<(int x, int y), GameObject> _terrainLoaded;//击落已加载过的地形
	private Dictionary<(int x, int y), GameObject> _dictTemp;//记录临时储存的地形
	private Dictionary<(int x, int y), GameobjAndCoroutine> _unloadTerrCountDown;//即将进行隐藏显示的地形记录
	private Stack<GameObject> _terrainPool;
	private (int x, int y) _lastPos = (0, 0);//玩家末位置

	struct GameobjAndCoroutine
	{
		public GameObject Go;
		public Coroutine Cor;
	}
	private void Awake()
	{
		_terrainLoaded = new Dictionary<(int x, int y), GameObject>();
		_dictTemp = new Dictionary<(int x, int y), GameObject>();
		_unloadTerrCountDown = new Dictionary<(int x, int y), GameobjAndCoroutine>();
		_terrainPool = new Stack<GameObject>();
	}

	private void Start()
	{
	    //对角色周围一圈九宫格进行检查
		for (int i = -1; i < 2; i++)
		{
			for (int j = -1; j < 2; j++)
			{
				if (_terrainLoaded.TryGetValue((i, j), out GameObject terr))//判断该位置的地形是否已加载过
				{
					_dictTemp.Add((i, j), terr);//
					_terrainLoaded.Remove((i, j));
					terr.transform.position = new Vector3(i * 100f, 0f, j * 100f);
					terr.SetActive(true);
				}
				else
				{
					if (_unloadTerrCountDown.TryGetValue((i, j), out GameobjAndCoroutine val))//判断_unloadTerrCountDown已经存储有该地形的游戏对象
					{
						StopCoroutine(val.Cor);
						_dictTemp.Add((i, j), val.Go);
						_unloadTerrCountDown.Remove((i, j));
						val.Go.transform.position = new Vector3(i * 100f, 0f, j * 100f);
						val.Go.SetActive(true);
					}
					else
					{
						var newTerr = GetTerrain();//生成具体的地形
						_dictTemp.Add((i, j), newTerr);
						newTerr.transform.position = new Vector3(i * 100f, 0f, j * 100f);
						newTerr.SetActive(true);
					}
				}
			}
		}
		(_terrainLoaded, _dictTemp) = (_dictTemp, _terrainLoaded);
	}
	
	///获取一个地形对象
	private GameObject GetTerrain()
	{
		if (_terrainPool.Count > 0)//检查地形栈中是否有地形
		{
			return _terrainPool.Pop();//将某个地形弹出栈
		}
		return Instantiate(_terrainObj, _terrParent);//生成一个地形对象
	}

效果如下:

在这里插入图片描述

将上述脚本代码挂在场景中,然后拖入地形的父对象(场景中新建一个空对象即可),角色,和地块预制体。

这样就可以简单实现角色周围一圈的地块生成了,不过这样离我们要实现的效果还差挺多,接着往下写。


💛 第三步:玩家位置变化后自动生成新地形

上面的代码智能满足我们玩家周围一圈的地方生成地形,这样做的效果还远远不够。

所以这一步要让玩家的位置发生变化之后生成新的地块,一起来看看怎样操作吧!

上代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TerrainSlicing : MonoBehaviour
{
	public Transform _terrParent;
	public GameObject _terrainObj;
	public GameObject _player;

	private Dictionary<(int x, int y), GameObject> _terrainLoaded;//击落已加载过的地形
	private Dictionary<(int x, int y), GameObject> _dictTemp;//记录临时储存的地形
	private Dictionary<(int x, int y), GameobjAndCoroutine> _unloadTerrCountDown;//即将进行隐藏显示的地形记录
	private Stack<GameObject> _terrainPool;
	private (int x, int y) _lastPos = (0, 0);

	struct GameobjAndCoroutine
	{
		public GameObject Go;
		public Coroutine Cor;
	}
	private void Awake()
	{
		_terrainLoaded = new Dictionary<(int x, int y), GameObject>();
		_dictTemp = new Dictionary<(int x, int y), GameObject>();
		_unloadTerrCountDown = new Dictionary<(int x, int y), GameobjAndCoroutine>();
		_terrainPool = new Stack<GameObject>();
	}

	private void Start()
	{
		for (int i = -1; i < 2; i++)
		{
			for (int j = -1; j < 2; j++)
			{
				if (_terrainLoaded.TryGetValue((i, j), out GameObject terr))//判断该位置的地形是否已加载过
				{
					_dictTemp.Add((i, j), terr);//
					_terrainLoaded.Remove((i, j));
					terr.transform.position = new Vector3(i * 100f, 0f, j * 100f);
					terr.SetActive(true);
				}
				else
				{
					if (_unloadTerrCountDown.TryGetValue((i, j), out GameobjAndCoroutine val))//判断_unloadTerrCountDown已经存储有该地形的游戏对象
					{
						StopCoroutine(val.Cor);
						_dictTemp.Add((i, j), val.Go);
						_unloadTerrCountDown.Remove((i, j));
						val.Go.transform.position = new Vector3(i * 100f, 0f, j * 100f);
						val.Go.SetActive(true);
					}
					else
					{

						var newTerr = GetTerrain();
						_dictTemp.Add((i, j), newTerr);
						newTerr.transform.position = new Vector3(i * 100f, 0f, j * 100f);
						newTerr.SetActive(true);
					}
				}
			}
		}
		(_terrainLoaded, _dictTemp) = (_dictTemp, _terrainLoaded);
	}

//	/*
	private void FixedUpdate()
	{
		if (_player !=null )
		{
			//记录角色在地图上的位置
			(int x, int y) pos = (Mathf.RoundToInt(_player.transform.position.x / 100f), Mathf.RoundToInt(_player.transform.position.z / 100f));
			if (!(pos == _lastPos))//当位置发生改变时进行判断,玩家进入新区域
			{
				_lastPos = pos;
				_dictTemp.Clear();
				//围绕玩家当前进入的新地形的周围九宫格一圈进行检查
				for (int i = pos.x - 1; i < pos.x + 2; i++)
				{
					for (int j = pos.y - 1; j < pos.y + 2; j++)
					{
						if (_terrainLoaded.TryGetValue((i, j), out GameObject terr))//如果_terrainLoaded已经存储有该地形的游戏对象
						{
							_dictTemp.Add((i, j), terr);
							_terrainLoaded.Remove((i, j));
							terr.transform.position = new Vector3(i * 100f, 0f, j * 100f);
							terr.SetActive(true);
						}
						else
						{
							if (_unloadTerrCountDown.TryGetValue((i, j), out GameobjAndCoroutine val))//如果_unloadTerrCountDown已经存储有该地形的游戏对象
							{
								StopCoroutine(val.Cor);//停止隐藏地块的协程,玩家此时离开该地块但是在3秒内又回到了该地块时
								_dictTemp.Add((i, j), val.Go);
								_unloadTerrCountDown.Remove((i, j));
								val.Go.transform.position = new Vector3(i * 100f, 0f, j * 100f);
								val.Go.SetActive(true);
							}
							else
							{
								var newTerr = GetTerrain();
								_dictTemp.Add((i, j), newTerr);
								newTerr.transform.position = new Vector3(i * 100f, 0f, j * 100f);
								newTerr.SetActive(true);
							}
						}
					}
				}

				foreach (var item in _terrainLoaded)//遍历_terrainLoaded,此时的_terrainLoaded内的对象都不在角色当前九宫格一圈,所以遍历进行准备隐藏显示
				{
					_unloadTerrCountDown.Add(item.Key, new GameobjAndCoroutine
					{
						Cor = StartCoroutine(RemoveTerrDelay(item.Key)),// 开启取消地块显示的协程
						Go = item.Value,
					});
				}
				_terrainLoaded.Clear();
				(_terrainLoaded, _dictTemp) = (_dictTemp, _terrainLoaded);
			}
		}
	}

	/// <summary>
    /// 取消地块显示
    /// </summary>
    /// <param name="pos"></param>
    /// <returns></returns>
	private IEnumerator RemoveTerrDelay((int x, int y) pos)
	{
		yield return new WaitForSeconds(3f);//等待3秒隐藏地块显示
		if (_unloadTerrCountDown.TryGetValue(pos, out var v))
		{
			RecycleTerrain(v.Go);
			_unloadTerrCountDown.Remove(pos);
		}
	}

	/// <summary>
    /// 获取一个地块   
    /// </summary>
    /// <returns></returns>
	private GameObject GetTerrain()
	{
		if (_terrainPool.Count > 0)
		{
			return _terrainPool.Pop();//
		}
		return Instantiate(_terrainObj, _terrParent);//生成一个地形对象

	}

	/// <summary>
    /// 回收地块
    /// </summary>
    /// <param name="t"></param>
	private void RecycleTerrain(GameObject t)
	{
		t.SetActive(false);
		_terrainPool.Push(t);
	}
}

此时效果如下:

在这里插入图片描述

此时已经满足地块自动生成的效果了,如果我们不需要提前自定义一些固定的地块,那么到这一步就可以满足我们自动化生成地形的功能了。

但是我们如果想要自定义好一个初始地形的话,目前还没法实现,所以接着往下看,怎样将地形提前定义好!


💚 第四步:固定一个初始地形的位置,以及随机生成地形的列表

上面的代码已经基本实现自动化生成地形的方法了,所以接下来对代码进行一个简单的优化就好。

完整代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TerrainSlicing1 : MonoBehaviour
{
	[Header("玩家")]
	public Transform _player;
	[Header("生成地形的父对象")]
	public Transform _terrParent;
	[Header("生成地形的大小(单个预制体 长/宽)")]
	public Vector2 _terrainSize=new Vector2(10,10);
	[Header("固定场景中的已有地形的位置")]
	public StructData[] datas;
	[Header("超出已设置的地形范围后,随机生成的地形")]
	public List<GameObject> _terrainObjs;
	[Header("离开地形区域后地形自动隐藏的时间")]
	public float _timer = 3f;

	private Vector2 terrainSize;
	private Dictionary<Vector2, GameObject> _terrainLoadedFixed;
	private Dictionary<(int x, int y), GameObject> _terrainLoaded;
	private Dictionary<(int x, int y), GameObject> _dictTemp;
	private Dictionary<(int x, int y), GameobjAndCoroutine> _unloadTerrCountDown;
	private Stack<GameObject> _terrainPool;
	private List<GameObject> _terrainLoadObjs;
	private (int x, int y) _lastPos = (0, 0);

	struct GameobjAndCoroutine
	{
		public GameObject Go;
		public Coroutine Cor;
	}

	[System.Serializable]
	public struct StructData
	{
		public Vector2 key;
		public GameObject value;
	}

	private void Awake()
	{
		terrainSize = new Vector2(_terrainSize.x*10, _terrainSize.y*10);
		_terrainLoadedFixed = new Dictionary<Vector2, GameObject>();
		_terrainLoaded = new Dictionary<(int x, int y), GameObject>();
		_dictTemp = new Dictionary<(int x, int y), GameObject>();
		_unloadTerrCountDown = new Dictionary<(int x, int y), GameobjAndCoroutine>();
		_terrainPool = new Stack<GameObject>();
		_terrainLoadObjs = new List<GameObject>();
		InitData();
	}

	private void Start()
	{
		FirstLoadTerrain();
	}

	private void FixedUpdate()
	{
		LoadTerrain();
	}

	/// <summary>
	/// 将固定场景中的地形传给字典
	/// </summary>
	void InitData()
	{
		for (int i = 0; i < datas.Length; i++)
		{
			if (_terrainLoadedFixed.ContainsKey(datas[i].key))
				return;
			_terrainLoadedFixed.Add(datas[i].key, datas[i].value);
		}
	}

	/// <summary>
	/// 第一次加载地形
	/// </summary>
	private void FirstLoadTerrain()
	{
		for (int i = -1; i < 2; i++)
		{
			for (int j = -1; j < 2; j++)
			{
				if (_terrainLoaded.TryGetValue((i, j), out GameObject terr))
				{
					_dictTemp.Add((i, j), terr);
					_terrainLoaded.Remove((i, j));
					terr.transform.position = new Vector3(i * terrainSize.x, 0f, j * terrainSize.y);
					terr.SetActive(true);

				}
				else
				{
					if (_unloadTerrCountDown.TryGetValue((i, j), out GameobjAndCoroutine val))
					{
						StopCoroutine(val.Cor);
						_dictTemp.Add((i, j), val.Go);
						_unloadTerrCountDown.Remove((i, j));
						val.Go.transform.position = new Vector3(i * terrainSize.x, 0f, j * terrainSize.y);
						val.Go.SetActive(true);
					}
					else
					{
						//var newTerr = GetTerrain();
						var newTerr = GetTerrainNew(i, j);
						_dictTemp.Add((i, j), newTerr);
						newTerr.transform.position = new Vector3(i * terrainSize.x, 0f, j * terrainSize.y);
						newTerr.SetActive(true);
					}
				}
			}
		}
		(_terrainLoaded, _dictTemp) = (_dictTemp, _terrainLoaded);
	}

	/// <summary>
    /// 加载地形
    /// </summary>
	private void LoadTerrain()
    {
		if (_player != null)
		{
			(int x, int y) pos = (Mathf.RoundToInt(_player.position.x / terrainSize.x), Mathf.RoundToInt(_player.position.z / terrainSize.y));
			if (!(pos == _lastPos))//当位置发生改变时进行判断,玩家进入新区域
			{
				_lastPos = pos;
				_dictTemp.Clear();
				//围绕玩家当前进入的新地形的周围九宫格一圈进行检查
				for (int i = pos.x - 1; i < pos.x + 2; i++)
				{
					for (int j = pos.y - 1; j < pos.y + 2; j++)
					{
						if (_terrainLoaded.TryGetValue((i, j), out GameObject terr))//如果_terrainLoaded已经存储有该地形的游戏对象
						{
							_dictTemp.Add((i, j), terr);
							_terrainLoaded.Remove((i, j));
							terr.transform.position = new Vector3(i * terrainSize.x, 0f, j * terrainSize.y);
							terr.SetActive(true);
						}
						else//地图的生成脚本
						{
							if (_unloadTerrCountDown.TryGetValue((i, j), out GameobjAndCoroutine val))//若玩家进入新地形后,在原来的地形还未取消激活时又返回到该位置时,执行该部分!
							{
								StopCoroutine(val.Cor);
								_dictTemp.Add((i, j), val.Go);
								_unloadTerrCountDown.Remove((i, j));
								val.Go.transform.position = new Vector3(i * terrainSize.x, 0f, j * terrainSize.y);
								val.Go.SetActive(true);
							}
							else//玩家每次离开原来的位置,进入新地方时调用该方法
							{
								var newTerr = GetTerrainNew(i, j);
								_dictTemp.Add((i, j), newTerr);
								newTerr.transform.position = new Vector3(i * terrainSize.x, 0f, j * terrainSize.y);
								newTerr.SetActive(true);
							}
						}
					}
				}

				//遍历_terrainLoaded,此时的_terrainLoaded内的对象都不在角色当前九宫格一圈,所以遍历进行准备隐藏显示
				foreach (var item in _terrainLoaded)//利用循环检测 将除了正在使用的9块地形之外的全部取消激活
				{
					_unloadTerrCountDown.Add(item.Key, new GameobjAndCoroutine
					{
						Cor = StartCoroutine(RemoveTerrDelay(item.Key)),// 开启取消地块显示的协程
						Go = item.Value,
					});
				}
				_terrainLoaded.Clear();
				(_terrainLoaded, _dictTemp) = (_dictTemp, _terrainLoaded);
			}
		}
	}
	
	/// <summary>
	/// 等待一段时间后,将地形对象取消激活
	/// </summary>
	/// <param name="pos"></param>
	/// <returns></returns>
	private IEnumerator RemoveTerrDelay((int x, int y) pos)
	{
		yield return new WaitForSeconds(_timer);
		if (_unloadTerrCountDown.TryGetValue(pos, out var v))
		{
			RecycleTerrain(v.Go);
			_unloadTerrCountDown.Remove(pos);
		}
	}

	/// <summary>
    /// 回收地形
    /// </summary>
    /// <param name="t"></param>
	private void RecycleTerrain(GameObject t)
	{
		t.SetActive(false);
	}

	/// <summary>
	/// 获取新地形
	/// </summary>
	/// <returns></returns>
	private GameObject GetTerrainNew(int x, int y)
	{
		if (_terrainLoadedFixed.TryGetValue(new Vector2(x,y), out GameObject terr))
		{
			if (!_terrainLoadObjs.Contains(terr))//第一次添加该区域的地形,新建并返回一个新的地形对象!
			{
				_terrainLoadObjs.Add(terr);
				return Instantiate(terr, _terrParent); ;
			}
			return Instantiate(terr, _terrParent); ;
		}

		int randomTer = Random.Range(0, _terrainObjs.Count);//从随机地形中抽取一个地形生成并返回
		return Instantiate(_terrainObjs[randomTer], _terrParent);//栈中没有该位置的地形,新建并返回一个新的地形对象!
	}
}

效果如下:

在这里插入图片描述

在代码中加入了一个初始地形的配置,包括单个地块的大小(长宽),初始地形的配置(使用坐标设置),超出提前设置好的位置后随机生成的地形列表,以及地块自动消失的时间等。

在这里插入图片描述


文章示例资源下载

本文其实只用到了一个脚本代码以及部分游戏资源

但是为了方便某些小伙伴使用,上传了一个Demo的资源包,有需要的也可以直接下载使用

游戏资源 – Unity中地块自动化生成 – 地形优化必备资源


👥 总结

  • 本文对游戏中的 地块自动化生成 做了一个简单的介绍以及实例演示。
  • 其中加入了一个初始地块的配置以及超出范围后随机生成地块的方法。
  • 在实际的项目中如果对此有需求的则可以直接将脚本代码复制,然后简单配置完参数就可以正常使用啦!

在这里插入图片描述


专栏推广

Unity专栏简介
以Unity引擎为基础,介绍关于Unity的各种文章学习,共同进步!Unity是一款跨平台的专业游戏引擎,用它创建的游戏可以在PC端、移动设备等常见平台上运行。

资料白嫖,技术互助

学习路线指引(点击解锁) 知识定位 人群定位
🧡 Unity系统学习专栏 🧡 入门级 本专栏从Unity入门开始学习,快速达到Unity的入门水平
💛 Unity实战类项目 💛 进阶级 计划制作Unity的 100个实战案例!助你进入Unity世界,争取做最全的Unity原创博客大全。
❤️ 游戏制作专栏 ❤️ 难度偏高 分享学习一些Unity成品的游戏Demo和其他语言的小游戏!
💚 游戏爱好者九万人社区💚 互助/吹水 九万人游戏爱好者社区,聊天互助,白嫖奖品
💙 Python零基础到入门 💙 Python初学者 针对没有经过系统学习的小伙伴,核心目的就是让我们能够快速学习Python的知识以达到入门

温馨提示: 点击下面卡片可以获取更多编程知识,包括各种语言学习资料,上千套PPT模板和各种游戏源码素材等等资料。更多内容可自行查看哦!
请添加图片描述

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注