Сбор ресурсов в стратегии [RTS]

Для развития необходимы ресурсы, это обязательное условие как для реальной жизни, так и для виртуальной, в частности, для стратегии. Задача состояла в следующем. При наличии на карте базы, то есть завода по переработке ресурса, и его источника, то есть поле, где комбайн должен собирать ресурсы. Если и то и другое есть, тогда всё должно происходить в автоматическом режиме, комбайн сам находит ближайший источник, относительно место своего положения, затем оправляется на сбор. После, возвращается на базу и передает ресурсы. Креме всего прочего, дополнительно, должна быть возможность вручную, конкретному комбайну, назначить сбор ресурса, например, отправить одного в дальний участок карты.


Для начала, необходимо чтобы в проекте бала создана карта навигации, которую использует NavMesh Agent, о том как это сделать можно почитать как в официальном мануале, там и у нас, поиск в помощь, сложного там ничего нет.

Далее. Создадим сам ресурс, как бы он не выглядел, у него должен быть свой тег, коллайдер в режиме триггера и скрипт:

using UnityEngine;
using System.Collections;

public class ResourceElement : MonoBehaviour { // ресурс, дочерний объект для ResourceSource

	public bool empty; // ресурс собран или нет
	public bool active; // занят комбайном или нет

	public void ResetResource() // восстановление ресурса
	{
		active = false;
		empty = false;

		transform.localScale = Vector3.one;
	}

	public bool ResourceUpdate() // сбор ресурса
	{
		// тут использовано условие уменьшение размера объекта, что засчитывается как - сбор закончен
		// разумеется можно использовать какое-нибудь другое условие
		transform.localScale -= Vector3.one * 0.05f;

		if(transform.localScale.x < 0.1f)
		{
			transform.localScale = Vector3.zero;

			empty = true;
			return true;
		}

		return false;
	}

}

Когда ресурс готов, нужно их собрать в группу, для этого создаем пустышку, делаем ресурс дочерним, а на родителя вешаем скрипт:

using UnityEngine;
using System.Collections;

public class ResourceSource : MonoBehaviour { // источник ресурса / поле для сбора

	public ResourceElement[] resources; // сюда в ручную добавляем все дочерние ресурсы
	public float value = 10; // сколько добавляет один ресурс
	public int id = 1; // номер, для воды например - 1, для метала - 2 и т.п.
	public float timeout = 40; // время в секундах, по истечение которого ресурсы будут восстановлены
	public bool empty; // ресурсы исчерпаны или нет

	void Start()
	{
		ResourceManager.source.Add(this);
	}

	public void CheckSource() // проверка, есть-ли ресурсы
	{
		int j = 0;
		foreach(ResourceElement e in resources)
		{
			if(e.empty) 
			{
				j++;
			}
		}

		if(j == resources.Length)
		{
			empty = true;
			StartCoroutine(Reset());
		}
	}

	IEnumerator Reset() // если ресурсов нет, запуск счетчика до восстановления
	{
		yield return new WaitForSeconds(timeout);
		foreach(ResourceElement e in resources)
		{
			e.ResetResource();
		}
		empty = false;
	}
}

Размножаем ресурсы до нужно количества и настраиваем скрипт.

Теперь, база. Место куда должен вернуться комбайн, на этот объект вешаем скрипт:

using UnityEngine;
using System.Collections;

public class ResourceBase : MonoBehaviour { // база для комбайна / завод по переработке

	public float addValue = 1; // сколько ресурса будет добавлено за один шаг
	public bool active; // база занята или нет
	public float step = 0.1f; // шаг по времени, чтобы добавление ресурсов не происходило мгновенно

	private float curTime;
	private float index;

	void Start()
	{
		active = false;
		ResourceManager.bases.Add(this); // добавляем этот объект в массив
	}

	public bool BaseUpdate(int id, float value) // функция добавления ресурсов
	{
		if(index >= value)
		{
			index = 0;
			active = false;
			return true;
		}

		curTime += Time.deltaTime;
		if(curTime > step)
		{
			index += addValue;
			ResourceManager.AddResource(id, addValue);
			curTime = 0;
		}

		return false;
	}
}

Еще нам понадобиться что-то вроде менеджера, добавляем на сцену:

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

public class ResourceManager : MonoBehaviour { // менеджер ресурсов

	public static List<ResourceSource> source; // массив со всеми источниками ресурсов / поля для сбора
	public static List<ResourceBase> bases; // массив со всеми базами / заводы по переработке

	public Text info; // вывод текстовой информации о ресурсах

	public static float _water; // вода, для примера
	public static float _metal; // метал, для примера

	private Harvester harvester;

	void Awake()
	{
		source = new List<ResourceSource>();
		bases = new List<ResourceBase>();
		ClearAll();
	}

	public static void ClearAll() // сброс ресурсов
	{
		_water = 0;
		_metal = 0;
	}

	public static void AddResource(int res_id, float value) // добавление нового ресурса, на основе id и добавочного значения
	{
		switch(res_id)
		{
		case 1:
			_water += value;
			break;
		case 2:
			_metal += value;
			break;
		}

	}

	void LateUpdate()
	{
		info.text = "Water: " + _water + "\nMetal: " + _metal; // обновление текстовой информации

		if(Input.GetMouseButtonDown(0)) // если ЛКМ
		{
			// это пример, использованы стандартные теги, в реальной игре, нужно изменить теги на отдельные для комбайна и ресурса
			RaycastHit hit;
			Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
			if(Physics.Raycast(ray, out hit))
			{
				if(hit.transform.tag == "Player" && !harvester) // поиск комбайна по тегу Player
				{
					harvester = hit.transform.GetComponent<Harvester>();
					harvester.GetComponent<MeshRenderer>().material.color = Color.cyan; // для теста, визуальное обозначение, если выбран
				}
				else if(hit.transform.tag == "GameController" && harvester) // поиск ресурса по тегу GameController
				{
					harvester.GetComponent<MeshRenderer>().material.color = Color.red; // для теста, визуальное обозначение, если отправлен собирать ресурс
					ResourceSource source = hit.transform.GetComponentInParent<ResourceSource>();
					harvester.SetNewTarget(source);
					harvester = null;
				}
			}
		}
	}

}

