首页 > 代码库 > 动画精灵的实现

动画精灵的实现

1.  动画精灵概念

动画就是动态的画面,在计算机中表现为一种运行时数据结构和算法。数据结构表示动画的存储方式,可以事先存储,也可以运行时计算获得。

而算法则声明如何将这种数据结构映射到屏幕上。

精灵是一个渲染单元,存储一些表示如何渲染到屏幕上的数据。
动画精灵是一种特殊的精灵,因此这次的目标就是创建一个精灵类(Sprite)的子类(AnimatedSprite).

2. Sprite类

在设计之前,需要预览一下Sprite类的代码。(省略了用于缩放和旋转的部分代码)
    /// <summary>
    /// 精灵类
    /// </summary>
    public class Sprite
    {
        /// <summary>
        /// 顶点数量
        /// </summary>
        const int VertexAmount = 6;    //2个三角形

        /// <summary>
        /// 顶点坐标
        /// </summary>
        Vector[] _vertexPositions = new Vector[VertexAmount];
        
        /// <summary>
        /// 顶点色彩(局部色彩)
        /// </summary>
        GlColor[] _vertexColors = new GlColor[VertexAmount];
        
        /// <summary>
        /// 顶点映射(纹理贴图用于渲染多边形的特定部分)
        /// </summary>
        GlPoint[] _vertexUVs = new GlPoint[VertexAmount];

        /// <summary>
        /// 纹理
        /// </summary>
        Texture2D _texture = new Texture2D();

        /// <summary>
        /// 获取或设置精灵纹理
        /// </summary>
        public Texture2D Texture
        {
            get { return _texture; }
            set
            {
                _texture = value;

                //默认使用纹理自身的宽高
                InitVertexPositions(CenterPosition, _texture.Width, _texture.Height);
            }
        }

        /// <summary>
        /// 获取顶点数组
        /// </summary>
        public Vector[] VertexPositions
        {
            get { return _vertexPositions; }
        }

        /// <summary>
        /// 获取顶点颜色数组
        /// </summary>
        public GlColor[] VertexColors
        {
            get { return _vertexColors; }
        }

        /// <summary>
        /// 获取顶点坐标数组
        /// </summary>
        public GlPoint[] VertexUVs
        {
            get { return _vertexUVs; }
        }

        /// <summary>
        /// 获取或设置宽度
        /// </summary>
        public double Width
        {
            get
            {
                // 获取实际显示在屏幕上的宽度
                return _vertexPositions[1].X - _vertexPositions[0].X;
            }
            set
            {
                InitVertexPositions(CenterPosition, value, Height);
            }
        }

        /// <summary>
        /// 获取或设置高度
        /// </summary>
        public double Height
        {
            get
            {
                // topleft - bottomleft
                return _vertexPositions[0].Y - _vertexPositions[2].Y;
            }
            set
            {
                InitVertexPositions(CenterPosition, Width, value);
            }
        }

        /// <summary>
        /// 创建一个精灵
        /// </summary>
        public Sprite()
        {
            InitVertexPositions(Vector.Zero, 1, 1);
            SetColor(GlColor.GetWhite());
            SetUVs(new GlPoint(0, 0), new GlPoint(1, 1));

            //正确设置默认的初始位置
            _currentPosition = new Vector(
                _vertexPositions[0].X + Width / 2,
                _vertexPositions[0].Y - Height / 2,
                _vertexPositions[0].Z);
        }

        /// <summary>
        /// 初始化顶点信息
        /// </summary>
        void InitVertexPositions(Vector center, double width, double height)
        {
            double halfWidth = width / 2;
            double halfHeight = height / 2;

            //顺时针创建两个三角形构成四方形
            // TopLeft, TopRight, BottomLeft
            _vertexPositions[0] = new Vector(center.X - halfWidth, center.Y + halfHeight, center.Z);
            _vertexPositions[1] = new Vector(center.X + halfWidth, center.Y + halfHeight, center.Z);
            _vertexPositions[2] = new Vector(center.X - halfWidth, center.Y - halfHeight, center.Z);

            // TopRight, BottomRight, BottomLeft
            _vertexPositions[3] = new Vector(center.X + halfWidth, center.Y + halfHeight, center.Z);
            _vertexPositions[4] = new Vector(center.X + halfWidth, center.Y - halfHeight, center.Z);
            _vertexPositions[5] = new Vector(center.X - halfWidth, center.Y - halfHeight, center.Z);
        }

        /// <summary>
        /// 获取或设置中心位置
        /// </summary>
        public Vector CenterPosition
        {
            get
            {
                return _currentPosition;
            }
            set
            {
                Matrix m = new Matrix();
                m.SetTranslation(value - _currentPosition);
                ApplyMatrix(m);

                _currentPosition = value;
            }
        }

        /// <summary>
        /// 设置颜色
        /// </summary>
        public void SetColor(GlColor color)
        {
            for (int i = 0; i < Sprite.VertexAmount; i++)
            {
                _vertexColors[i] = color;
            }
        }

        /// <summary>
        /// 设置UV,进行纹理映射
        /// </summary>
        public void SetUVs(GlPoint topLeft, GlPoint bottomRight)
        {
            // TopLeft, TopRight, BottomLeft
            _vertexUVs[0] = topLeft;
            _vertexUVs[1] = new GlPoint(bottomRight.X, topLeft.Y);
            _vertexUVs[2] = new GlPoint(topLeft.X, bottomRight.Y);

            // TopRight, BottomRight, BottomLeft
            _vertexUVs[3] = new GlPoint(bottomRight.X, topLeft.Y);
            _vertexUVs[4] = bottomRight;
            _vertexUVs[5] = new GlPoint(topLeft.X, bottomRight.Y);
        }
       
        /// <summary>
        /// 应用矩阵操作
        /// </summary>
        public void ApplyMatrix(Matrix m)
        {
            for (int i = 0; i < VertexPositions.Length; i++)
            {
                VertexPositions[i] *= m;
            }
        }

    }

