Как стать автором
Обновить

Комментарии 25

Достаточно хорошая статья, но позволю себе одно небольшое замечание — z-сортировку предпочтительней делать с помощью Sorting Layers и Order in Layer, а не z координатой, поскольку это менее «интуитивно». В вашем случае можно сделать три слоя — для ловушек (самый «низкий»), для земли (средний) и для объектов переднего плана.
В этом случае не придется менять Z координату у объектов, они по умолчанию будут находиться в нужном месте с нужной «глубиной».
Спасибо за замечание! Обязательно изучу вопрос и расскажу об этом в следующих частях :)
А как же кешировать transform? Мне за такое в комментариях хотели руки пересадить куда надо :)
Сборник вредных советов.
    void Update () {
        transform.Rotate (new Vector3(0f,0f,-3f));
    }

Необходимо умножать вектор на Time.deltaTime, иначе вращение будет неравномерным и зависящим от FPS.
if ((col.gameObject.name == "dieCollider")||(col.gameObject.name == "saw"))

Определение типа объекта по его строковому имени? На костер. Хотя бы теги уже для этого использовали — они для этого предназначены.
 if (!(GameObject.Find("star")))

Вообще шикарно. Вы же представляете, насколько феерически медленно работает этот метод? ИМХО его если и стоит юзать, то разве что при загрузке сцены.

И да, кешировать transform не помешало бы.
Спасибо за сборник хороших советов :)

Про Time.deltaTime совершенно вылетело из головы — питерская осень жестокая штука.

Насчет поиска всех «съеденных» звезд — предложите быструю альтернативу?
Насчет поиска всех «съеденных» звезд — предложите быструю альтернативу?
Не искать звезды вовсе :) Заводим некий «игровой контроллер» (для простоты можно сделать его синглотоном, а для гурманов — можно заюзать какой-нибудь IoC-контейнер). Создаем также компонент для звезд, который при создании и при смерти будет кидать игровому контроллеру некое событие. Таким образом, игровой контроллер всегда будет точно знать, сколько сейчас звезд на карте, а игрок может его запрашивать без лишних расходов.
Отлично!) Надо будет в следующий раз на этом примере показать межскриптовое взаимодействие.

Еще раз спасибо!)
Кстати, вместо использования тегов можно повесить скрипт «убивания» на саму пилу или «компонент убивания» и настроить слои столкновений таким образом, чтобы столкновения проверялись только с персонажем.

При обнаружении столкновения на стороне пил или места падения ни тэг, ни имя проверять не придется, просто запускаем логику «убийства» персонажа
Что прикажете делать с триггером конца уровня и со звездами? В любом случае нужно определить, с чем именно столкнулись.
Ну и эти вещи можно по тому же принципу делать, разбить логику по разным объектам, заставлять их влиять как-то на персонажа. Скажем, если игроку надо собрать звезду, то при столкновении с игроком каждая звезда уменьшает в менеджере звезд какую-нибудь переменную. Идентификатор конца уровня, например, может проверить текущее значение этой переменной и не загрузить следующий уровень. В данном случае проверять по имени или тегу ничего не придется, компоненты тащить с гейм объекта, с которым столкнулись, тоже.
Это конечно все очень абстрактно, но в целом позволяет отойти от антипаттерна «God Object», который содержит в себе огромное количество логики, которую теоретически можно (и часто желательно) растащить по разным компонентам.

Но это все очень субъективно, я совершенно не претендую на догматичные «истины»
За что я люблю подобные топики — в комментах много умных мыслей, которые могут натолкнуть на развитие. Я вот почитал разные комментарии, погуглил другие топики и написал приблизительно такой рабочий код для приложения из этих трех статтей

Общие классы, не вешаются:

Scene
using UnityEngine;
using System.Collections;

public class Scene {

	private static Scene sceneInstance = null;

	public static Scene GetInstance () {
		if (sceneInstance == null) {
			sceneInstance = new Scene();
		}

		return sceneInstance;
	}

	public Character player = null;

	public BonusManager bonuses = new BonusManager();

	public void EndLevel () {
		Application.LoadLevel("scene2");
	}

	public void ReloadLevel () {
		Application.LoadLevel(Application.loadedLevel);
	}

}
BonusManager
using UnityEngine;
using System.Collections;

public class BonusManager {

	private int createdBonuses = 0;
	private int takkenBonuses = 0;
	
	public void CreateBonus (Bonus bonus) {
		createdBonuses += bonus.value;
	}
	
	public void TakeBonus (Bonus bonus) {
		if (!AllCollected()) {	
			takkenBonuses  += bonus.value;
			createdBonuses -= bonus.value;
		}
	}

	public int GetScore () {
		return takkenBonuses;
	}
	