Финальный штрих, сам комбайн, у него тоже должен быть свой тег и коллайдер, вешаем на него:

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

[RequireComponent(typeof(NavMeshAgent))]

public class Harvester : MonoBehaviour { // сборщик ресурсов / комбайн

	// мин/макс дистанция до точки назначения, для создания эффекта, когда один комбайн ожидает свободное место
	public float minDistance = 0;
	public float maxDistance = 7;

	private NavMeshAgent agent;
	private ResourceSource source;
	private ResourceElement element;
	private ResourceBase _base;
	private List<int> e_sort;
	private int id;
	private float value;
	private bool wait, target;

	void Awake()
	{
		agent = GetComponent<NavMeshAgent>();
	}

	void LateUpdate()
	{
		if(!source)
		{
			element = null;
			_base = null;
			source = null;
			NearSource();
		}
		else
		{
			GetAction();
		}
	}

	public void SetNewTarget(ResourceSource targetSource) // сброс текущей задачи и ручное назначение нового источника ресурсов
	{
		source = targetSource;
		id = targetSource.id;
		value = targetSource.value;
		if(_base) _base.active = false;
		if(element) element.active = false;
		target = true;
		element = null;
		_base = null;
	}

	void GetAction() // выбор текущего действия
	{
		if(!element && !_base)
		{
			e_sort = new List<int>();
			for(int j = 0; j < source.resources.Length; j++)
			{
				if(!source.resources[j].empty && !source.resources[j].active)
					e_sort.Add(j); // поиск не занятых другими комбайнами и полных ресурсов
			}
			if(e_sort.Count == 0) // если их нет
			{
				target = false; // сброс ручного назначения
				source = null; // сброс текущего участка где производился поиск
				return;
			}
			int i = e_sort[Random.Range(0, e_sort.Count)]; // если на участке найдены ресурсы, выбираем случайный из них
			element = source.resources[i]; // назначаем конкретный объект
			element.active = true; // помечаем его, что он теперь занят
			agent.SetDestination(element.transform.position); // отправляемся к нему
		}
		else if(element && !agent.hasPath && !_base && element.ResourceUpdate()) // если - ресурс назначен + комбайн приехал к нему + база возврата не назначена + ресурс собран
		{
			NearBase(false); // поиск ближайшей свободной базы
			if(!_base) // если таковых нет
			{
				NearBase(true); // поиск ближайшей занятой базы
				if(!_base) return;
				wait = true; // переходим в режим ожидания
				agent.stoppingDistance = maxDistance; // назначаем дистанцию ожидания
			}

			_base.active = true; // помечаем что она занята
			element.active = false; // освобождаем ресурс
			agent.SetDestination(_base.transform.position); // едим на базу
			source.CheckSource(); // запуск проверки источника ресурсов
		}
		else if(_base && !agent.hasPath && !wait && _base.BaseUpdate(id, value)) // назначена база + комбайн доехал до нее + не ждет очереди + передал ресурсы
		{
			if(target)
			{
				element = null;
				_base = null;
			}
			else
			{
				source = null;	
			}
		}
		else if(_base && wait && !_base.active) // назначена база + ждет очереди + база занята
		{
			_base.active = true;
			wait = false;
			agent.stoppingDistance = minDistance;
		}
	}

	void NearSource() // поиск ближайшего источника ресурсов, относительно базы в которой находится юнит
	{
		float dist = Mathf.Infinity;
		foreach(ResourceSource s in ResourceManager.source)
		{
			if(s && !s.empty)
			{
				float curDist = Vector3.Distance(transform.position, s.transform.position);
				if(curDist < dist)
				{
					source = s;
					dist = curDist;
					id = s.id;
					value = s.value;
				}	
			}
		}
	}

	void NearBase(bool isActive) // поиск ближайшей базы
	{
		float dist = Mathf.Infinity;
		foreach(ResourceBase b in ResourceManager.bases)
		{
			if(b && b.active == isActive)
			{
				float curDist = Vector3.Distance(transform.position, b.transform.position);
				if(curDist < dist)
				{
					_base = b;
					dist = curDist;
				}	
			}
		}
	}
}

Итак, немного о логике работы, поведение комбайна.
  • на старте, делается запрос о текущих полях с ресурсами
  • выбирается то, которое ближе всего от текущего положения
  • в самом поле, выбирается случайный элемент ресурса, который не занят другим комбайном
  • после сбора, поиск баз, которые не заняты комбайнами
  • если таких нет, будет выбрана ближайшая занятая и юнит прейдет в режим ожидания, когда доедит до базы
  • если свободные базы есть, будет выбрана ближайшая

Скачать демо проект:

Внимание! Посетители, находящиеся в группе Гости, не могут скачивать файлы.

Комментариев 3

Офлайн
Light 5 февраля 2019
mr_carlos0000, планируется обновление проекта, в новых версиях, скорей всего всё это будет.
Офлайн
mr_carlos0000 1 февраля 2019
А как можно сделать чтобы комбайны собирали только тот ресурс на который я нажал мышью ?
Офлайн
AlexMcKing 23 октября 2017
Спасибо за урок!
У меня такой вопрос как реализовать, например генератор энергии, чтобы генератор построил и ресурсы прибавляются?
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика