如何在 XNA 4.0 中使用插值计时器在纹理上执行淡入淡出过渡
创建一个插值计时器,并使用它在 XNA 4.0 中淡入淡出纹理
引言
在使用 XNA Game Studio 3.1 一段时间后又休息了一段时间,我在弄清楚如何为纹理应用 alpha 值并进行转换时遇到了麻烦。我似乎找不到任何像样的教程来完成这个目的,所以我在编写 XNA 4.0 的屏幕保护程序时坐下来,把这两个类捆绑在了一起。
它们简单易用,注释和文档都很完善,所以每个人应该都能轻松理解。
请温柔对待,因为这是我的第一篇(可能也是唯一一篇)文章/教程。
对您的假设:
我假设您至少有一些 XNA Game Studio 4.0 和 C# 的经验,并且您能够
- 创建 Windows Game 项目
- 将纹理添加到您的项目
- 将类添加到您的项目(附带提供的代码)
- 调试发生的任何错误
- 理解相当简单的代码(这里没有矩阵、四元数或三角函数...)
使用代码
您的项目中需要创建两个类:
- InterpolationTimer.cs - 这是一个自定义计时器类,可以用作简单计时器,也可以接受最小值和最大值,并在持续时间内自动在最小值和最大值之间进行插值
- FadingTexture.cs - 这个类利用 4 个插值计时器来自动化纹理在需要时的延迟后的过渡,从完全透明到不透明,然后再回到透明。
如果您在从本页复制代码时遇到任何问题,我附上了一个正在运行的演示项目: 下载演示项目和源代码
插值计时器类
#region Timer Finished Event Delegate
internal delegate void TimerFinished();
#endregion
/// <summary>
/// A Timer class that can automatically interpolate between two values over the specified time, or simply run for the specified time. Raises an event when complete.
/// </summary>
internal class InterpolationTimer
{
#region Private Members
private TimeSpan _timeLimit;
private TimeSpan _timeElapsed;
private bool _isRunning = false;
private float _deltaPerMillisecond;
private float _currentValue;
private float _minValue, _maxValue;
private bool _interpolate;
#endregion
#region Internally-accessible Properties
/// <summary>
/// Gets the Time Limit of this timer. To Set the time limit, please use SetTimeLimit().
/// </summary>
internal TimeSpan TimeLimit
{
get { return _timeLimit; }
}
/// <summary>
/// Gets the current time elapsed of this timer.
/// </summary>
internal TimeSpan TimeElapsed
{
get { return _timeElapsed; }
}
/// <summary>
/// Returns true if the timer is currently running.
/// </summary>
internal bool IsRunning
{
get { return _isRunning; }
}
/// <summary>
/// This event will be fired once the timer is complete.
/// </summary>
internal event TimerFinished OnTimerFinished;
/// <summary>
/// This value is calculated by the class.
/// It is the delta value of CurrentValue, per millisecond, that the timer uses to interpolate based on gametime.
/// </summary>
internal float DeltaPerMillisecond
{
get {
if (_interpolate)
return _deltaPerMillisecond;
else
throw new NullReferenceException("You cannot retrieve DeltaPerMillisecond from a non-interpolated timer!");
}
}
/// <summary>
/// The current value of interpolation.
/// </summary>
internal float CurrentValue
{
get {
if (_interpolate)
return _currentValue;
else
throw new NullReferenceException("You cannot retrieve CurrentValue from a non-interpolated timer!");
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a Timer that interpolates currentValue between minValue and maxValue over timeLimit.
/// Raises an event when the timer is finished. Very useful for fading textures in/out, or moving along an axis at a steady speed over time.
/// To interpolate backwards, simply input the higher value as the minValue.
/// </summary>
/// <param name="timeLimit">A TimeSpan variable containing the total time that the timer is to run before finishing.</param>
/// <param name="minValue">The minimum value to interpolate from.</param>
/// <param name="maxValue">The maximum value to interpolate to.</param>
internal InterpolationTimer(TimeSpan timeLimit, float minValue, float maxValue)
{
_timeLimit = timeLimit;
_timeElapsed = TimeSpan.Zero;
// calculate the amount to interpolate by per millisecond of gametime.
_deltaPerMillisecond = (maxValue - minValue) / (float)TimeLimit.TotalMilliseconds;
_minValue = minValue;
_maxValue = maxValue;
_interpolate = true;
}
/// <summary>
/// Creates a Timer that raises an event once the time limit has been reached.
/// </summary>
/// <param name="timeLimit">A TimeSpan variable containing the total time that the timer is to run before finishing.</param>
internal InterpolationTimer(TimeSpan timeLimit)
{
_timeLimit = timeLimit;
Stop(); // make sure it's not running
_timeElapsed = TimeSpan.Zero;
_interpolate = false;
}
#endregion
#region Timer Control Methods
/// <summary>
/// Starts the timer.
/// </summary>
internal void Start()
{
_timeElapsed = TimeSpan.Zero;
_isRunning = true;
if (_interpolate)
{
// set the current value to the minimum
_currentValue = _minValue;
}
}
/// <summary>
/// Stops the timer. Automatically called when the elapsed time reaches the time limit.
/// </summary>
internal void Stop()
{
_timeElapsed = TimeSpan.Zero;
_isRunning = false;
}
/// <summary>
/// Update the time limit for an interpolating timer. Will throw an exception if the timer is running.
/// You must provide minValue and maxValue so the DeltaPerMillisecond can be recalculated based on the new time limit!
/// </summary>
/// <param name="newTimeLimit">The new time limit.</param>
/// <param name="minValue">Minimum value for interpolation.</param>
/// <param name="maxValue">Maximum value for interpolation.</param>
internal void SetTimeLimit(TimeSpan newTimeLimit, float minValue, float maxValue)
{
if (!_interpolate)
{
SetTimeLimit(newTimeLimit);
return;
}
if (IsRunning)
throw new ApplicationException("You must stop the timer before changing the time limit!");
// recalculate deltapermillisecond
_deltaPerMillisecond = (maxValue - minValue) / (float)newTimeLimit.TotalMilliseconds;
_timeLimit = newTimeLimit;
}
/// <summary>
/// Update the time limit for a non-interpolating timer.
/// NOTE: Will throw an exception if this IS an interpolating timer or if the timer is running.
/// </summary>
/// <param name="newTimeLimit"></param>
internal void SetTimeLimit(TimeSpan newTimeLimit)
{
if (_interpolate)
throw new NullReferenceException("You must re-specify the minimum and maximum values for an interpolation timer! See - SetTimeLimit(time, minValue, maxValue)");
if (IsRunning)
throw new ApplicationException("You must stop the timer before changing the time limit!");
_timeLimit = newTimeLimit;
}
#endregion
#region Update Method
/// <summary>
/// Call this from your game's update method. Or if you are using this class with my FadingTexture class, it will call this automatically for you.
/// </summary>
/// <param name="elapsedTime">The gameTime.ElapsedGameTime value (from your Update Method)</param>
internal void Update(TimeSpan elapsedTime)
{
if (IsRunning)
{
// add elapsed time
_timeElapsed += elapsedTime;
// if we are interpolating between minValue and maxValue, do it here
if (_interpolate)
_currentValue += DeltaPerMillisecond * (float)elapsedTime.TotalMilliseconds;
// check if we have matched or exceeded the time limit, if so,
// stop the timer and raise the OnTimerFinished event if it is assigned to
if (_timeElapsed >= _timeLimit)
{
// make sure final value is what it should be (value is always *slightly* lower (like 0.0005 off for me!)
if (_currentValue != _maxValue)
_currentValue = _maxValue;
// stop the timer
Stop();
// trigger the event if it is assigned to
if (OnTimerFinished != null)
OnTimerFinished();
}
}
}
#endregion
}
FadingTexture 类
这个类是一个 DrawableGameComponent
,这意味着它将与您的游戏集成,而您的游戏将自动调用它的更新和绘制方法。
#region Sequence Complete Event Delegate
internal delegate void FadeSequenceComplete();
#endregion
/// <summary>
/// A class that utilises the InterpolationTimer class to fade a texture in and out.
/// </summary>
internal class FadingTexture : DrawableGameComponent
{
#region Private Members
/// <summary>
/// Used only by this class, hence inside the scope of the class.
/// Keeps track of the current fade state.
/// </summary>
enum FadeState { InitialDelay, FadingIn, Opaque, FadingOut, Complete };
private Texture2D _splashTexture;
private Game1 _game;
private Vector2 _position;
private TimeSpan _fadeInTime, _fadeOutTime, _opaqueTime, _initialDelayTime;
private FadeState _currentState;
private float _opacity;
#endregion
#region Internally-accessible Properties & Members
// timers
internal InterpolationTimer FadeInTimer, OpaqueTimer, FadeOutTimer, InitialDelayTimer;
/// <summary>
/// This event will fire once the entire sequence (initial delay + fade in + opaque + fade out) is completed.
/// If you wish, you can use this event to call Reset() which sets the sequence to fire again at a later time.
/// </summary>
internal event FadeSequenceComplete OnFadeSequenceCompleted;
/// <summary>
/// Gets the sum duration of the entire sequence.
/// </summary>
internal TimeSpan TotalDuration
{
get {
return _fadeInTime + _fadeOutTime + _opaqueTime + _initialDelayTime;
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a FadingTexture sequence WITHOUT an initial delay (eg, fade in will begin immediately).
/// </summary>
/// <param name="game">Obvious.</param>
/// <param name="position">The *top-left* coordinates of the texture.</param>
/// <param name="texture">The texture to fade in and out.</param>
/// <param name="fadeInTime">How long you want the fade in transition to last.</param>
/// <param name="opaqueTime">How long you want the texture to be fully opaque.</param>
/// <param name="fadeOutTime">How long you want the fade out transition to last.</param>
internal FadingTexture(Game1 game, Vector2 position, Texture2D texture, TimeSpan fadeInTime, TimeSpan opaqueTime, TimeSpan fadeOutTime)
: base(game)
{
_game = game;
// add to game components
if (!_game.Components.Contains(this))
_game.Components.Add(this);
// make sure this draws on top of everything else
DrawOrder = 30000;
// set members according to constructor params
_splashTexture = texture;
_fadeInTime = fadeInTime;
_opaqueTime = opaqueTime;
_fadeOutTime = fadeOutTime;
_initialDelayTime = TimeSpan.Zero;
_currentState = FadeState.FadingIn;
_position = position;
// call initialize, since the game may not call it if it instantiated at the wrong time (I still don't get how that works...)
Initialize();
}
/// <summary>
/// Creates a FadingTexture sequence WITH an initial delay (eg, fade in will begin *AFTER* the initial delay has passed).
/// </summary>
/// <param name="game">Obvious.</param>
/// <param name="position">The *top-left* coordinates of the texture.</param>
/// <param name="texture">The texture to fade in and out.</param>
/// <param name="initialDelayTime">How long you want the class to wait before starting the fade in transition.</param>
/// <param name="fadeInTime">How long you want the fade in transition to last.</param>
/// <param name="opaqueTime">How long you want the texture to be fully opaque.</param>
/// <param name="fadeOutTime">How long you want the fade out transition to last.</param>
internal FadingTexture(Game1 game, Vector2 position, Texture2D texture, TimeSpan initialDelayTime, TimeSpan fadeInTime, TimeSpan opaqueTime, TimeSpan fadeOutTime)
: base(game)
{
_game = game;
// add to game components
if (!_game.Components.Contains(this))
_game.Components.Add(this);
// make sure this draws on top of everything else
DrawOrder = 30000;
// set members according to constructor params
_splashTexture = texture;
_fadeInTime = fadeInTime;
_opaqueTime = opaqueTime;
_fadeOutTime = fadeOutTime;
_initialDelayTime = initialDelayTime;
_currentState = FadeState.InitialDelay;
_position = position;
// call initialize, since the game may not call it if it instantiated at the wrong time (I still don't get how that works...)
Initialize();
}
#endregion
#region Overridden XNA Methods (Initialize, Update & Draw)
public override void Initialize()
{
base.Initialize();
// if we have an initial delay, create the initial delay timer.
if (_currentState == FadeState.InitialDelay)
{
InitialDelayTimer = new InterpolationTimer(_initialDelayTime);
InitialDelayTimer.OnTimerFinished += new TimerFinished(InitialDelayTimer_OnTimerFinished);
}
// set up timers and their events
FadeInTimer = new InterpolationTimer(_fadeInTime, 0.0f, 1.0f);
FadeInTimer.OnTimerFinished += new TimerFinished(FadeInTimer_OnTimerFinished);
OpaqueTimer = new InterpolationTimer(_opaqueTime);
OpaqueTimer.OnTimerFinished += new TimerFinished(OpaqueTimer_OnTimerFinished);
FadeOutTimer = new InterpolationTimer(_fadeOutTime, 1.0f, 0.0f);
FadeOutTimer.OnTimerFinished += new TimerFinished(FadeOutTimer_OnTimerFinished);
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
// respond appropriately to the current state
switch (_currentState)
{
case FadeState.InitialDelay:
if (!InitialDelayTimer.IsRunning)
InitialDelayTimer.Start();
InitialDelayTimer.Update(gameTime.ElapsedGameTime);
_opacity = 0.0f; // going to be fully transparent at this stage
break;
case FadeState.FadingIn:
if (!FadeInTimer.IsRunning)
FadeInTimer.Start();
FadeInTimer.Update(gameTime.ElapsedGameTime);
_opacity = FadeInTimer.CurrentValue;
break;
case FadeState.Opaque:
if (!OpaqueTimer.IsRunning)
OpaqueTimer.Start();
OpaqueTimer.Update(gameTime.ElapsedGameTime);
_opacity = 1.0f;
break;
case FadeState.FadingOut:
if (!FadeOutTimer.IsRunning)
FadeOutTimer.Start();
FadeOutTimer.Update(gameTime.ElapsedGameTime);
_opacity = FadeOutTimer.CurrentValue;
break;
}
// prevent calls to Draw() unless the texture is visible
if (_opacity > 0.0f)
Visible = true;
else
Visible = false;
}
public override void Draw(GameTime gameTime)
{
base.Draw(gameTime);
_game.spriteBatch.Begin();
_game.spriteBatch.Draw(_splashTexture, _position, Color.White * _opacity);
_game.spriteBatch.End();
}
#endregion
#region Custom Methods
/// <summary>
/// Resets the current sequence's InitialDelayTimer to delay and begins anew.
/// </summary>
/// <param name="delay">The time to wait before starting the fade sequence again.</param>
internal void Reset(TimeSpan delay)
{
if (_currentState != FadeState.Complete)
throw new Exception("Please wait until the sequence is complete before calling Reset!");
_initialDelayTime = delay;
// make sure InitialDelayTimer is not null (which would be the case if no initial delay was specified in the constructor)
if (InitialDelayTimer == null)
{
InitialDelayTimer = new InterpolationTimer(delay);
InitialDelayTimer.OnTimerFinished += new TimerFinished(InitialDelayTimer_OnTimerFinished);
}
else
InitialDelayTimer.SetTimeLimit(delay);
_currentState = FadeState.InitialDelay;
}
#endregion
#region InterpolationTimer Events
void FadeOutTimer_OnTimerFinished()
{
_currentState = FadeState.Complete;
// fire timer complete event if it is assigned to
if (OnFadeSequenceCompleted != null)
OnFadeSequenceCompleted();
}
void OpaqueTimer_OnTimerFinished()
{
_currentState = FadeState.FadingOut;
}
void FadeInTimer_OnTimerFinished()
{
_currentState = FadeState.Opaque;
}
void InitialDelayTimer_OnTimerFinished()
{
_currentState = FadeState.FadingIn;
}
#endregion
}
使用 FadingTexture 类
您需要在 Game.LoadContent
方法中加载一个 Texture2D
,然后将其传递给这个类的构造函数。
如果您不喜欢这样做,我相信您足够聪明,可以弄清楚如何让这个类加载纹理。
这个类有两个构造函数,一个是在淡入纹理之前设置初始延迟,另一个是立即淡入纹理。
实例化该类后,它将为您完成所有工作。您的 Game.Update 或 Game.Draw 中不需要其他代码。
1)。使您的 Game 类中的 spriteBatch
变量成为 INTERNAL 或 PUBLIC,以便此类可以访问它:
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
internal SpriteBatch spriteBatch; // YOU MUST MAKE THIS ACCESSIBLE TO INTERNAL OR PUBLIC SCOPE, OR ADD YOUR OWN SPRITEBATCH TO THE FADING TEXTURE CLASS!
// ...
}
2)。为了让此类访问您的 spritebatch,它需要知道您游戏的类型。
查看 FadingTexture
类中的代码,并进行相应更改以适应您的游戏。如果您的游戏类只是 Game1,则可以跳过此步骤。
在 FadingTexture.cs
中:
// change "Game1" to the name of your game class in each of the following lines:
private Game1 _game;
internal FadingTexture(Game1 game, Vector2 position, Texture2D texture, TimeSpan fadeInTime, TimeSpan opaqueTime, TimeSpan fadeOutTime)
internal FadingTexture(Game1 game, Vector2 position, Texture2D texture, TimeSpan initialDelayTime, TimeSpan fadeInTime, TimeSpan opaqueTime, TimeSpan fadeOutTime)
3)。在您的游戏中定义类变量,如果您还没有,则定义一个包含纹理的 Texture2D
,以及适用的 fadeIn、Opaque、fadeOut 和 InitialDelay 的 TimeSpan
。
在 Game1.cs
中:(或您的 Game 类文件名是什么)
// VARIABLES
FadingTexture _myFadingTexture;
FadingTexture _myFadingTextureDelayed;
Texture2D _myTexture;
Vector2 _texturePosition;
TimeSpan _fadeInTime = TimeSpan.FromSeconds(1.5),
_fadeOutTime = TimeSpan.FromSeconds(1.5),
_opaqueTime = TimeSpan.FromSeconds(5);
// if you want an initial delay...
TimeSpan _delayTime = TimeSpan.FromSeconds(3); // don't worry, we won't make this occur at the same time as the immediate transition, see below
4)。在**加载完内容后**实例化该类
在 Game1.cs
中:
protected override void LoadContent()
{
// load your content here
_myTexture = Content.Load<Texture2D>(@"Textures\fadeTexture");
// ... load the rest of your content if you have any
// set where you want your texture to be on the screen (top-left coordinates, so if we want it to be centered we need
// to take half of the texture dimensions away from the screen center
Viewport vp = graphics.GraphicsDevice.Viewport;
Vector2 textureSize = new Vector2(_myTexture.Width, _myTexture.Height);
_texturePosition = new Vector2(vp.Width / 2, vp.Height / 2) - textureSize / 2;
// now instantiate the class
// for an immediate fade-in:
_myFadingTexture = new FadingTexture(this, _texturePosition, _myTexture, _fadeInTime, _opaqueTime, _fadeOutTime);
// or for an initial delay (make sure you've set this variable above or you can call the static timespan methods directly)
// here is a useful property:
delayTime += _myFadingTexture.TotalDuration; // this gets the total transition time of _myFadingTexture, then adds _delayTime on to it
_myFadingTextureDelayed = new FadingTexture(this, _texturePosition, _myTexture, _delayTime, _fadeInTime, _opaqueTime, _fadeOutTime);
}
步骤 5)。运行您的游戏,观看华丽的画面!!
如果您希望您的游戏在过渡完成时收到通知,只需像这样订阅 OnFadeSequenceCompleted
事件
在 Game1.cs
或需要知道过渡何时完成的另一个类中:
_myFadingTextureDelayed.OnFadeSequenceCompleted += new FadeSequenceComplete(_myFadingTextureDelayed_OnFadeSequenceCompleted);
// then do whatever you want when the event is triggered:
void _myFadingTextureDelayed_OnFadeSequenceCompleted()
{
// you can dispose the object...
_myFadingTextureDelayed.Dispose();
// or you can reset it with a delay:
_myFadingTextureDelayed.Reset(TimeSpan.FromMinutes(1)); // will start the fade in exactly 1 minute from when this is called
}
如果您能理解 FadingTexture
类的代码,您应该会意识到您不需要与任何 InterpolationTimer 类进行交互。FadingTexture
为您完成了所有这些工作。
如果您只需要类的计时器功能,以下是如何在不使用 FadingTexture
类的情况下使用 InterpolationTimer
类。
在不使用 FadingTexture 类的情况下使用 InterpolationTimer 类
简单计时器
1)。创建类的实例并订阅 OnTimerFinished
事件
// VARIABLE
// define the timer
InterpolationTimer _myTimer;
// INSTANTIATION
// instantiate it in a function somewhere (Game.Initialize is good)
_myTimer = new InterpolationTimer(TimeSpan.FromSeconds(10)); // makes a timer that will fire the completed event 10 seconds after it is started
// subscribe to the completed event
_myTimer.OnTimerFinished += new TimerFinished(_myTimer_OnTimerFinished); // use intellisense to automatically create the method (after you type +=, press TAB), you can rename it later if you wish
2)。确保计时器在运行时得到更新,否则什么都不会发生!
// In your game's UPDATE method:
protected override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (_myTimer.IsRunning)
_myTimer.Update(gameTime.ElapsedGameTime);
}
3)。在您认为合适的时候启动计时器
_myTimer.Start();
4)。计时器完成后应触发您的事件。如果没有任何反应,请检查您是否在 Update
方法中正确更新了计时器。
// your intellisense-generated code should look like this:
void _myTimer_OnTimerFinished()
{
// delete this and the next line, it will stop your game unless you are handling NotImplementedExceptions, but it's a good way to test the timer is working.
throw new NotImplementedException();
}
插值计时器
与上述代码唯一的区别是重载的构造函数以及如何获取插值。
1)。创建类的实例并订阅 OnTimerFinished
事件
// VARIABLE
// define the timer
InterpolationTimer _myInterpolationTimer;
// INSTANTIATION
// instantiate it in a function somewhere (Game.Initialize is good)
_myInterpolationTimer = new InterpolationTimer(TimeSpan.FromSeconds(10), 0.0f, 1.0f); // timer will change _myInterpolationTimer.CurrentValue from 0.0f to 1.0f over 10 seconds
// or if you want to interpolate backwards:
_myInterpolationTimer = new InterpolationTimer(TimeSpan.FromSeconds(10), 1.0f, 0.0f); // timer will change _myInterpolationTimer.CurrentValue from 1.0f to 0.0f over 10 seconds
// subscribe to the completed event
_myInterpolationTimer.OnTimerFinished += new TimerFinished(_myInterpolationTimer_OnTimerFinished); // use intellisense to automatically create the method (after you type +=, press TAB), you can rename it later if you wish
2)。使用上述简单计时器的代码来确保您的计时器已更新(但将变量名从 _myTimer
重命名为 _myInterpolationTimer
)
您可能想在这里的更新方法中使用插值,要做到这一点,只需像这样引用 _myInterpolationTimer.CurrentValue
:
// In your game's UPDATE method:
protected override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (_myInterpolationTimer.IsRunning)
_myInterpolationTimer.Update(gameTime.ElapsedGameTime);
_textureAlphaLevel = _myInterpolationTimer.CurrentValue;
}
3)。以与上面相同的方式启动计时器
4)。您的事件将与上面相同的方式触发
要使用计时器生成的 alpha 值绘制对象,只需像平常一样绘制它,颜色为白色,乘以 alpha 值,如下所示:
// in your game's Draw method:
spriteBatch.Begin();
spriteBatch.Draw(texture, position, Color.White * _textureAlphaLevel);
spriteBatch.End();
我希望至少有一些人觉得这有用!