	public bool AllCollected () {
		return createdBonuses <= 0;
	}
}
ExtendedMonoBehaviour
using UnityEngine;
using System.Collections;

public class ExtendedMonoBehaviour : MonoBehaviour {
	
	private Transform cachedTransform = null;
	
	public new Transform transform {
		get {
			if (cachedTransform == null) {
				cachedTransform = base.transform;
			}
			return cachedTransform;
		}
		protected set {
			cachedTransform = value;
		}
	}

	protected Scene scene {
		get {
			return Scene.GetInstance();
		}
	}



	protected bool isPlayerCol (Collider2D col) {
		return col.gameObject == scene.player.gameObject;
	}
}

Классы для объектов игры (на пилу навешивается Saw и Terminator, на DieCollider только Terminator)

Bonus
using UnityEngine;
using System.Collections;

public class Bonus : ExtendedMonoBehaviour {

	public int value = 1;

	public void OnEnable() {
		scene.bonuses.CreateBonus(this);
	}

	public void OnTriggerEnter2D (Collider2D col) {
		if (isPlayerCol(col)) {
			scene.bonuses.TakeBonus(this);
			Destroy(this.gameObject);
		}
	}
}
Camera
using UnityEngine;
using System.Collections;

public class Camera : ExtendedMonoBehaviour {
	public Transform target;
	public float dampTime = 0.15f;

	private Vector3 velocity = Vector3.zero;
	
	// Update is called once per frame
	void Update () {
		if (!target) return;

		Vector3 point = camera.WorldToViewportPoint(target.position);
		Vector3 delta = target.position - camera.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, point.z));
		Vector3 destination = transform.position + delta;
		 
		transform.position = Vector3.SmoothDamp(transform.position, destination, ref velocity, dampTime);
	}
}
Character
using UnityEngine;
using System.Collections;

public class Character : ExtendedMonoBehaviour {

	public float maxSpeed = 20f;
	public float jumpForce = 1000f;

	private bool facingRight = true;

	public void Start () {
		scene.player = this;
	}
	
	public void Update(){
		float move = Input.GetAxis("Horizontal");

		if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow)) {
			rigidbody2D.AddForce( new Vector2(0f, jumpForce) );
		}

		rigidbody2D.velocity = new Vector2(move * maxSpeed, rigidbody2D.velocity.y);

		if (move != 0) {
			CheckDirection(move > 0);
		}
		
		if (Input.GetKey(KeyCode.Escape)) {
			Application.Quit();
		}
		
		if (Input.GetKey(KeyCode.R)) {
			Application.LoadLevel(Application.loadedLevel);
		}
	}
	
	public void CheckDirection(bool right){
		if (facingRight != right) {
			facingRight = right;
			Vector3 scale = transform.localScale;
			scale.x *= -1;
			transform.localScale = scale;
		}
	}  

	public void Die() {
		scene.ReloadLevel();
	}
	
	public void OnGUI(){
		GUI.Box( new Rect(5, 5, 80, 22), "Stars: " + scene.bonuses.GetScore() );
	}
}
Exit
using UnityEngine;
using System.Collections;

public class Exit : ExtendedMonoBehaviour {
	
	public void OnTriggerEnter2D (Collider2D col) {
		if (isPlayerCol(col) && scene.bonuses.AllCollected()) {
			scene.EndLevel();
		}
	}
	
}
Saw
using UnityEngine;
using System.Collections;

public class Saw : ExtendedMonoBehaviour {

	public float speed = 180f;

	public void Update () {
		transform.Rotate( new Vector3(0f, 0f, speed * Time.deltaTime) );
	}

}
Terminator
using UnityEngine;
using System.Collections;

public class Terminator : ExtendedMonoBehaviour {

	public void OnTriggerEnter2D (Collider2D col) {
		if (isPlayerCol(col)) {
			scene.player.Die();
		}
	}
	
}


Оно не совсем корректно работает, но более интересно пообсуждать сам подход.
1. Мне не очень понравилась идея с Синглтоном, но я не совсем понял как создать инстанс для одной сцены и иметь к нему доступ?
2. Как ту реализуется DI?
3. Как, допустим, вынести ГУИ в отдельный обработчик с методом OnGui?
4. Можно конечно было бы повесить его на камеру или героя, но это как-то странно.
5. Как создать «Синглтон только для одной сцены»? Неплохо было бы, чтобы он автоматически уничтожался во время выгрузки сцены, дополнительный контроль утечек не помешает.
6. Или его вручную удалять после выгрузки сцены?
7. Как повесится на события выгрузки и загрузки сцены (абстрактные, а не привязанные к объекту)?

