×

DirectX与VB.NET编程(十三)全屏动画

Kalet Kalet 发表于2009-03-20 12:00:14 浏览343 评论0

抢沙发发表评论

由于前些天下了个IE8 BETA1,发现无法在百度上传图片,试了好久原来是IE8的问题,将IE8卸载了才能继续上传图片,因此博客更新也间隔了这么久,如果你也遇见这个问题,卸载IE8即可解决这个问题,直接在控制面板的添加/删除程序中就可以卸载了,卸载之后IE会回到你之前安装的版本,所以你无须担心。
下面开始说正事,这次的内容是全屏动画。


===============华丽的分割线===============

DirectX与VB.NET编程(十三)全屏动画

学习内容:
·了解DirectDraw动画的原理
·利用翻转绘制无闪烁的高帧动画


===============华丽的分割线===============


首先,需要介绍一下动画的实现原理,动画之所以能够实现是由于多张细微变化的图片快速地在眼前切换,因为人眼能够辨别的最小时间为0.1秒,只要这个切换的速度足够快,就能够让人产生连续动作,从而形成动画,我们将动画的每张画面称为“帧(Frame)”,帧的切换速度我们称为“帧速”,帧速的单位普遍为秒来计算,因此又称为“每秒帧速(Frame per second/FPS)”。游戏的流畅度通常用帧速来衡量,越高帧速的动画的画面看起来更加流畅。


===============华丽的分割线===============


在游戏中,很多时候都要利用到动画,可能你以前尝试过用控件或者GDI,但是有一个最明显的缺点就是它们根本无法达到你所需要的帧数,或者你根本就无法控制它们的帧数,而且画面还闪烁不定,让人无法忍受,DirectDraw绘制动画的优势就在于可以绘制高帧无闪烁的动画,可以用于对显示要求苛刻游戏和高级应用。
DirectDraw之所以可以这样,原理就是在于DirectDraw采用了一种名为“翻转(Flip)”的技术。
DirectDraw在绘制动画时,会准备两个表面用于承载画面,一个画面用于后台,而另一个用于前台呈现,程序要做的就是将需要绘制的元素绘制到后台表面中,然后利用另一个线程周期性地将后台画面拷贝到前台,拷贝完毕后再将后台表面清空来绘制下一帧画面,这样就避免了将前台画面清空-绘制-清空-绘制的过程,即避免了画面闪烁。加之DirectDraw采用硬件加速,高帧绘制也不会有任何问题。


===============华丽的分割线===============
这次的素材如下图所示,你可以在图片上点击右键来下载该图片。我们的最终效果就是程序启动时以全屏方式显示数个小球的自由移动,无论小球有多少,画面都不会出现闪烁的情况。



打开VB.NET,新建一个工程,不用进行窗口设计,因为我们不需要使用其它控件。


添加引用DirectDraw和Threading名称空间:
Imports Microsoft.DirectX
Imports Microsoft.DirectX.DirectDraw
Imports System.Threading


根据动画原理,我们需要声明两个表面用于前后台显示以及另外一个表面用于储存图片。具体做法是声明一个主表面用于前台显示,一个后备表面用于后台缓冲以及一个离屏表面用于储存动画的画面。设备和表面的声明如下:
Dim vDev As Device
Dim vPrimarySurface As Surface
Dim vBackSurface As Surface
Dim vOffSurface As Surface


因为有数个小球移动,因此我们声明一对数组用于储存每个小球的信息(X、Y坐标和X、Y方向速度),此外还添加一个常数用于控制小球的数量:
Public Const BALL_COUNT As Integer = 50
Dim X(BALL_COUNT) As Integer
Dim Y(BALL_COUNT) As Integer
Dim SpeedX(BALL_COUNT) As Integer
Dim SpeedY(BALL_COUNT) As Integer


绘图时采用多线程绘图,因此我们需要声明一个新的线程用于专门的绘图工作,虽然这里我们此时的绘制与处理是在同一个函数中完成,但是大多数的游戏是将数据处理与绘图显示分隔开的。
Dim Running As Boolean
Dim DrawThread As Thread


===============华丽的分割线===============
下面初始化设备,因为翻转必须采用全屏独占模式,因此协作级别采用的枚举是CooperativeLevelFlags.FullscreenExclusive,分辨率为640x480,刷新率为60Hz,24位色深,代码如下:
vDev = New Device(CreateFlags.Default)
vDev.SetCooperativeLevel(Me, CooperativeLevelFlags.FullscreenExclusive)
vDev.SetDisplayMode(640, 480, 2 ^ 24, 60, True)


在初始化主表面时,有几点需要注意,主表面的SurfaceCaps的FlipComplex属性必须设置为True,以告诉系统这个主表面用于翻转模式的显示,此外,其BackBufferCount也要进行设置,以告诉系统其有多少个后备缓冲表面,代码如下:
Dim vPrimarySurfaceCaps As New SurfaceCaps
vPrimarySurfaceCaps.PrimarySurface = True
vPrimarySurfaceCaps.VideoMemory = True
vPrimarySurfaceCaps.Flip = True
vPrimarySurfaceCaps.Complex = True
Dim vPrimarySurfaceDesc As New SurfaceDescription(vPrimarySurfaceCaps)
vPrimarySurfaceDesc.BackBufferCount = 1
vPrimarySurface = New Surface(vPrimarySurfaceDesc, vDev)


初始化后备表面时,我们不是采用New来获取实例,而是使用主表面的GetAttachedSurface方法获取表面,代码如下:
Dim vBackSurfaceCaps As New SurfaceCaps
vBackSurfaceCaps.BackBuffer = True
vBackSurfaceCaps.VideoMemory = True
vBackSurface = vPrimarySurface.GetAttachedSurface(vBackSurfaceCaps)


离屏表面就没有什么好说的了,因为是用于储存图片的,所以和前几个例子里面的初始化大致相同,关键色设置为紫红色(RGB(255,0,255)):
Dim vOffSurfaceCaps As New SurfaceCaps
vOffSurfaceCaps.OffScreenPlain = True
vOffSurfaceCaps.VideoMemory = True
Dim vOffSurfaceDesc As New SurfaceDescription(vOffSurfaceCaps)
vOffSurface = New Surface("C:\ball.bmp", vOffSurfaceDesc, vDev)
'设置关键色以去除背景
Dim vColorKey As New ColorKey
vColorKey.ColorSpaceHighValue = RGB(255, 0, 255)
vColorKey.ColorSpaceLowValue = RGB(255, 0, 255)


下面是小球的一些属性的初始化,我们让小球的初始位置和速度都随机化:
Randomize()
For i As Integer = 0 To BALL_COUNT
        X(i) = Int(Rnd() * 620)
        Y(i) = Int(Rnd() * 460)
        SpeedX(i) = -5 + Int(Rnd() * 10)
        SpeedY(i) = -5 + Int(Rnd() * 10)
Next


最后是绘图线程的初始化,绘图线程指向Draw方法,这一点也比较简单:
Running = True
DrawThread = New Thread(AddressOf Draw)
DrawThread.Start()


===============华丽的分割线===============
下面就是程序的重点了,Draw方法的实现,我们将要实现的语句放进一个WHILE循环中,并使用Running变量控制线程的运行,线程每绘制一次便处理一次小球的动作,如此循环便实现了小球的运动过程,需要注意的是,因为我们采用的是全屏独占模式,一旦绘图过程中产生异常,程序是无法切换出去的,此时程序便会产生假死情况,貌似只有重启能够解决问题,所以这里是很危险的,因此我们将绘制语句放入Try块中,一旦出现异常必须强制结束程序:
While Running
        Try
            '将各小球重复地从离屏表面中绘制到后备表面上
            For i As Integer = 0 To BALL_COUNT
                vBackSurface.DrawFast(X(i), Y(i), vOffSurface, New Rectangle(0, 0, 20, 20), DrawFastFlags.SourceColorKey)
            Next
            '让主表面实现翻转,从而将后备表面中的图像绘制到主表面上
            vPrimarySurface.Flip(Nothing, FlipFlags.NoVSync)
            '清除后备表面中的图片
            vBackSurface.ColorFill(Color.Black)
        Catch ex As Exception
            MsgBox("程序出现异常,被迫退出。")
            ExitProg()
            Exit Sub
         End Try
        '小球处理位置
        For i As Integer = 0 To BALL_COUNT
            X(i) += SpeedX(i)
            Y(i) += SpeedY(i)
                If X(i) <= 0 Then
                X(i) = 0
                SpeedX(i) = -SpeedX(i)
            End If
            If X(i) >= 620 Then
                X(i) = 620
                SpeedX(i) = -SpeedX(i)
            End If
            If Y(i) <= 0 Then
                Y(i) = 0
                SpeedY(i) = -SpeedY(i)
            End If
            If Y(i) >= 460 Then
                Y(i) = 460
                SpeedY(i) = -SpeedY(i)
            End If
        Next
        '线程延迟休息
        Thread.Sleep(50)
End While


小球的处理原理是在碰到屏幕边缘时相对的方向反向改变。


===============华丽的分割线===============
下面处理程序退出时内存释放的过程,在这里需要注意的是,在释放内存之前必须先等待线程结束,以免引起共享冲突问题,代码如下:
'结束程序
Running = False
'等待线程结束
While DrawThread.ThreadState <> ThreadState.Stopped
End While
'释放内存
vOffSurface.Dispose()
vOffSurface = Nothing
vBackSurface.Dispose()
vBackSurface = Nothing
vPrimarySurface.Dispose()
vPrimarySurface = Nothing
vDev.Dispose()
vDev = Nothing
X = Nothing
Y = Nothing
SpeedX = Nothing
SpeedY = Nothing
Running = Nothing
DrawThread = Nothing
'退出程序
End


===============华丽的分割线===============
这里我们设定在程序运行时按ESC键或者被窗口被关闭时执行内存释放并退出程序,因此添加如下两个方法:
Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
    '按ESC退出程序
    If e.KeyCode = Keys.Escape Then
        ExitProg()
    End If
End Sub


Private Sub Form1_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
    ExitProg()
End Sub


===============华丽的分割线===============
以下是程序的全部代码:
Imports Microsoft.DirectX
Imports Microsoft.DirectX.DirectDraw
Imports System.Threading


Public Class Form1
    Inherits System.Windows.Forms.Form


    Dim vDev As Device
    Dim vPrimarySurface As Surface
    Dim vBackSurface As Surface
    Dim vOffSurface As Surface


    Public Const BALL_COUNT As Integer = 50DirectX与VB.NET编程(十三)全屏动画
    Dim X(BALL_COUNT) As Integer
    Dim Y(BALL_COUNT) As Integer
    Dim SpeedX(BALL_COUNT) As Integer
    Dim SpeedY(BALL_COUNT) As Integer


    Dim Running As Boolean
    Dim DrawThread As Thread


#Region " Windows 窗体设计器生成的代码 "


    Public Sub New()
        MyBase.New()


        '该调用是 Windows 窗体设计器所必需的。
        InitializeComponent()


        '在 InitializeComponent() 调用之后添加任何初始化


    End Sub


    '窗体重写 dispose 以清理组件列表。
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub


    'Windows 窗体设计器所必需的
    Private components As System.ComponentModel.IContainer


    '注意: 以下过程是 Windows 窗体设计器所必需的
    '可以使用 Windows 窗体设计器修改此过程。
    '不要使用代码编辑器修改它。
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        '
        'Form1
        '
        Me.AutoScaleBaseSize = New System.Drawing.Size(6, 14)
        Me.ClientSize = New System.Drawing.Size(216, 128)
        Me.Name = "Form1"
        Me.Text = "动画"


    End Sub


