Можно задать время переходов и (для каждого перехода) набор эффектов, включающих :
- фоновый" цвет освещения (ambientSkyColor)
- "осадки" (реализуются скриптом, применяющим MeshParticleEmitter)
- фоновый звук
- длительность перехода в секундах (в течение которого и будет происходить смена)
- шанс получить этот эффект (сумма всех шансов для периода - 1).
Для смены скайбокса применён шейдер из http://wiki.unity3d.com/index.php?title=SkyboxBlended
Код наверняка ужасен ) Критика желательна.
Архив с демкой - https://www.dropbox.com/s/7d9plyli7c05d50/game.zip?dl=0
Если интересует реализация - читайте ниже.
Globals, cинглтон для хранения глобального состояния и подписки на его изменение
Скрытый текст:
Синтаксис:
Синтаксис: [ Показать ]
Используется csharp
using System.Collections.Generic;
using System;
public class GlobalSetEventArgs {
public string key;
public object value;
}
public class Globals {
private static Globals instance = null;
private Dictionary<string, object> Values;
public delegate void SetEventHandler(GlobalSetEventArgs e);
public event SetEventHandler SetEvent;
private Globals() {
Values = new Dictionary<string, object> ();
}
public static Globals Get() {
if (Globals.instance == null) {
Globals.instance = new Globals ();
}
return Globals.instance;
}
public void SetValue(string key, object value) {
if (!Values.ContainsKey (key)) {
Values.Add (key, value);
} else {
Values[key] = value;
}
GlobalSetEventArgs e = new GlobalSetEventArgs ();
if (SetEvent != null) {
e.key = key;
e.value = value;
SetEvent(e);
}
}
public object SetValue(string name) {
return Values[name];
}
public object this[string key] {
get {
return SetValue(key);
}
set {
SetValue(key, value);
}
}
}
using System;
public class GlobalSetEventArgs {
public string key;
public object value;
}
public class Globals {
private static Globals instance = null;
private Dictionary<string, object> Values;
public delegate void SetEventHandler(GlobalSetEventArgs e);
public event SetEventHandler SetEvent;
private Globals() {
Values = new Dictionary<string, object> ();
}
public static Globals Get() {
if (Globals.instance == null) {
Globals.instance = new Globals ();
}
return Globals.instance;
}
public void SetValue(string key, object value) {
if (!Values.ContainsKey (key)) {
Values.Add (key, value);
} else {
Values[key] = value;
}
GlobalSetEventArgs e = new GlobalSetEventArgs ();
if (SetEvent != null) {
e.key = key;
e.value = value;
SetEvent(e);
}
}
public object SetValue(string name) {
return Values[name];
}
public object this[string key] {
get {
return SetValue(key);
}
set {
SetValue(key, value);
}
}
}
Time, реализация игрового времени (крепится к постоянно существующему объекту, я использовал террейн)
Скрытый текст:
Синтаксис:
Синтаксис: [ Показать ]
Используется csharp
using UnityEngine;
using System.Collections.Generic;
using System;
public class Time : MonoBehaviour {
public DateTime time;
[System.Serializable]
public struct TimeData {
public int year;
public int month;
public int day;
public int hour;
public int minute;
public int second;
}
public TimeData startTime;
public int cooficient;
void Start () {
try {
time = (System.DateTime)Globals.Get ()["time"];
}
catch(KeyNotFoundException) {
time = new DateTime(
startTime.year, startTime.month, startTime.day, startTime.hour, startTime.minute, startTime.second
);
}
}
void Update () {
int addMilliseconds = (int)(1000 * UnityEngine.Time.deltaTime * cooficient);
time = time.AddMilliseconds (addMilliseconds);
Globals.Get ()["time"] = time;
}
}
using System.Collections.Generic;
using System;
public class Time : MonoBehaviour {
public DateTime time;
[System.Serializable]
public struct TimeData {
public int year;
public int month;
public int day;
public int hour;
public int minute;
public int second;
}
public TimeData startTime;
public int cooficient;
void Start () {
try {
time = (System.DateTime)Globals.Get ()["time"];
}
catch(KeyNotFoundException) {
time = new DateTime(
startTime.year, startTime.month, startTime.day, startTime.hour, startTime.minute, startTime.second
);
}
}
void Update () {
int addMilliseconds = (int)(1000 * UnityEngine.Time.deltaTime * cooficient);
time = time.AddMilliseconds (addMilliseconds);
Globals.Get ()["time"] = time;
}
}
Назначение публичных полей вроде очевидно, но всё же - startTime - начальное время, если в Globals время ещё не задано, cooficient - коофициент ускорения относительно реального времени.
Fallout, управление "осадками". Крепится к префабу, включающему MeshParticleEmitter. Могут быть отключены по заданию Globals.Get()["playerInShelter"]=true, включены по false.
Скрытый текст:
Синтаксис:
Синтаксис: [ Показать ]
Используется csharp
using UnityEngine;
using System.Collections;
public class Fallout : MonoBehaviour {
MeshParticleEmitter emitter {
get {
return gameObject.GetComponent<MeshParticleEmitter> ();
}
}
void SetIntensity(float value) {
emitter.minEmission = value * emitter.maxEmission;
}
float GetIntensity() {
return emitter.minEmission / emitter.maxEmission;
}
public float instensity {
get { return GetIntensity (); }
set { SetIntensity (value); }
}
void ShelterEvent(GlobalSetEventArgs e) {
if (e.key == "playerInShelter") {
bool inShelter = (bool)e.value;
if (inShelter) {
StopEmitter();
} else {
StartEmitter();
}
}
}
public void StartEmitter() {
emitter.emit = true;
}
public void StopEmitter() {
emitter.emit = false;
}
/**
* Initializing
*/
void Start() {
Globals.Get().SetEvent += ShelterEvent;
}
/**
* Unloading
*/
void OnDestroy() {
Globals.Get ().SetEvent -= ShelterEvent;
}
}
using System.Collections;
public class Fallout : MonoBehaviour {
MeshParticleEmitter emitter {
get {
return gameObject.GetComponent<MeshParticleEmitter> ();
}
}
void SetIntensity(float value) {
emitter.minEmission = value * emitter.maxEmission;
}
float GetIntensity() {
return emitter.minEmission / emitter.maxEmission;
}
public float instensity {
get { return GetIntensity (); }
set { SetIntensity (value); }
}
void ShelterEvent(GlobalSetEventArgs e) {
if (e.key == "playerInShelter") {
bool inShelter = (bool)e.value;
if (inShelter) {
StopEmitter();
} else {
StartEmitter();
}
}
}
public void StartEmitter() {
emitter.emit = true;
}
public void StopEmitter() {
emitter.emit = false;
}
/**
* Initializing
*/
void Start() {
Globals.Get().SetEvent += ShelterEvent;
}
/**
* Unloading
*/
void OnDestroy() {
Globals.Get ().SetEvent -= ShelterEvent;
}
}
Weather, собственно - погода.
Скрытый текст:
Синтаксис:
Синтаксис: [ Показать ]
Используется csharp
using UnityEngine;
using System.Collections;
using System;
/**
* Unity3D weather system.
* Needs "Skybox/Blended" (or compatible) shader to switch skyboxes
*/
public class Weather : MonoBehaviour {
[System.Serializable]
public class WeatherEffect {
public float switchTime;
public Material skybox;
public double chance;
public Color ambientColor;
public AudioSource ambientSound;
public GameObject falloutPrefab;
}
/**
* Daytime period
*/
[System.Serializable]
public class WeatherPeriod {
public int fromHour;
public int fromMinute;
public int toHour;
public int toMinute;
public WeatherEffect[] effects;
}
/**
* Fallout prefabs instances will be binded to it (prefferly - controller camera)
*/
public GameObject bindFalloutToObject;
/**
* Shader for skybox
*/
public Shader skyboxShader;
/**
* Daytime periods
*/
public WeatherPeriod[] periods;
/**
* Random number generator to switch effects
*/
System.Random random;
/**
* is effect ready, or we need to play effect "animations"
*/
bool effectReady = true;
/**
* Current period
*/
WeatherPeriod activePeriod = null;
/**
* Current effect
*/
WeatherEffect activeEffect = null;
/**
* Previous effect ambient color (to animated change to new)
*/
Color previousAmbientColor;
/**
* Previous effect ambient sound (to decrease volume)
*/
AudioSource previousAmbientSound = null;
/**
* Previous fallout effect (to change intensivity)
*/
GameObject previousFallout = null;
/**
* Effect switching started at
*/
DateTime switchStartedAt;
/**
* Current fallout effect
*/
GameObject fallout = null;
/**
* Validate weather periods/effects information
*/
void Validate() {
foreach (WeatherPeriod period in periods) {
if (period.fromHour <0 || period.fromHour > 23) {
throw new WeatherConfigurationException("Period 'fromHour' must be in interval [0..23]");
}
if (period.toHour <0 || period.toHour > 24) {
throw new WeatherConfigurationException("Period 'toHour' must be in interval [0..24]");
}
if (period.fromMinute <0 || period.fromMinute > 59) {
throw new WeatherConfigurationException("Period 'fromMinute' must be in interval [0..59]");
}
if (period.toMinute <0 || period.toMinute > 60) {
throw new WeatherConfigurationException("Period 'toMinute' must be in interval [0..60]");
}
int from = period.fromHour*60+period.fromMinute;
int to = period.toHour*60 + period.toMinute;
if (from >= to) {
throw new WeatherConfigurationException("Period start time must be less then end");
}
double chanceSumm = 0;
foreach(WeatherEffect effect in period.effects) {
chanceSumm += effect.chance;
}
if (chanceSumm < 1) {
throw new WeatherConfigurationException("Sum of period effects chances summ must be 1");
}
}
}
/**
* Initialize behaviour
*/
void Start () {
Validate ();
random = new System.Random ();
Globals.Get().SetEvent += TimeEvent;
}
/**
* Get the latest period that time is less or equal to time argument
*/
WeatherPeriod GetPeriod(DateTime time) {
WeatherPeriod[] sortedPeriods = periods;
Array.Sort (sortedPeriods, delegate(WeatherPeriod p1, WeatherPeriod p2) {
return p1.fromHour > p2.fromHour ? -1 : 1;
});
foreach (WeatherPeriod period in sortedPeriods) {
if(activePeriod == null &&
(
(period.fromHour == time.Hour && period.fromMinute <= time.Minute) ||
(period.fromHour < time.Hour)
)
&&
(
(period.toHour > time.Hour) ||
(period.toHour == time.Hour && period.toMinute >= time.Minute)
)
) {
return period;
}
else if(period.fromHour == time.Hour && time.Minute == 0 && time.Second == 0) {
return period;
}
}
return null;
}
/**
* Get random effect from given period
*/
WeatherEffect GetEffect(WeatherPeriod period) {
WeatherEffect[] effects = period.effects;
//Sort periods by chance asc.
// e.g. [0.6, 0.2, 0.4] will be sorted to [0.2, 0.4, 0.6]
Array.Sort (effects, delegate(WeatherEffect e1, WeatherEffect e2) {
return e1.chance > e2.chance ? -1 : 1;
});
//Get number between 0.0 and 1.0
double number = random.Next () / (double)Int32.MaxValue;
double sum = 0;
foreach (WeatherEffect effect in effects) {
//compare number with chances.
sum += effect.chance;
if(number <= sum) {
return effect;
}
}
return null;
}
/**
* Set skybox textures
*/
void SetSkyboxTextures(Material newSkybox) {
if (RenderSettings.skybox == null) {
RenderSettings.skybox = new Material(newSkybox);
}
RenderSettings.skybox.shader = skyboxShader;
string[] textureNames = {"_FrontTex", "_BackTex", "_LeftTex", "_RightTex", "_UpTex", "_DownTex"};
foreach (string textureName in textureNames) {
//Move current textures to "second layer"
string newTextureName = textureName + "2";
Texture oldTexture = RenderSettings.skybox.GetTexture(textureName);
RenderSettings.skybox.SetTexture(newTextureName, oldTexture);
//Set new texture
RenderSettings.skybox.SetTexture(textureName, newSkybox.GetTexture(textureName));
}
}
/**
* Switch to selected period and effect
*/
void UseEffect(WeatherPeriod toPeriod, WeatherEffect toEffect) {
effectReady = false;
activePeriod = toPeriod;
if (activeEffect != null) {
previousAmbientSound = activeEffect.ambientSound;
}
activeEffect = toEffect;
previousAmbientColor = RenderSettings.ambientSkyColor;
SetSkyboxTextures (activeEffect.skybox);
if (activeEffect.ambientSound != null) {
activeEffect.ambientSound.volume = 0;
activeEffect.ambientSound.Play();
}
previousFallout = fallout;
if (activeEffect.falloutPrefab != null) {
fallout = (GameObject)Instantiate (activeEffect.falloutPrefab, bindFalloutToObject.transform.position, bindFalloutToObject.transform.rotation);
fallout.transform.parent = bindFalloutToObject.transform;
} else {
fallout = null;
}
}
/**
* Start switching to new effect
*/
void SelectNewEffect(DateTime time) {
WeatherPeriod period = GetPeriod (time);
if (period == null) {
return;
}
WeatherEffect effect = GetEffect (period);
if (effect == null) {
return;
}
switchStartedAt = time;
UseEffect (period, effect);
}
/**
* Get 'readiness' of new effect
*/
float GetEffectSwitchProportion(DateTime time) {
TimeSpan deltaTime = time - switchStartedAt;
float fromSwitch = deltaTime.Milliseconds/1000 + deltaTime.Seconds + deltaTime.Minutes * 60 + deltaTime.Hours * 3600;
float proportion = fromSwitch / activeEffect.switchTime;
return proportion;
}
/**
* Effect switch animation
*/
void SwithcEffectAnimation(DateTime time) {
float proportion = GetEffectSwitchProportion (time);
Color deltaColor = activeEffect.ambientColor - previousAmbientColor;
RenderSettings.ambientSkyColor += deltaColor * new Color (proportion, proportion, proportion);
if (previousAmbientSound != null) {
previousAmbientSound.volume = 1 - proportion;
}
if (activeEffect.ambientSound != null) {
activeEffect.ambientSound.volume = proportion;
}
if (previousFallout != null) {
previousFallout.GetComponent<Fallout>().instensity = 1 - proportion;
}
if (fallout != null) {
fallout.GetComponent<Fallout>().instensity = proportion;
}
RenderSettings.skybox.SetFloat ("_Blend", 1 - proportion);
if (proportion >= 1) {
effectReady = true;
if(previousAmbientSound != null) {
previousAmbientSound.Stop();
}
if (previousFallout!=null) {
Destroy(previousFallout);
previousFallout = null;
}
}
}
/**
* Game time update handler
*/
void TimeEvent(GlobalSetEventArgs e) {
if (e.key != "time") {
return;
}
DateTime time = (DateTime)e.value;
if (effectReady) {
SelectNewEffect (time);
} else {
SwithcEffectAnimation (time);
}
}
/**
* Unloading
*/
void OnDestroy() {
Globals.Get ().SetEvent -= TimeEvent;
}
}
using System.Collections;
using System;
/**
* Unity3D weather system.
* Needs "Skybox/Blended" (or compatible) shader to switch skyboxes
*/
public class Weather : MonoBehaviour {
[System.Serializable]
public class WeatherEffect {
public float switchTime;
public Material skybox;
public double chance;
public Color ambientColor;
public AudioSource ambientSound;
public GameObject falloutPrefab;
}
/**
* Daytime period
*/
[System.Serializable]
public class WeatherPeriod {
public int fromHour;
public int fromMinute;
public int toHour;
public int toMinute;
public WeatherEffect[] effects;
}
/**
* Fallout prefabs instances will be binded to it (prefferly - controller camera)
*/
public GameObject bindFalloutToObject;
/**
* Shader for skybox
*/
public Shader skyboxShader;
/**
* Daytime periods
*/
public WeatherPeriod[] periods;
/**
* Random number generator to switch effects
*/
System.Random random;
/**
* is effect ready, or we need to play effect "animations"
*/
bool effectReady = true;
/**
* Current period
*/
WeatherPeriod activePeriod = null;
/**
* Current effect
*/
WeatherEffect activeEffect = null;
/**
* Previous effect ambient color (to animated change to new)
*/
Color previousAmbientColor;
/**
* Previous effect ambient sound (to decrease volume)
*/
AudioSource previousAmbientSound = null;
/**
* Previous fallout effect (to change intensivity)
*/
GameObject previousFallout = null;
/**
* Effect switching started at
*/
DateTime switchStartedAt;
/**
* Current fallout effect
*/
GameObject fallout = null;
/**
* Validate weather periods/effects information
*/
void Validate() {
foreach (WeatherPeriod period in periods) {
if (period.fromHour <0 || period.fromHour > 23) {
throw new WeatherConfigurationException("Period 'fromHour' must be in interval [0..23]");
}
if (period.toHour <0 || period.toHour > 24) {
throw new WeatherConfigurationException("Period 'toHour' must be in interval [0..24]");
}
if (period.fromMinute <0 || period.fromMinute > 59) {
throw new WeatherConfigurationException("Period 'fromMinute' must be in interval [0..59]");
}
if (period.toMinute <0 || period.toMinute > 60) {
throw new WeatherConfigurationException("Period 'toMinute' must be in interval [0..60]");
}
int from = period.fromHour*60+period.fromMinute;
int to = period.toHour*60 + period.toMinute;
if (from >= to) {
throw new WeatherConfigurationException("Period start time must be less then end");
}
double chanceSumm = 0;
foreach(WeatherEffect effect in period.effects) {
chanceSumm += effect.chance;
}
if (chanceSumm < 1) {
throw new WeatherConfigurationException("Sum of period effects chances summ must be 1");
}
}
}
/**
* Initialize behaviour
*/
void Start () {
Validate ();
random = new System.Random ();
Globals.Get().SetEvent += TimeEvent;
}
/**
* Get the latest period that time is less or equal to time argument
*/
WeatherPeriod GetPeriod(DateTime time) {
WeatherPeriod[] sortedPeriods = periods;
Array.Sort (sortedPeriods, delegate(WeatherPeriod p1, WeatherPeriod p2) {
return p1.fromHour > p2.fromHour ? -1 : 1;
});
foreach (WeatherPeriod period in sortedPeriods) {
if(activePeriod == null &&
(
(period.fromHour == time.Hour && period.fromMinute <= time.Minute) ||
(period.fromHour < time.Hour)
)
&&
(
(period.toHour > time.Hour) ||
(period.toHour == time.Hour && period.toMinute >= time.Minute)
)
) {
return period;
}
else if(period.fromHour == time.Hour && time.Minute == 0 && time.Second == 0) {
return period;
}
}
return null;
}
/**
* Get random effect from given period
*/
WeatherEffect GetEffect(WeatherPeriod period) {
WeatherEffect[] effects = period.effects;
//Sort periods by chance asc.
// e.g. [0.6, 0.2, 0.4] will be sorted to [0.2, 0.4, 0.6]
Array.Sort (effects, delegate(WeatherEffect e1, WeatherEffect e2) {
return e1.chance > e2.chance ? -1 : 1;
});
//Get number between 0.0 and 1.0
double number = random.Next () / (double)Int32.MaxValue;
double sum = 0;
foreach (WeatherEffect effect in effects) {
//compare number with chances.
sum += effect.chance;
if(number <= sum) {
return effect;
}
}
return null;
}
/**
* Set skybox textures
*/
void SetSkyboxTextures(Material newSkybox) {
if (RenderSettings.skybox == null) {
RenderSettings.skybox = new Material(newSkybox);
}
RenderSettings.skybox.shader = skyboxShader;
string[] textureNames = {"_FrontTex", "_BackTex", "_LeftTex", "_RightTex", "_UpTex", "_DownTex"};
foreach (string textureName in textureNames) {
//Move current textures to "second layer"
string newTextureName = textureName + "2";
Texture oldTexture = RenderSettings.skybox.GetTexture(textureName);
RenderSettings.skybox.SetTexture(newTextureName, oldTexture);
//Set new texture
RenderSettings.skybox.SetTexture(textureName, newSkybox.GetTexture(textureName));
}
}
/**
* Switch to selected period and effect
*/
void UseEffect(WeatherPeriod toPeriod, WeatherEffect toEffect) {
effectReady = false;
activePeriod = toPeriod;
if (activeEffect != null) {
previousAmbientSound = activeEffect.ambientSound;
}
activeEffect = toEffect;
previousAmbientColor = RenderSettings.ambientSkyColor;
SetSkyboxTextures (activeEffect.skybox);
if (activeEffect.ambientSound != null) {
activeEffect.ambientSound.volume = 0;
activeEffect.ambientSound.Play();
}
previousFallout = fallout;
if (activeEffect.falloutPrefab != null) {
fallout = (GameObject)Instantiate (activeEffect.falloutPrefab, bindFalloutToObject.transform.position, bindFalloutToObject.transform.rotation);
fallout.transform.parent = bindFalloutToObject.transform;
} else {
fallout = null;
}
}
/**
* Start switching to new effect
*/
void SelectNewEffect(DateTime time) {
WeatherPeriod period = GetPeriod (time);
if (period == null) {
return;
}
WeatherEffect effect = GetEffect (period);
if (effect == null) {
return;
}
switchStartedAt = time;
UseEffect (period, effect);
}
/**
* Get 'readiness' of new effect
*/
float GetEffectSwitchProportion(DateTime time) {
TimeSpan deltaTime = time - switchStartedAt;
float fromSwitch = deltaTime.Milliseconds/1000 + deltaTime.Seconds + deltaTime.Minutes * 60 + deltaTime.Hours * 3600;
float proportion = fromSwitch / activeEffect.switchTime;
return proportion;
}
/**
* Effect switch animation
*/
void SwithcEffectAnimation(DateTime time) {
float proportion = GetEffectSwitchProportion (time);
Color deltaColor = activeEffect.ambientColor - previousAmbientColor;
RenderSettings.ambientSkyColor += deltaColor * new Color (proportion, proportion, proportion);
if (previousAmbientSound != null) {
previousAmbientSound.volume = 1 - proportion;
}
if (activeEffect.ambientSound != null) {
activeEffect.ambientSound.volume = proportion;
}
if (previousFallout != null) {
previousFallout.GetComponent<Fallout>().instensity = 1 - proportion;
}
if (fallout != null) {
fallout.GetComponent<Fallout>().instensity = proportion;
}
RenderSettings.skybox.SetFloat ("_Blend", 1 - proportion);
if (proportion >= 1) {
effectReady = true;
if(previousAmbientSound != null) {
previousAmbientSound.Stop();
}
if (previousFallout!=null) {
Destroy(previousFallout);
previousFallout = null;
}
}
}
/**
* Game time update handler
*/
void TimeEvent(GlobalSetEventArgs e) {
if (e.key != "time") {
return;
}
DateTime time = (DateTime)e.value;
if (effectReady) {
SelectNewEffect (time);
} else {
SwithcEffectAnimation (time);
}
}
/**
* Unloading
*/
void OnDestroy() {
Globals.Get ().SetEvent -= TimeEvent;
}
}
bindFalloutToObject - указывает объект, к которому прицепятся prefab-ы с осадками. Например - контроллер персонажа.
skyboxShader - указывает шейдер, используемый для скайбокса. В моём случае - "SkyboxBlended", но от шейдера требуется только совместимость по названиям текстур и свойству Blend.
periods - собственно, периоды. Содержат :
- fromHour, fromMinute, toHour, toMinute - используются при выборе нового эффекта со сменой времени.
- switchTime - время переключения эффектов (в течение которого наблюдаем затухание прошлого звука/нарастание нового, изменени скайбокса, смену осадков).
- chance - шанс выпадения этого эффекта. Сумма всех шансов для интервала - 1.
- ambientColor - цвет фонового освещения
- skybox - собственно, скайбокс.
- ambientSound - источник звука, воспроизводимого, пока эффект активен.
- falloutPrefab - префаб осадков