Я Юнити сегодня впервые вижу. Думаю, большинство вопросов уйдет, но, как я уже сказал, интересно не так прямые ответы на эти пункты, как общие рассуждения по поводу подхода. Даже если не уверены в правильности. Очень интересно такое читать и из сукупности идей делать своё мнение.
        get {
            if (cachedTransform == null) {
                cachedTransform = base.transform;
            }
            return cachedTransform;
        }

Нюанс — такого лучше избегать, так как оператор == для наследников UnityEngine.Object (коим является и Transform) перегружен и «под капотом» вызывает нативный код движка. В итоге такой код для «кеширования» не даст практически никаких преимуществ по скорости.
То же самое будет.
Полурешение — использовать ReferenceEquals, он проверяет именно ссылку. Как-то так:
        get {
            if (ReferenceEquals(cachedTransform, null)) {
                cachedTransform = base.transform;
            }
            return cachedTransform;
        }

Но это будет работать правильно, только если компонента не была уничтожена после первого вызова. Иначе останется «пустой» объект в managed-коде, который на ReferenceEquals(obj, null) возвращает false, но нативный объект, оберткой над которым он является, уже будет уничтожен, и попытка это заюзать закончится все тем же NullReferenceException. Так что, к сожалению, 100% надежного способа тут нет, разве что всегда помнить, какой объект должен существовать в каждый момент времени.
P.S. В Unity 5 все это дело очень сильно заоптимизировали, так что там можно уже не переживать по этому поводу, и подобные костыли просто не нужны.
я так понимаю, можно сохранять
bool transformCached = true
if(!trasformCached) set

а по остальному что скажете?
Да, я именно так и делаю. По остальному чуть позже отвечу
Я тормоз :( Уже, наверное, неактуально, но…

1. Ну… Все тем же синглтоном, в простейшем случае. Вешаете скрипт на объект в сцене. Скрипт тогда может выглядеть примерно так:
using UnityEngine;
using System.Collections;

public class FooManager : MonoBehaviour {

    private static FooManager instance = null;

    public static FooManager Instance () {
        get {
            return instance;
        }
    }
    private void Awake() {
        instance = this;
    }

}

2. Для DI сначала нужен DI-контейнер иметь, коих достаточно много для Юнити существует. Ну и можете почитать, например:
blogs.unity3d.com/2014/05/07/dependency-injection-and-abstractions/
habrahabr.ru/post/188438/
3-4. Мм, а в чем проблема? Делаете отдельный скрипт для GUI, даете ему любым удобным способом ссылки на нужные объекты. Скрипт берет публичные свойства нужных вам объектов и рисует.
5-6. Просто повесьте скрипт на объект в этой самой сцене. Таким образом, он уже будет создан при загрузке сцены, и при смене сцены будет автоматически выгружен вместе со всеми остальными объектами.
Ну и еще помимо вещей, которые имеют смысл только в пределах сцены, есть глобальные штуки, которые должны существовать практически с самого старта приложения (например, хранение настроек). Для таких я завожу отдельную сцену, которая только прогружает их заранее, и продолжает грузить уже, скажем, меню игры. И да, все эти объекты помечены DontDestroyOnLoad, чтобы переживать все смены сцен.
7. А нет таких событий. Можно нагородить костылей, но проще банально избегать ситуаций, когда такие события нужны… Так-то за загрузку сцены можно считать Awake, а за выгрузку — OnDestroy, лишь бы никто этот объект руками не трогал.
Почему? Актуально, спасибо)
Спасибо! Буду ждать продолжения! Особенно об анимации, я голосовал за неё.
Спасибо за статьи. Меня смущает странньій баг. Если герой упирается в колайдер боком, то прилипает к нему, пока не отпустить направление движения. Можно как-то обойти такую особенность?

О, это предмет бурных обсуждений на официальных ресурсах, там можно Санта Барбару снимать. Гуглится по запросу
unity3d 2d controller sticks to walls

Вкратце — большинство решает проблему закидывая на стены физический материал с нулевым трением, но в данном случае это не прокатит, потому что платформа и есть стена. Некоторые проверяют, находится ли сейчас персонаж в воздухе, и если да, то накидывают физический материал с нулевым трением на него. Кто-то не дает двигаться в сторону возможного «застревания» (кто-то рейкастом, кто-то свиптестом). Советую погуглить по приведенному выше запросу (для достижения наибольшего объема найденных тем запрос можно немного поменять).
Можно еще на платформу слева и справа прилепить коллайдеры, на которые кинуть материал с нулевым трением. В случае с префабом особой мороки вызвать не должно.
Почему бы в нашем примере не повесить нуль-материал на Бокс-Колайдер героя? Скольжение при беге будет корректным из-за Циркл-Колайдера, а об стены он будет скользить. Какие подводные камни этого решения?
Возможно резкое ускорение героя при скольжении по откосам. Чем ближе угол откоса к 90 градусам, тем выше скорость.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий