Article: Vacant Skies – Action RPG Tutorial Series
Part 4
In this tutorial we will be going over some effects to give our game a little eye candy. We will go over how to implement a Day/Night cycle, add lighting to our engine, and then how to create shadow casters to add in some dynamic shadowing.
So to start we will list some of the new resources we will be using in this tutorial. A good chunk of this tutorial will be referencing an amazing tutorial on how to create 2D lights and shadows.
So let’s begin with setting up our Day/Night cycle. First we will be adding a new class to our Library called WorldTime. This is basically a custom Time class for our game.
[csharp]
public class WorldTime
{
#region Fields
private double elapsed = 0;
#endregion
#region Properties
public int Year {get; set;}
public int Month {get; set;}
public int Day {get; set;}
public int Hour { get; set; }
public int Minute { get; set; }
public float Interval { get; set; }
public bool IsDay
{
get
{
return (Hour >= Sunrise && Hour < Sunset);
}
}
public bool IsNight
{
get
{
return ((Hour >= 0 && Hour < Sunrise) || (Hour >= Sunset));
}
}
public int Sunrise { get; set; }
public int Sunset { get; set; }
#endregion
#region Constructor
public WorldTime()
{
Year = 1;
Month = 1;
Day = 1;
Hour = 0;
Minute = 0;
Interval = 50f;
Sunrise = 6;
Sunset = 18;
}
public WorldTime(int month, int day, int year, int hour, int minute)
{
Year = year;
Month = month;
Day = day;
Hour = hour;
Minute = minute;
Interval = 50f;
Sunrise = 6;
Sunset = 18;
}
#endregion
#region Methods
public override string ToString()
{
return string.Format("Date: {0}//{1}//{2:0000} | Time: {3:00}:{4:00} | {5}", Month, Day, Year, Hour, Minute, IsDay ? "Day Time" : "Night Time");
}
public void Update(GameTime gameTime)
{
elapsed += gameTime.ElapsedGameTime.TotalMilliseconds;
if (elapsed >= Interval)
{
elapsed = 0;
Minute++;
if (Minute >= 60)
{
Hour++;
Minute = 0;
}
if (Hour >= 24)
{
Day++;
Hour = 0;
}
if (Day > 30)
{
Month++;
Day = 1;
}
if (Month > 12)
{
Year++;
Month = 1;
}
}
}
#endregion
}
[/csharp]
It holds enough information so that we can have a world where time can elapse with specified days and nights. Everything can be adjusted also, so we can customize it to suit our needs. So next we need to implement it in our World class.
[csharp]
public static WorldTime Time { get; set; }
[/csharp]
Then we need it to update within our game when the game is active (not paused). So we will add the update to our Update method in our GameplayScreen.
[csharp]
World.Time.Update(gameTime);
[/csharp]
Now let’s add a line to our Draw method to display the Date, Time, and whether its day or night.
[csharp]
// Draw UI
spriteBatch.Begin();
Vector2 timeSize = ScreenManager.Font.MeasureString(World.Time.ToString()) / 2;
Vector2 timePosition = new Vector2((ScreenManager.Game.GraphicsDevice.Viewport.Width / 2) - (timeSize.X / 2), 0f);
spriteBatch.DrawString(ScreenManager.Font, World.Time.ToString(), timePosition, Color.Yellow, 0f, Vector2.Zero, 0.5f, SpriteEffects.None, 0f);
spriteBatch.End();
[/csharp]
One last thing we need to do before we run our game, is initialize it in our LoadContent. We only want it to initialize when the gameplay screen is first loaded and not each time a map loads, so it has to go in the LoadContent method instead of the LoadMap method.
[csharp]
World.Time = new WorldTime(1,1,1,6,0);
[/csharp]
So here we set our time to be the first day of the first month of the first year, and its 6 in the morning. Also since it is set for 6 in the morning, it will be daytime (day is set by default to 6 am to 6 pm).
So now when we run our game, we will see a nice display at the top of the screen showing our date and time.
So now that we have our data for our Day/Night cycle ready, let’s add in some lighting to make the day to night transitions more dramatic.
We are going to implement two different lighting systems, one for just standard lighting without shadows and another for full lighting and shadows. The reason behind this is to show two different methods of doing lighting, one does everything on the CPU, and the other does it all on the GPU.
CPU Lighting
So first we are going to implement the CPU lighting. So add a new folder in our library called Lights and add a new class called Light. This will be what stores all our information about our lights.
[csharp]
public class Light
{
#region Static
public static Texture2D Texture { get; set; }
#endregion
#region Fields
#endregion
#region Properties
public Vector2 Position {get; set;}
public float Range {get; set;}
public Color Color {get; set;}
public int Index { get; set; }
public Vector2 Center
{
get
{
return new Vector2(Texture.Width / 2, Texture.Height / 2);
}
}
public float Scale
{
get
{
return Range / ((float)Texture.Width / 2.0f);
}
}
#endregion
#region Constructor
public Light(Vector2 position, float range, Color color)
{
Color = color;
Range = range;
Position = position;
}
#endregion
#region Methods
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(Texture, Position, null, Color, 0, Center, Scale, SpriteEffects.None, 1);
}
#endregion
}
[/csharp]
As you can see this is pretty straight forward, it holds the data to render our light and a single method used to draw it.
Also before we get too much further, if you take a look at the source download for this tutorial, you will notice that our World class has a lot more in it now. I moved most of our drawing methods and updates to the World class since all our data for our world are stored there also. This allows use of our World data and rendering throughout our game.
Next we are going to add another class to our Lights library called LightMap. The light map will store our list of lights and render the lights to a texture, and then draw it to the screen for us.
[csharp]
public class LightMap
{
#region Static
public static Texture2D ClearTexture { get; set; }
#endregion
#region Fields
private RenderTarget2D renderTarget;
private SpriteBatch spriteBatch;
#endregion
#region Properties
public GraphicsDevice GraphicsDevice;
public Color AmbientLight { get; set; }
public List<Light> Lights { get; set; }
public Light this[int index]
{
get
{
return Lights[index];
}
}
public Texture2D Texture
{
get
{
return renderTarget.GetTexture();
}
}
#endregion
#region Constructor
public LightMap(GraphicsDevice graphicsDevice)
{
GraphicsDevice = graphicsDevice;
PresentationParameters pp = GraphicsDevice.PresentationParameters;
renderTarget = new RenderTarget2D(GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, 1, SurfaceFormat.Color, pp.MultiSampleType, pp.MultiSampleQuality);
spriteBatch = new SpriteBatch(GraphicsDevice);
AmbientLight = new Color(35, 35, 35);
Lights = new List<Light>();
}
public LightMap(GraphicsDevice graphicsDevice, Color ambientLight)
{
GraphicsDevice = graphicsDevice;
PresentationParameters pp = GraphicsDevice.PresentationParameters;
renderTarget = new RenderTarget2D(GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, 1, SurfaceFormat.Color, pp.MultiSampleType, pp.MultiSampleQuality);
spriteBatch = new SpriteBatch(GraphicsDevice);
AmbientLight = ambientLight;
Lights = new List<Light>();
}
#endregion
#region Methods
private void ClearAlphaToOne()
{
GraphicsDevice.RenderState.ColorWriteChannels = ColorWriteChannels.Alpha;
spriteBatch.Begin(SpriteBlendMode.None);
spriteBatch.Draw(ClearTexture, new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height), Color.White);
spriteBatch.End();
GraphicsDevice.RenderState.ColorWriteChannels = ColorWriteChannels.All;
}
public void Render()
{
GraphicsDevice.SetRenderTarget(0, renderTarget);
GraphicsDevice.Clear(AmbientLight);
foreach (Light light in Lights)
{
ClearAlphaToOne();
GraphicsDevice.RenderState.ColorWriteChannels = ColorWriteChannels.All;
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState, Camera.World);
GraphicsDevice.RenderState.DestinationBlend = Blend.One;
GraphicsDevice.RenderState.SourceBlend = Blend.DestinationAlpha;
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = true;
light.Draw(spriteBatch);
spriteBatch.End();
}
ClearAlphaToOne();
GraphicsDevice.SetRenderTarget(0, null);
}
public void Draw()
{
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
GraphicsDevice.RenderState.SourceBlend = Blend.Zero;
GraphicsDevice.RenderState.DestinationBlend = Blend.SourceColor;
GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
spriteBatch.Draw(Texture, Vector2.Zero, Color.White);
spriteBatch.End();
}
public void Clear()
{
Lights.Clear();
Lights = new List<Light>();
}
public Light AddLight(Vector2 position, float range, Color color)
{
Light light = new Light(position, range, color);
Lights.Add(light);
return light;
}
public void RemoveLight(int index)
{
Lights.RemoveAt(index);
}
public bool RemoveLight(Light light)
{
return Lights.Remove(light);
}
#endregion
}
[/csharp]
That’s all there is to the CPU lighting system, besides the rendering and drawing to the screen, but we will implement that with our GPU lighting. So now let’s add our GPU lighting.
GPU Lighting
Add another folder in our library called DarkLight. This is a nice library that renders lights and shadows on the GPU using shaders. First we need to add a class called DarkLight_Light. This will be a data class specific to the DarkLight renderer.
[csharp]
public abstract class DarkLight_Light
{
#region Fields
private static VertexDeclaration _vertDecl = null;
protected internal VertexPositionColorTexture[] _vertices;
#endregion
#region Properties
public abstract Vector2 Position { get; set; }
public abstract Color Color { get; set; }
public abstract float Intensity { get; set; }
public abstract Vector2 WorldPosition { get; set; }
public int Index { get; set; }
public bool Updated { get; set; }
public bool Drawn { get; set; }
public Rectangle Bounds { get; set; }
#endregion
#region Methods
internal void Draw(GraphicsDevice graphicsDevice)
{
if (Bounds.Intersects(Camera.VisibleArea))
{
if (_vertDecl == null) _vertDecl = new VertexDeclaration(graphicsDevice, VertexPositionColorTexture.VertexElements);
graphicsDevice.VertexDeclaration = _vertDecl;
graphicsDevice.DrawUserPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleFan, _vertices, 0, _vertices.Length - 2);
Drawn = true;
}
else
{
Drawn = false;
}
}
#endregion
}
[/csharp]
As you can see this is an abstract class and we will add in some more light types to make our lighting system more robust. So we will add in three new classes in our library called PointLight, SkyLight, and SpotLight. A Point Light will render a single light that radiates from a specific point. A Sky Light is a light that will radiates in a direction specified by an axis, and the Spot Light will create a light that radiates from one point to another.
PointLight
[csharp]
public class PointLight : DarkLight_Light
{
#region Fields
private float _radius;
#endregion
#region Properties
public override float Intensity { get; set; }
public override Vector2 Position
{
get
{
return new Vector2(_vertices[0].Position.X, _vertices[0].Position.Y);
}
set
{
_vertices[0].Position = new Vector3(value.X - Camera.Position.X, value.Y - Camera.Position.Y, 0);
for (int i = 0; i < _vertices.Length; i++)
{
_vertices[i].Position = _vertices[0].Position;
}
Bounds = new Rectangle((int)(WorldPosition.X - Radius), (int)(WorldPosition.Y - Radius), (int)(Radius * 2f), (int)(Radius * 2f));
}
}
public override Vector2 WorldPosition { get; set; }
public override Color Color
{
get
{
return _vertices[0].Color;
}
set
{
_vertices[0].Color = value;
}
}
public float Radius
{
get { return _radius; }
set
{
if (_radius != value)
{
_radius = value;
CalculatePosition(new Vector2(_vertices[0].Position.X, _vertices[0].Position.Y), _radius);
}
}
}
#endregion
#region Constructor
public PointLight(Vector2 position, Color color, float radius)
{
_vertices = new VertexPositionColorTexture[33];
CalculatePosition(position, radius);
Color = color;
_radius = radius;
Intensity = 1f;
}
public PointLight(Vector2 position, Color color, float radius, float intensity)
{
_vertices = new VertexPositionColorTexture[33];
WorldPosition = position;
CalculatePosition(position, radius);
Color = color;
_radius = radius;
Intensity = intensity;
}
protected PointLight()
{
}
#endregion
#region Methods
protected virtual void CalculatePosition(Vector2 centre, float size)
{
float interval = MathHelper.TwoPi / (_vertices.Length - 2);
CalculatePosition(centre, size, interval, 0.0f);
}
protected void CalculatePosition(Vector2 centre, float size, float interval, float angleOffset)
{
_vertices[0].Position = new Vector3(centre, 0);
for (int i = 0; i < (_vertices.Length - 1); i++)
{
_vertices[i + 1].Position = _vertices[0].Position;
_vertices[i + 1].TextureCoordinate = new Vector2((float)Math.Sin(interval * -i + angleOffset) * size, (float)Math.Cos(interval * -i + angleOffset) * size);
}
}
#endregion
}
[/csharp]
SkyLight
[csharp]
public class SkyLight : DarkLight_Light
{
#region Fields
private Vector2 _position;
private Vector2 _axis;
private float _width;
#endregion
#region Properties
public override Vector2 Position
{
get { return _position; }
set
{
_position = value;
GenerateVertices();
Bounds = new Rectangle((int)(WorldPosition.X - Axis.X), (int)(WorldPosition.Y - Axis.Y), (int)(Axis.X * 2f), (int)(Axis.Y * 2f));
}
}
public override Color Color
{
get { return _vertices[0].Color; }
set
{
_vertices[0].Color = value;
_vertices[1].Color = value;
}
}
public override float Intensity { get; set; }
public override Vector2 WorldPosition { get; set; }
public Vector2 Axis
{
get { return _axis; }
set { _axis = value; GenerateVertices(); }
}
public float Width
{
get { return _width; }
set { _width = value; GenerateVertices(); }
}
#endregion
#region Constructor
public SkyLight(Vector2 position, Vector2 axis, float width, Color color)
{
_vertices = new VertexPositionColorTexture[4];
_position = position;
_width = width;
_axis = axis;
GenerateVertices();
Color = color;
Intensity = 1f;
}
#endregion
#region Methods
private void GenerateVertices()
{
_axis.Normalize();
Vector2 normal = new Vector2(_axis.Y, _axis.X);
float halfWidth = _width * 0.5f;
Vector2 left = _position - normal * halfWidth;
Vector2 right = _position + normal * halfWidth;
Vector2 axis = _axis * _width;
_vertices[0].Position = _vertices[3].Position = new Vector3(left, 0);
_vertices[1].Position = _vertices[2].Position = new Vector3(right, 0);
_vertices[2].TextureCoordinate = _vertices[3].TextureCoordinate = axis;
}
#endregion
}
[/csharp]
SpotLight
[csharp]
public class SpotLight : PointLight
{
#region Fields
private float _width, _direction;
#endregion
#region Properties
public float Width
{
get { return _width; }
set { _width = value; CalculatePosition(Position, Radius); }
}
public override Vector2 Position
{
get
{
return base.Position;
}
set
{
base.Position = value;
Bounds = new Rectangle((int)(WorldPosition.X - Radius), (int)(WorldPosition.Y - Radius), (int)(Radius * 2f), (int)(Radius * 2f));
}
}
public float Direction
{
get { return _direction; }
set
{
Matrix transform = Matrix.CreateRotationZ(_direction - value);
for (int i = 1; i < _vertices.Length; i++)
Vector3.Transform(ref _vertices[i].Position, ref transform, out _vertices[i].Position);
_direction = value;
}
}
#endregion
#region Constructor
public SpotLight(Vector2 position, Color color, float radius, float width, float direction)
{
_vertices = new VertexPositionColorTexture[16];
_width = width;
_direction = direction;
CalculatePosition(position, radius);
Color = color;
Intensity = 1f;
}
#endregion
#region Methods
protected override void CalculatePosition(Vector2 centre, float size)
{
float interval = _width / (_vertices.Length - 2);
CalculatePosition(centre, size, interval, _direction);
}
#endregion
}
[/csharp]
So now that we have our lights ready, we need to add the DarkLightRenderer, but in order to do that, we need to add in the ShaderUtility class. So add a new class called ShaderUtility. The ShaderUtility is a class used to make some shader methods available throughout our library.
[csharp]
public static class ShaderUtility
{
#region Shader Recompilation
#if WINDOWS
public struct Shader
{
public Shader(FileSystemWatcher fsw, DateTime lastAccess, Effect effect, string path, Action<Effect> recompiledEffect)
{
this.fsw = fsw;
this.lastAccess = lastAccess;
this.effect = effect;
this.path = path;
this.recompiledEffect = recompiledEffect;
}
public FileSystemWatcher fsw;
public DateTime lastAccess;
public Effect effect;
public string path;
public Action<Effect> recompiledEffect;
}
private static Dictionary<string, Shader> _shaderList = new Dictionary<string, Shader>();
private static GraphicsDevice GraphicsDevice;
#endif
public static void RegisterForRecompile(string path, Effect effect, GraphicsDevice device, Action<Effect> recompiledEvent)
{
#if WINDOWS
FileSystemWatcher fsw = new FileSystemWatcher(Path.GetDirectoryName(path));
fsw.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.LastAccess;
fsw.Changed += new FileSystemEventHandler(fsw_Changed);
fsw.EnableRaisingEvents = true;
_shaderList.Add(path, new Shader(fsw, new DateTime(0), effect, path, recompiledEvent));
GraphicsDevice = device;
#endif
}
#if WINDOWS
private static void fsw_Changed(object sender, FileSystemEventArgs e)
{
if (!_shaderList.ContainsKey(e.FullPath)) return;
if (e.ChangeType != WatcherChangeTypes.Changed) return;
Shader s = _shaderList[e.FullPath];
String path = e.FullPath;
DateTime lastAccess = s.lastAccess;
DateTime time = File.GetLastWriteTime(path);
if (time.Subtract(s.lastAccess).Seconds > 1)
{
s.lastAccess = time;
for (int tries = 3; tries > 0; tries--)
{
if (compile(ref s)) break;
}
Console.WriteLine(String.Format("Recompiled: {0} : {1}", Path.GetFileName(path), s.lastAccess));
_shaderList[e.FullPath] = s;
}
}
private static bool compile(ref Shader s)
{
lock (s.effect)
{
try
{
using (FileStream stream = File.Open(s.path, FileMode.Open))
{
CompiledEffect newEffect = Effect.CompileEffectFromFile(stream, null, null, CompilerOptions.None, TargetPlatform.Windows);
if (newEffect.Success)
{
s.effect = new Effect(GraphicsDevice, newEffect.GetEffectCode(), CompilerOptions.None, new EffectPool());
s.recompiledEffect(s.effect);
}
else
{
Console.WriteLine(newEffect.ErrorsAndWarnings);
//MessageBox.Show(newEffect.ErrorsAndWarnings);
}
}
}
catch (IOException)
{
return false;
}
}
return true;
}
#endif
#endregion
#region Fullscreen Quad
private static bool _loadedResources = false;
private static VertexPositionTexture[] _vertices = null;
private static VertexDeclaration _vertDecl = null;
/// <summary>
/// Draws a fullscreen quad polygon to the screen.
/// </summary>
/// <param name="device">Graphics device to draw with</param>
public static void DrawFullscreenQuad(GraphicsDevice device)
{
if (!_loadedResources)
{
_vertices = new VertexPositionTexture[]
{
new VertexPositionTexture(new Vector3(-1,-1,0), new Vector2(0,1)),
new VertexPositionTexture(new Vector3(-1, 1,0), new Vector2(0,0)),
new VertexPositionTexture(new Vector3( 1, 1,0), new Vector2(1,0)),
new VertexPositionTexture(new Vector3( 1,-1,0), new Vector2(1,1))
};
_vertDecl = new VertexDeclaration(device, VertexPositionTexture.VertexElements);
}
device.VertexDeclaration = _vertDecl;
device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleFan, _vertices, 0, 2);
}
#endregion
public static void CalculateGaussian(Vector2 direction, float gaussianAmount, int numSamples, out Vector2[] offset, out float[] weight)
{
//Create offsets along the direction
offset = new Vector2[numSamples];
for (int i = 0; i < numSamples; i++)
{
float amount = i - (numSamples / 2);
offset[i] = new Vector2(direction.X * amount, direction.Y * amount);
}
//Calculate gaussian for each offset
weight = new float[numSamples];
float total = 0f;
for (int i = 0; i < numSamples; i++)
{
weight[i] = GaussianEquation(direction.Length(), gaussianAmount);
total += weight[i];
}
//Normalize the weight so E(weight 0..numSamples) = 1.0
for (int i = 0; i < numSamples; i++)
weight[i] /= total;
}
private static float GaussianEquation(float x, float gaussianAmount)
{
double top = Math.Exp(-((x * x) / (2 * gaussianAmount * gaussianAmount)));
double bottom = Math.Sqrt(2 * Math.PI * gaussianAmount * gaussianAmount);
return (float)(top / bottom);
}
}
[/csharp]
Now we need to add in one more class called DarkLightRenderer. This will handle all our renderering and drawing of our lights and shadows.
[csharp]
public class DarkLightRenderer
{
#region Fields
private Color _AmbientLight = Color.White;
private GraphicsDevice _graphicsDevice;
private Effect _shadowEffect;
private EffectParameter _shadowEffectChannelMask;
private enum Passes : int { ShadowShader, LightShader, FullscreenPass };
private int _lastPass = -1;
private List<DarkLight_Light> _lightList = new List<DarkLight_Light>();
private Action _drawSceneOccluders, _drawBackground, _drawForeground;
private static bool _createdResources = false;
private static RenderTarget2D _occluderBuffer, _shadowBuffer, _lightBuffer, _lightMapBuffer;
#endregion
#region Properties
public int LightsDrawn { get; set; }
public Color AmbientColor { get; set; }
public Color BackgroundColor { get; set; }
public Texture2D LightBuffer
{
get
{
return _lightBuffer.GetTexture();
}
}
public Texture2D LightMap { get; set; }
public Color AmbientLight
{
get
{
return _AmbientLight;
}
set
{
_AmbientLight = value;
_shadowEffect.Parameters["AmbientLight"].SetValue(new Vector4(value.R, value.G, value.B, value.A));
}
}
#endregion
#region Constructor
public DarkLightRenderer(GraphicsDevice graphicsDevice, Effect shadowEffect, Action drawSceneOccluders, Action drawBackground, Action drawForeground)
{
AmbientColor = Color.Black;
BackgroundColor = Color.Black;
_graphicsDevice = graphicsDevice;
_shadowEffect = shadowEffect;
_shadowEffectChannelMask = _shadowEffect.Parameters["ChannelMask"];
_drawSceneOccluders = drawSceneOccluders;
_drawBackground = drawBackground;
_drawForeground = drawForeground;
if (!_createdResources)
{
int width = graphicsDevice.PresentationParameters.BackBufferWidth;
int height = graphicsDevice.PresentationParameters.BackBufferHeight;
MultiSampleType type = graphicsDevice.PresentationParameters.MultiSampleType;
int quality = graphicsDevice.PresentationParameters.MultiSampleQuality;
_occluderBuffer = new RenderTarget2D(graphicsDevice, width, height, 1, SurfaceFormat.Color, type, quality, RenderTargetUsage.DiscardContents);
_shadowBuffer = new RenderTarget2D(graphicsDevice, width, height, 1, SurfaceFormat.Color, type, quality, RenderTargetUsage.DiscardContents);
_lightBuffer = new RenderTarget2D(graphicsDevice, width, height, 1, SurfaceFormat.Color, type, quality, RenderTargetUsage.PreserveContents);
_lightMapBuffer = new RenderTarget2D(graphicsDevice, width, height, 1, SurfaceFormat.Color, type, quality, RenderTargetUsage.PreserveContents);
_createdResources = true;
}
_shadowEffect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0f, graphicsDevice.PresentationParameters.BackBufferWidth, graphicsDevice.PresentationParameters.BackBufferHeight, 0f, -100f, 100f));
}
#endregion
#region Methods
#region Light List Interface
public DarkLight_Light this[int index]
{
get { return _lightList[index]; }
set { _lightList[index] = value; }
}
public int Count
{
get { return _lightList.Count; }
}
public void Add(DarkLight_Light light)
{
light.Index = Count;
_lightList.Add(light);
}
public bool Remove(DarkLight_Light light)
{
return _lightList.Remove(light);
}
public void Clear()
{
_lightList.Clear();
}
#endregion
private void Begin(Passes pass)
{
_shadowEffect.Begin();
_shadowEffect.CurrentTechnique.Passes[(int)pass].Begin();
_lastPass = (int)pass;
}
private void End()
{
_shadowEffect.CurrentTechnique.Passes[_lastPass].End();
_shadowEffect.End();
}
public void Draw(SpriteBatch spriteBatch)
{
LightsDrawn = 0;
_graphicsDevice.SetRenderTarget(0, _lightMapBuffer);
_graphicsDevice.Clear(AmbientColor);
_graphicsDevice.SetRenderTarget(0, _lightBuffer);
_graphicsDevice.Clear(AmbientColor);
_graphicsDevice.SetRenderTarget(0, _occluderBuffer);
_graphicsDevice.Clear(BackgroundColor);
if (_drawSceneOccluders != null) _drawSceneOccluders();
int count = (int)Math.Ceiling((float)_lightList.Count / 6.0f) * 6;
// Light Map Pass
for (int i = 0; i < count; i += 6)
{
int lightsInPass = Math.Min(i + 6, _lightList.Count) - i;
_graphicsDevice.SetRenderTarget(0, _shadowBuffer);
_graphicsDevice.Textures[0] = _occluderBuffer.GetTexture();
_graphicsDevice.Clear(AmbientColor);
Begin(Passes.ShadowShader);
for (int j = 0; j < lightsInPass; j++)
{
_shadowEffectChannelMask.SetValue(new Vector3(j == 0 ? 1 : 0, j == 1 ? 1 : 0, j == 2 ? 1 : 0));
_shadowEffect.CommitChanges();
}
End();
_graphicsDevice.RenderState.ColorWriteChannels = ColorWriteChannels.All;
_graphicsDevice.SetRenderTarget(0, _lightMapBuffer);
_graphicsDevice.Textures[1] = _shadowBuffer.GetTexture();
Begin(Passes.LightShader);
for (int j = 0; j < lightsInPass; j++)
{
DarkLight_Light light = _lightList[i + j];
_shadowEffectChannelMask.SetValue(new Vector3(j == 0 ? 1 : 0, j == 1 ? 1 : 0, j == 2 ? 1 : 0));
_shadowEffect.Parameters["Intensity"].SetValue(light.Intensity);
_shadowEffect.CommitChanges();
light.Draw(_graphicsDevice);
if (light.Drawn) LightsDrawn++;
}
End();
}
_graphicsDevice.SetRenderTarget(0, null);
LightMap = _lightMapBuffer.GetTexture();
// Shadow Pass
for (int i = 0; i < count; i += 6)
{
int lightsInPass = Math.Min(i + 6, _lightList.Count) - i;
_graphicsDevice.SetRenderTarget(0, _shadowBuffer);
_graphicsDevice.Textures[0] = _occluderBuffer.GetTexture();
_graphicsDevice.Clear(AmbientColor);
Begin(Passes.ShadowShader);
for (int j = 0; j < lightsInPass; j++)
{
DarkLight_Light light = _lightList[i + j];
_shadowEffectChannelMask.SetValue(new Vector3(j == 0 ? 1 : 0, j == 1 ? 1 : 0, j == 2 ? 1 : 0));
_shadowEffect.Parameters["Intensity"].SetValue(light.Intensity);
_shadowEffect.CommitChanges();
light.Draw(_graphicsDevice);
if (light.Drawn) LightsDrawn++;
}
End();
_graphicsDevice.RenderState.ColorWriteChannels = ColorWriteChannels.All;
_graphicsDevice.SetRenderTarget(0, _lightBuffer);
_graphicsDevice.Textures[1] = _shadowBuffer.GetTexture();
Begin(Passes.LightShader);
for (int j = 0; j < lightsInPass; j++)
{
DarkLight_Light light = _lightList[i + j];
_shadowEffectChannelMask.SetValue(new Vector3(j == 0 ? 1 : 0, j == 1 ? 1 : 0, j == 2 ? 1 : 0));
_shadowEffect.Parameters["Intensity"].SetValue(light.Intensity);
_shadowEffect.CommitChanges();
light.Draw(_graphicsDevice);
if (light.Drawn) LightsDrawn++;
}
End();
}
if (_drawBackground != null) _drawBackground();
if (_drawForeground != null) _drawForeground();
}
#endregion
}
[/csharp]
So now we have both of our lighting system data and logic implemented, so let’s add this to our world.
So back in our World class add in the following variables.
[csharp]
// Components
public static DarkLightRenderer DarkLight { get; set; }
// DarkLight objects
private static RenderTarget2D GroundBuffer { get; set; }
private static RenderTarget2D OverlayBuffer { get; set; }
private static RenderTarget2D ColorBuffer { get; set; }
private static Texture2D LastFrame { get; set; }
// LightMap Objects
private static LightMap LightMap { get; set; }
public static Color AmbientLight
{
get
{
return LightMap.AmbientLight;
}
set
{
LightMap.AmbientLight = value;
}
}
[/csharp]
Now we need to add in our initializations to our map loading. We will do it in the map loading section, to ensure our lights are specific to the map we are on. So first on the list to be modified is our World.LoadMap() method.
[csharp]
public static void LoadMap(string assetName)
{
Portals = new List<Portal>();
ClipMap = new Dictionary<Vector2, Rectangle>();
Monsters = new List<Monster>();
Map = Content.Load<Map>(assetName);
Camera.Initialize(GraphicsDevice, new Vector2(World.Map.Width, World.Map.Height), new Vector2(World.Map.TileWidth, World.Map.TileHeight), new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height));
LoadMapProperties();
LoadDarkLight();
MapObjectLayer objects = World.Map.GetLayer("Objects") as MapObjectLayer;
foreach (MapObject obj in objects.Objects)
{
switch (obj.Name)
{
case "PlayerStart":
if (Player == null)
{
Player = new Player(new Vector2(obj.Location.X + (obj.Location.Width / 2), obj.Location.Y), new Vector2(138f, 192f), 6);
Player.LoadContent(Content, "Textures\Characters\Warrior1WalkLeft");
Player.Map = World.Map;
}
else
{
Player.Position = new Vector2(obj.Location.X + (obj.Location.Width / 2), obj.Location.Y);
}
break;
case "Portal":
Portal portal = new Portal();
portal.Position = new Vector2(obj.Location.X, obj.Location.Y);
portal.Size = new Vector2(obj.Location.Width, obj.Location.Height);
portal.DestinationMap = obj.Properties["DestinationMap"].Value;
string[] tileLoc = obj.Properties["DestinationTileLocation"].Value.Split(',');
portal.DestinationTileLocation = new Vector2(Convert.ToInt32(tileLoc[0]), Convert.ToInt32(tileLoc[1]));
World.Portals.Add(portal);
break;
}
}
if (Player == null) Player = new Player(Vector2.Zero, new Vector2(138f, 192f), 6);
Player.UpdateBounds(World.Map.TileWidth, World.Map.TileHeight);
Camera.FocusOn(World.Player);
LoadClipMap();
LoadMonsters();
LoadLights();
}
[/csharp]
Next we need to add a new method called LoadLights to load the data from our maps about the lighting.
[csharp]
private static void LoadLights()
{
DarkLight.Clear();
Light.Texture = Content.Load<Texture2D>("Textures\light");
LightMap.ClearTexture = Content.Load<Texture2D>("Textures\AlphaOne");
LightMap = new LightMap(GraphicsDevice);
if (Map.LayerExists("Lights"))
{
MapObjectLayer lightLayer = Map.GetLayer("Lights") as MapObjectLayer;
foreach (MapObject oLight in lightLayer.Objects)
{
string type = (oLight.Properties["Type"] != null ? oLight.Properties["Type"].Value : "Point");
Vector2 position = new Vector2(oLight.Location.X + (oLight.Location.Width / 2), oLight.Location.Y + (oLight.Location.Height / 2));
Color color = (oLight.Properties["Color"] != null ? oLight.Properties["Color"].ToColor() : Color.White);
float radius = (oLight.Properties["Radius"] != null ? Convert.ToSingle(oLight.Properties["Radius"].Value) : (float)(Math.Sqrt((oLight.Location.Width * oLight.Location.Width) + (oLight.Location.Height * oLight.Location.Height)) / 2));
float intensity = (oLight.Properties["Intensity"] != null ? Convert.ToSingle(oLight.Properties["Intensity"].Value) : 1.0f);
Vector2 axis = new Vector2(oLight.Location.Width, oLight.Location.Height);
float direction = (oLight.Properties["Direction"] != null ? MathHelper.ToRadians(Convert.ToSingle(oLight.Properties["Direction"].Value)) : MathHelper.ToRadians(0f));
LightMap.AddLight(position, radius, color);
switch (type)
{
case "Sky":
DarkLight.Add(new SkyLight(position, axis, oLight.Location.Width, color));
break;
case "Spot":
DarkLight.Add(new SpotLight(position, color, radius, oLight.Location.Width, direction));
break;
case "Point":
default:
DarkLight.Add(new PointLight(position, color, radius, intensity));
break;
}
}
}
DarkLight.Add(Player.DarkLight);
LightMap.Lights.Add(Player.Light);
foreach (Monster monster in Monsters)
{
DarkLight.Add(monster.DarkLight);
LightMap.Lights.Add(monster.Light);
}
}
[/csharp]
Next we will add in our utility methods for using DarkLight to our World class.
[csharp]
#region DarkLight Utilities
public static void LoadDarkLight()
{
Effect _shadowEffect = Content.Load<Effect>("Shaders\ShadowEffect");
ColorBuffer = new RenderTarget2D(GraphicsDevice, GraphicsDevice.PresentationParameters.BackBufferWidth, GraphicsDevice.PresentationParameters.BackBufferHeight, 1, SurfaceFormat.Color);
GroundBuffer = new RenderTarget2D(GraphicsDevice, GraphicsDevice.PresentationParameters.BackBufferWidth, GraphicsDevice.PresentationParameters.BackBufferHeight, 1, SurfaceFormat.Color);
OverlayBuffer = new RenderTarget2D(GraphicsDevice, GraphicsDevice.PresentationParameters.BackBufferWidth, GraphicsDevice.PresentationParameters.BackBufferHeight, 1, SurfaceFormat.Color);
DarkLight = new DarkLightRenderer(GraphicsDevice, _shadowEffect, DrawOccluders, DrawGround, null);
DarkLightEnabled = false;
}
private static void DrawOccluders()
{
if (DarkLightEnabled)
{
SpriteBatch.Begin(SpriteBlendMode.Additive, SpriteSortMode.Deferred, SaveStateMode.SaveState);
SpriteBatch.Draw(ColorBuffer.GetTexture(), Vector2.Zero, Color.White);
SpriteBatch.End();
}
}
public static void UpdateLights(GameTime gameTime)
{
if (DarkLightEnabled)
{
for (int i = 0; i < DarkLight.Count; i++)
{
DarkLight[i].Updated = false;
}
UpdateDarkLight(DarkLight[Player.DarkLight.Index], Player.DarkLight);
foreach (Monster monster in Monsters)
{
UpdateDarkLight(DarkLight[monster.DarkLight.Index], monster.DarkLight);
}
for (int i = 0; i < DarkLight.Count; i++)
{
DarkLight_Light light = DarkLight[i];
UpdateDarkLight(light, light);
}
}
}
private static void UpdateDarkLight(DarkLight_Light light, DarkLight_Light refLight)
{
if (!light.Updated)
{
light.Position = refLight.WorldPosition;
light.Updated = true;
}
}
#endregion
[/csharp]
Then finally we will modify our Draw method to handle the different lighting systems. I use Booleans that can be changed whenever to toggle between which lighting system is used. You can also do this from the maps as a map property, ‘LightsEnabled’ enables CPU lighting, ‘DarkLightEnabled’ enables GPU lighting.
[csharp]
public static void Draw()
{
if (DarkLightEnabled)
{
GraphicsDevice.SetRenderTarget(0, ColorBuffer);
GraphicsDevice.Clear(Color.TransparentBlack);
SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState, Camera.World);
SetVisible(false, false, false, true);
Map.Draw(SpriteBatch);
SpriteBatch.End();
DarkLight.Draw(SpriteBatch);
LastFrame = ColorBuffer.GetTexture();
}
else if(LightsEnabled)
{
LightMap.Render();
DrawGround();
}
else
{
DrawGround();
}
if (GroundBuffer != null)
{
GraphicsDevice.SetRenderTarget(0, null);
GraphicsDevice.Clear(Color.TransparentBlack);
SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
SpriteBatch.Draw(GroundBuffer.GetTexture(), Vector2.Zero, Color.White);
SpriteBatch.End();
}
}
private static void DrawGround()
{
GraphicsDevice.SetRenderTarget(0, GroundBuffer);
GraphicsDevice.Clear(Color.TransparentBlack);
SetVisible(true, true, false, false);
SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState, Camera.Matrix);
Map.Draw(SpriteBatch, Camera.VisibleArea);
SpriteBatch.End();
if (DarkLightEnabled)
{
SpriteBatch.Begin(SpriteBlendMode.Additive, SpriteSortMode.Immediate, SaveStateMode.None);
GraphicsDevice.RenderState.SourceBlend = Blend.One;
GraphicsDevice.RenderState.DestinationBlend = Blend.SourceColor;
SpriteBatch.Draw(DarkLight.LightBuffer, Vector2.Zero, new Color(Color.White, 128));
SpriteBatch.End();
}
SetVisible(false, true, false, false);
SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState, Camera.Matrix);
Map.Draw(SpriteBatch, Camera.VisibleArea);
SpriteBatch.End();
DrawCharacters();
SetVisible(false, false, true, false);
SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState, Camera.Matrix);
Map.Draw(SpriteBatch, Camera.VisibleArea);
SpriteBatch.End();
if (DarkLightEnabled)
{
SpriteBatch.Begin(SpriteBlendMode.Additive, SpriteSortMode.Immediate, SaveStateMode.None);
GraphicsDevice.RenderState.SourceBlend = Blend.One;
GraphicsDevice.RenderState.DestinationBlend = Blend.SourceColor;
SpriteBatch.Draw(DarkLight.LightMap, Vector2.Zero, new Color(Color.White, 255));
SpriteBatch.End();
}
else
{
if (LightsEnabled)
{
LightMap.Draw();
}
}
}
[/csharp]
And before we can run our game, we need to add in our shader file to our content. So add a new file under ContentShadersShadowEffect.fx
[csharp]
/*************************************************************************
ShadowEffect.fx
Handles the drawing of shadows and lights in the DarkLight engine.
Copyright Alex Parker 2009-2010 ©. All rights reserved.
*************************************************************************/
sampler Framebuffer : register(s0) = sampler_state
{
AddressU = clamp;
AddressV = clamp;
};
sampler Shadowbuffer : register(s1) = sampler_state
{
AddressU = clamp;
AddressV = clamp;
};
#define NUM_SAMPLES 20
float4x4 Projection;
float3 ChannelMask;
float Intensity;
float AmbientIntensity = 12;
float4 AmbientColor : AMBIENT = float4(.5,.5,.5,1);
//Converts from post-projection space (-1..1) to texture coordinate space (0..1)
float2 ToTexCoord(float2 value)
{
return (float2(value.x, -value.y) + 1.0) * 0.5;
}
void ShadowVS(
float4 position : POSITION,
float2 direction : TEXCOORD,
float3 color : COLOR,
out float4 oPosition : POSITION,
out float2 oLightDir : TEXCOORD0,
out float2 oLightPos : TEXCOORD1)
{
oPosition = mul(position + float4(direction, 0, 0), Projection);
float2 currentPos = ToTexCoord(oPosition.xy);
oLightPos = ToTexCoord(mul(position, Projection).xy);
oLightDir = (currentPos - oLightPos) / (NUM_SAMPLES-1);
}
float4 ShadowPS(
float2 lightDir : TEXCOORD0,
float2 lightPos : TEXCOORD1) : COLOR
{
float shadow = 0.0;
for (int i = 0; i<NUM_SAMPLES; i++)
{
shadow += tex2D(Framebuffer, lightPos + (lightDir * i)).x;
}
return float4(shadow * ChannelMask, 1);
}
void LightVS(
float4 position : POSITION,
float2 direction : TEXCOORD,
float3 color : COLOR,
out float4 oPosition : POSITION,
out float2 oLightDir : TEXCOORD0,
out float2 oCurrentPos : TEXCOORD1,
out float3 oColor : COLOR)
{
oPosition = mul(position + float4(direction, 0, 0), Projection);
oCurrentPos = ToTexCoord(oPosition.xy);
float2 lightPos = ToTexCoord(mul(position, Projection).xy);
oLightDir = (lightPos - oCurrentPos) / (NUM_SAMPLES * 3);
oColor = color;
}
float4 LightPS(
float2 lightDir : TEXCOORD0,
float2 currentPos : TEXCOORD1,
float3 color : COLOR) : COLOR
{
float shadow = 0.0;
for (int i = 0; i<NUM_SAMPLES; i++)
{
shadow += dot(ChannelMask, tex2D(Shadowbuffer, currentPos + (lightDir * i)));
}
shadow = saturate(1.0 - shadow);
return float4(color * shadow * Intensity, 1);
}
void VS(
float4 position : POSITION,
float2 texCoord : TEXCOORD,
out float4 oPosition : POSITION,
out float2 oTexCoord : TEXCOORD)
{
oPosition = position;
oTexCoord = texCoord;
}
float4 PS(
float2 texCoord : TEXCOORD) : COLOR
{
float4 color = tex2D(Framebuffer, texCoord);
return color * AmbientIntensity * AmbientColor;
}
technique RenderShadows
{
pass ShadowShader
{
AlphaBlendEnable = true;
SrcBlend = One;
DestBlend = One;
VertexShader = compile vs_3_0 ShadowVS();
PixelShader = compile ps_3_0 ShadowPS();
}
pass LightShader
{
VertexShader = compile vs_3_0 LightVS();
PixelShader = compile ps_3_0 LightPS();
}
pass FullscreenPass
{
//AlphaBlendEnable = true;
//SrcBlend = SrcColor;
//DestBlend = One;
VertexShader = compile vs_3_0 VS();
PixelShader = compile ps_3_0 PS();
}
}
[/csharp]
So now if you run the game you should get some nice lighting.
So to wrap things up, lets add in the Day/Night transitions with the CPU lighting.
So first, we are going to add some more to our WorldTime class to have it update a LightValue, and have it to where you can set values for the AmbientLight for various times of day. Also it will allow you to set specific times for four points in the day (Sunrise, Noon, Sunset, and Midnight).
[csharp]
public class WorldTime
{
#region Fields
private double elapsed = 0;
#endregion
#region Properties
public int Year {get; set;}
public int Month {get; set;}
public int Day {get; set;}
public int Hour { get; set; }
public int Minute { get; set; }
public float Interval { get; set; }
public bool IsDay
{
get
{
return (Hour >= Sunrise && Hour < Sunset);
}
}
public bool IsNight
{
get
{
return ((Hour >= 0 && Hour < Sunrise) || (Hour >= Sunset));
}
}
public int Sunrise { get; set; }
public int Sunset { get; set; }
public int Noon
{
get
{
return ((Sunset - Sunrise) / 2) + Sunrise;
}
}
public int Midnight
{
get
{
int ret = (((HoursInDay - Sunset) + (Sunrise)) / 2) + Sunset;
if (ret >= HoursInDay)
{
ret = 0;
}
return ret;
}
}
public int MinutesInHour { get; set; }
public int HoursInDay { get; set; }
public int DaysInMonth { get; set; }
public int MonthsInYear { get; set; }
public double Tick
{
get
{
return Minute + (Hour * MinutesInHour) + (Day * HoursInDay * MinutesInHour) + (Month * DaysInMonth * HoursInDay * MinutesInHour) + (Year * MonthsInYear * DaysInMonth * HoursInDay * MinutesInHour);
}
}
public double DayTick
{
get
{
return Minute + (Hour * MinutesInHour);
}
}
public byte LightValue
{
get
{
float ret = MidnightLightValue;
if (Hour >= Midnight && Hour < Sunrise)
{
float diff = (float)((Sunrise * MinutesInHour) - (Midnight * MinutesInHour));
float tick = ((Hour * MinutesInHour) + Minute) / diff;
float lightDiff = (SunriseLightValue - MidnightLightValue);
if (diff != 0f)
{
return Convert.ToByte(MathHelper.Clamp(MidnightLightValue + (tick * lightDiff), MidnightLightValue, SunriseLightValue));
}
else
{
return Convert.ToByte(MidnightLightValue);
}
}
if (Hour >= Sunrise && Hour < Noon)
{
float diff = (float)((Noon * MinutesInHour) - (Sunrise * MinutesInHour));
float tick = (((Hour * MinutesInHour) + Minute) - (Sunrise * MinutesInHour)) / diff;
float lightDiff = (NoonLightValue - SunriseLightValue);
if (diff != 0f)
{
return Convert.ToByte(MathHelper.Clamp(SunriseLightValue + (tick * lightDiff), SunriseLightValue, NoonLightValue));
}
else
{
return Convert.ToByte(SunriseLightValue);
}
}
if (Hour >= Noon && Hour < Sunset)
{
float diff = (float)((Sunset * MinutesInHour) - (Noon * MinutesInHour));
float tick = (((Hour * MinutesInHour) + Minute) - (Noon * MinutesInHour)) / diff;
float lightDiff = (SunsetLightValue - NoonLightValue);
if (diff != 0f)
{
return Convert.ToByte(MathHelper.Clamp(NoonLightValue + (tick * lightDiff), SunsetLightValue, NoonLightValue));
}
else
{
return Convert.ToByte(NoonLightValue);
}
}
if (Hour >= Sunset && Hour < (HoursInDay - Sunset) + Sunset)
{
float diff = (float)((((HoursInDay - Sunset) + Sunset) * MinutesInHour) - (Sunset * MinutesInHour));
float tick = (((Hour * MinutesInHour) + Minute) - (Sunset * MinutesInHour)) / diff;
float lightDiff = (SunsetLightValue - MidnightLightValue);
if (diff != 0f)
{
return Convert.ToByte(MathHelper.Clamp(SunsetLightValue - (tick * lightDiff), MidnightLightValue, SunsetLightValue));
}
else
{
return Convert.ToByte(SunsetLightValue);
}
}
return Convert.ToByte(ret);
}
}
public int MidnightLightValue { get; set; }
public int SunriseLightValue { get; set; }
public int NoonLightValue { get; set; }
public int SunsetLightValue { get; set; }
#endregion
#region Constructor
public WorldTime()
{
Year = 1;
Month = 1;
Day = 1;
Hour = 0;
Minute = 0;
Interval = 12f;
Sunrise = 6;
Sunset = 18;
MinutesInHour = 60;
HoursInDay = 24;
DaysInMonth = 30;
MonthsInYear = 12;
MidnightLightValue = 16;
SunriseLightValue = 192;
NoonLightValue = 255;
SunsetLightValue = 128;
}
public WorldTime(int month, int day, int year, int hour, int minute)
{
Year = year;
Month = month;
Day = day;
Hour = hour;
Minute = minute;
Interval = 12f;
Sunrise = 6;
Sunset = 18;
MinutesInHour = 60;
HoursInDay = 24;
DaysInMonth = 30;
MonthsInYear = 12;
MidnightLightValue = 16;
SunriseLightValue = 192;
NoonLightValue = 255;
SunsetLightValue = 128;
}
#endregion
#region Methods
public override string ToString()
{
return string.Format("Date: {0}//{1}//{2:0000} | Time: {3:00}:{4:00} | {5}", Month, Day, Year, Hour, Minute, IsDay ? "Day Time" : "Night Time");
}
public void Update(GameTime gameTime)
{
elapsed += gameTime.ElapsedGameTime.TotalMilliseconds;
if (elapsed >= Interval)
{
elapsed = 0;
Minute++;
if (Minute >= 60)
{
Hour++;
Minute = 0;
}
if (Hour >= 24)
{
Day++;
Hour = 0;
}
if (Day > 30)
{
Month++;
Day = 1;
}
if (Month > 12)
{
Year++;
Month = 1;
}
}
}
#endregion
}
[/csharp]
Then the last part is to change our World.UpdateLights method to use the changes to the WorldTime to adjust the AmbientLight of the world.
[csharp]
public static void UpdateLights(GameTime gameTime)
{
if (DarkLightEnabled)
{
for (int i = 0; i < DarkLight.Count; i++)
{
DarkLight[i].Updated = false;
}
UpdateDarkLight(DarkLight[Player.DarkLight.Index], Player.DarkLight);
foreach (Monster monster in Monsters)
{
UpdateDarkLight(DarkLight[monster.DarkLight.Index], monster.DarkLight);
}
for (int i = 0; i < DarkLight.Count; i++)
{
DarkLight_Light light = DarkLight[i];
UpdateDarkLight(light, light);
}
}
else if(LightsEnabled)
{
AmbientLight = new Color(Time.LightValue, Time.LightValue, Time.LightValue, 255);
LightMap.AmbientLight = AmbientLight;
}
}
[/csharp]
So now if we run our game, we will get nice smooth transitions from Day to Night, and Night to Day. Then you can adjust the interval of the WorldTime to speed up, or slow down the transitions.
So that concludes our fourth tutorial.
Download: Part4_Source.zip
Note: Source Licenses under MSPL




September 28th, 2010 - 18:24
Great Tutorial!
)
Since XNA 4.0 is out it may be a good idea to remove the recompile stuff since they removed this option. (I´m really missing this feature
If I did not overread it, you dont tell us the tint colors you use for the day night circle. I´ve to admit that I´m unable to find some good looking colors
October 2nd, 2010 - 13:37
Uh… Stupid Comment, the light values are in the code…