sprite类只存储用于绘制的数据,自己不负责绘制自身,而是交由Render类负责。

Matrix类用于矩阵运算,应用矩阵可以实现平移、缩放、旋转的效果,简化了sprite类的设计。



3. 一致的素材格式

在flash中,只有关键帧由用户提供,其余帧通过补间完成,而这次设计的动画精灵的所有帧全部由用户提供(来源于图片素材),这样大大简化了设计。
因此所面临的问题只是如何收集和呈现素材。“收集”即将零散的图片资源加载为有序组织的数据结构,“呈现”即将内存的数据利用图形渲染器绘制屏幕上。
我选择C#+opengl的渲染构件,已经实现将普通sprite渲染到屏幕上,只需要考虑如何依次的向opengl传送需要渲染的纹理,双缓冲和其他的渲染技术不需考虑。

动画所必须的素材可能拥有不同的呈现格式,但需要将这些素材加载为一致内存表示。大致有两种方式可以解决这个问题。
第一就是对动画素材进行预处理,通过非编程的手段将素材表示为一致的标准格式。
第二就是针对不同的格式制定相应的加载算法。很明显应该采用第一种解决方案,这有利于简化系统设计。

约定一下的素材格式:
1.一种动画精灵由一个图像文件(png/jpg/...)提供。
2.动画由若干帧组成,每帧的大小是相同的
3.帧按文字的排列方式排列,不能留空隙。
4.除了提供文件之外,还需指明帧的大小和数量。


4. 动画精灵的类设计

由于已经存在sprite类,所以可以简单的通过继承复用一些方法。我们需要做的就是将当前帧映射到显示数据上(顶点数组、颜色数组、UV数组),
这通过运行时计算获得,也可以事先计算。
默认的UV包含整个纹理图像,这就是常规的sprite显示方式-显示整个图像。可以通过修改UV只显示部分图像,
如果每一次当前帧改变的时候都重新设置UV以把纹理图像中代表相应帧的画面显示出来,就可以实现动画的效果。