#End Region


    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        '初始化设备
        vDev = New Device(CreateFlags.Default)
        vDev.SetCooperativeLevel(Me, CooperativeLevelFlags.FullscreenExclusive)
        vDev.SetDisplayMode(640, 480, 2 ^ 24, 60, True)


        '初始化主表面
        Dim vPrimarySurfaceCaps As New SurfaceCaps
        vPrimarySurfaceCaps.PrimarySurface = True
        vPrimarySurfaceCaps.VideoMemory = True
        vPrimarySurfaceCaps.Flip = True
        vPrimarySurfaceCaps.Complex = True
        Dim vPrimarySurfaceDesc As New SurfaceDescription(vPrimarySurfaceCaps)
        vPrimarySurfaceDesc.BackBufferCount = 1
        vPrimarySurface = New Surface(vPrimarySurfaceDesc, vDev)


        '初始化后备表面
        Dim vBackSurfaceCaps As New SurfaceCaps
        vBackSurfaceCaps.BackBuffer = True
        vBackSurfaceCaps.VideoMemory = True
        vBackSurface = vPrimarySurface.GetAttachedSurface(vBackSurfaceCaps)


        '初始化离屏表面
        Dim vOffSurfaceCaps As New SurfaceCaps
        vOffSurfaceCaps.OffScreenPlain = True
        vOffSurfaceCaps.VideoMemory = True
        Dim vOffSurfaceDesc As New SurfaceDescription(vOffSurfaceCaps)
        vOffSurface = New Surface("C:\ball.bmp", vOffSurfaceDesc, vDev)
        '设置关键色以去除背景
        Dim vColorKey As New ColorKey
        vColorKey.ColorSpaceHighValue = RGB(255, 0, 255)
        vColorKey.ColorSpaceLowValue = RGB(255, 0, 255)
        vOffSurface.SetColorKey(ColorKeyFlags.SourceDraw, vColorKey)


        '随机初始化各小球位置
        Randomize()
        For i As Integer = 0 To BALL_COUNT
            X(i) = Int(Rnd() * 620)
            Y(i) = Int(Rnd() * 460)
            SpeedX(i) = -5 + Int(Rnd() * 10)
            SpeedY(i) = -5 + Int(Rnd() * 10)
        Next


        '初始化设置线程
        Running = True
        DrawThread = New Thread(AddressOf Draw)
        DrawThread.Start()
    End Sub


    Private Sub Draw()
        While Running
            Try
                '将各小球重复地从离屏表面中绘制到后备表面上
                For i As Integer = 0 To BALL_COUNT
                    vBackSurface.DrawFast(X(i), Y(i), vOffSurface, New Rectangle(0, 0, 20, 20), DrawFastFlags.SourceColorKey)
                Next
                '让主表面实现翻转,从而将后备表面中的图像绘制到主表面上
                vPrimarySurface.Flip(Nothing, FlipFlags.NoVSync)
                '清楚后备表面中的图片
                vBackSurface.ColorFill(Color.Black)
            Catch ex As Exception
                MsgBox("程序出现异常,被迫退出。")
                ExitProg()
                Exit Sub
            End Try
            '小球处理位置
            For i As Integer = 0 To BALL_COUNT
                X(i) += SpeedX(i)
                Y(i) += SpeedY(i)
                If X(i) <= 0 Then
                    X(i) = 0
                    SpeedX(i) = -SpeedX(i)
                End If
                If X(i) >= 620 Then
                    X(i) = 620
                    SpeedX(i) = -SpeedX(i)
                End If
                If Y(i) <= 0 Then
                    Y(i) = 0
                    SpeedY(i) = -SpeedY(i)
                End If
                If Y(i) >= 460 Then
                    Y(i) = 460
                    SpeedY(i) = -SpeedY(i)
                End If
            Next
            '线程延迟休息
            Thread.Sleep(50)
        End While
    End Sub


    Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
        '按ESC退出程序
        If e.KeyCode = Keys.Escape Then
            ExitProg()
        End If
    End Sub


    Private Sub Form1_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
        ExitProg()
    End Sub


    Private Sub ExitProg()
        '结束程序
        Running = False
        '等待线程结束
        While DrawThread.ThreadState <> ThreadState.Stopped
        End While
        '释放内存
        vOffSurface.Dispose()
        vOffSurface = Nothing
        vBackSurface.Dispose()
        vBackSurface = Nothing
        vPrimarySurface.Dispose()
        vPrimarySurface = Nothing
        vDev.Dispose()
        vDev = Nothing
        X = Nothing
        Y = Nothing
        SpeedX = Nothing
        SpeedY = Nothing
        Running = Nothing
        DrawThread = Nothing
        '退出程序
        End
    End Sub
End Class


===============华丽的分割线===============
程序的运行效果如下:



===============华丽的分割线===============
你可以尝试一下不使用翻转,而是直接在主表面上绘制再清除,你会发现即使有DirectDraw硬件加速的情况下依然会有闪烁,而且闪烁的程度会随内容的增多而加剧,而如果使用翻转模式的话顶多只是帧数减低,不会出现闪烁的情况,可见翻转的重要性。DirectX与VB.NET编程(十三)全屏动画
此时翻转的内容已经讲完了,可能你已经觉得翻转只能在全屏独占下使用,局限性过大,如果需要在窗口下进行我们还必须使用仿翻转模式和剪切,这是下次的内容了。



群贤毕至

访客