通过倒推法可以完成此项设计。
->知道当前帧,如何设置UV数据
->调用基类的SetUVs方法,传递两个能够唯一确定显示区域的坐标。
->已经知道整张图像的大小和单位帧的大小,只要知道当前帧在纹理图像上的左上角坐标就可以了。
->可以计算得知一行有多少帧,而且已经知道了帧的大小...
->OK
这其实并不是倒推法,这是解决问题的真正的正常思路。总不可能从条件出发,那才是违逆正常思维的倒推法。
    /// <summary>
    /// 动画精灵
    /// </summary>
    public class AnimatedSprite : Sprite
    {
        /// <summary>
        /// 总帧数
        /// </summary>
        int _totalFrame;

        /// <summary>
        /// 帧宽
        /// </summary>
        double _frameWidth;

        /// <summary>
        /// 帧高
        /// </summary>
        double _frameHeight;

        /// <summary>
        /// 当前帧
        /// </summary>
        int _currentFrame;

        /// <summary>
        /// 当前帧的计时
        /// </summary>
        double _currentFrameTime;

        /// <summary>
        /// 获取或设置每一帧持续的时间
        /// </summary>
        public double FrameDuration { get; set; } 

        /// <summary>
        /// 是否循环播放
        /// </summary>
        public bool Looping { get; set; }

        /// <summary>
        /// 是否播放结束
        /// </summary>
        public bool Finished { get; private set; }

        /// <summary>
        /// 获取一行的帧数量
        /// </summary>
        public int RowFrame
        {
            get
            {
                return (int)Math.Round(Width / _frameWidth);
            }
        }

        /// <summary>
        /// 创建一个可播放的动画精灵
        /// </summary>
        public AnimatedSprite()
        {
            Looping = false;
            Finished = false;
            FrameDuration = 0.05;

            _frameHeight = 0;
            _frameWidth = 0;
            _currentFrame = 0;
            _totalFrame = 1;
            _currentFrameTime = FrameDuration;
        }

        /// <summary>
        /// 拨动到下一帧
        /// </summary>
        public void AdvanceFrame()
        {
            _currentFrame = (_currentFrame + 1) % _totalFrame;
        }

        /// <summary>
        /// 获取帧值在图源中的位置索引
        /// </summary>
        GlPoint GetIndexFromFrame(int frame)
        {
            GlPoint point = new GlPoint();
            point.Y = frame / RowFrame;
            point.X = frame - (point.Y * RowFrame);
            return point;
        }

        /// <summary>
        /// 更新显示数据
        /// </summary>
        void UpdateDisplay()
        {
            GlPoint index = GetIndexFromFrame(_currentFrame);
            Vector startPosition = new Vector(index.X * _frameWidth, index.Y * _frameHeight);
            Vector endPosition = startPosition + new Vector(_frameWidth, _frameHeight);

            SetUVs(new GlPoint((float)(startPosition.X / Width), (float)(startPosition.Y / Height)),
                new GlPoint((float)(endPosition.X / Width), (float)(endPosition.Y / Height)));
        }

        /// <summary>
        /// 通知动画精灵的总帧数
        /// </summary>
        public void SetTotalFrame(int totalFrame)
        {
            _totalFrame = totalFrame;
            _currentFrame = 0;
            _currentFrameTime = FrameDuration;

            UpdateDisplay();
        }

        /// <summary>
        /// 通知动画精灵的帧大小,每一帧将应用一致的尺寸
        /// </summary>
        public void SetFrameSize(double width, double height)
        {
            _frameWidth = width;
            _frameHeight = height;

            UpdateDisplay();
        }

        /// <summary>
        /// 处理更新
        /// </summary>
        public override void Process(double elapsedTime)
        {
            if (_currentFrame == _totalFrame - 1 && Looping == false)
            {
                Finished = true;
                return;
            }

            _currentFrameTime -= elapsedTime;
            if (_currentFrameTime < 0)
            {
                _currentFrameTime = FrameDuration;

                AdvanceFrame();
                UpdateDisplay();
            }
        }

        /// <summary>
        /// 缩放时维护帧大小
        /// </summary>
        public override void SetScale(double x, double y)
        {
            _frameWidth /= _scaleX;
            _frameHeight /= _scaleY;

            base.SetScale(x, y);

            _frameWidth *= x;
            _frameHeight *= y;

            UpdateDisplay();
        }
    }

需要特别注意的是,对于返回整型值的RowFrame属性,不能简单的对求值结果向下取整或强制装换,因为浮点运算的结果是有误差的,一般的游戏可以无视浮点运算的误差,但是当将浮点值装换为整型时,这种误差就有可能发生质的飞跃。

5. 测试

这里写了简单的测试代码,更重要的测试是在实际运行时观察效果。
        public void ProcessTest()
        {
            AnimatedSprite target = new AnimatedSprite();
            target.Texture = new Texture2D(0, 256, 256);
            target.SetTotalFrame(16);
            target.SetFrameSize(64, 64);
            target.CenterPosition = Vector.Zero;

            Assert.IsTrue(target.CurrentFrame == 0);
            Assert.IsTrue(target.Finished == false);

            double elapsedTime = 0.32;
            MultiProcess(target, elapsedTime);

            Assert.IsTrue(target.CurrentFrame == 3);

            MultiProcess(target, elapsedTime);
            MultiProcess(target, elapsedTime);
            MultiProcess(target, elapsedTime);
            MultiProcess(target, elapsedTime);
            MultiProcess(target, elapsedTime);
            MultiProcess(target, elapsedTime);
            MultiProcess(target, elapsedTime);

            Assert.IsTrue(target.Finished == true);
            Assert.IsTrue(target.CurrentFrame == 15);
        }