• Thứ Sáu, 06/08/2004 20:57 (GMT+7)

    Trò chơi xếp hình với VB6


    Chương trình sẽ cắt một hình cho trước ra thành nhiều mảnh có hình dạng ngẫu nhiên, và người chơi sẽ phải ráp từng mảnh lại. Trò chơi thật đơn giản nhưng lập trình để tạo trò chơi này không đơn giản chút nào. Bài viết giới thiệu một chương trình như vậy được thực hiện với VB6.

    Tạo Activex control

    Tạo một Standard Project mới. Vào menu Project/Add Usercontrol để thêm một Usercontrol mới (đặt tên tùy thích, ở đây tôi đặt là ShapeControl, AutoRedraw=True). Để tạo ShapeControl (SC) có hình dạng đặc biệt cần dùng 4 hàm API: CreateRectRgn, CreateEllipseticRgn, CombineRgn và SetWindowRgn. Khai báo các hàm trên trong SC. Khai báo thêm hàm DeleteObject dùng để hủy đối tượng đã tạo để giải phóng bộ nhớ.

    Để thuận tiện cho việc "tạo hình" cho SC, ta sử dụng cấu trúc để lưu trữ dữ liệu các cạnh:

    Private Type CauTruc

     Top As Long

      Bottom As Long     

      Left As Long

      Right As Long

    End Type

    Function sau tạo hình cho SC:

    Private Function CreateFormRegion(ScaleX As Single, ScaleY As Single, OffsetX As Integer, OffsetY As Integer, DrawStyle As CauTruc) As Long

    Hình chữ nhật chính có tọa độ (22,22)-(77,77), hình ellipse có bán kính lớn=22 và bán kính nhỏ=13 (H.1). Do hình ảnh chúng ta muốn cắt có chiều rộng và chiều dài bất kì nên ta phải nhân tỉ lệ này cho chiều dài, rộng thực của mỗi miếng hình nhỏ (bằng với chiều dài và chiều rộng của SC, do SC sẽ là mỗi miếng hình nhỏ).

    Các giá trị Top, Left, Right, Bottom trong CauTruc có thể nhận các giá trị -1, 0, 1. Nếu Top nhận giá trị 1 có nghĩa là hình chữ nhật sẽ kết hợp với hình ellipse (H.2), nếu nhận giá trị 0 nghĩa là không có hình ellipse, còn giá trị -1 thì ellipse sẽ cắt hình chữ nhật (H.3) (tương tự cho Left, Right và Bottom). Bạn sẽ thấy cách qui định giá trị này rất hữu ích trong các bước sau.

    Sở dĩ khai báo Function CreateFormRegion là Private vì nếu bạn chuyển qua Public thì khi chạy chương trình, VB sẽ báo lỗi là kiểu người dùng định nghĩa (CauTruc) không được làm đối số (chỉ khi nào bạn tạo ActiveX Control riêng và biên dịch thành *.ocx mới không gặp lỗi này). Do đó ta phải tạo 1 Sub có tính Public gọi Function này. Sub này sẽ trở thành một Method của SC.

    Public Sub DrawShape(vLeft As Long, vTop As Long, vRight As Long, vBottom As Long)

     Dim DrawStyle As CauTruc

     Dim nRet As Long

     DrawStyle.Left = vLeft

     DrawStyle.Top = vTop

     DrawStyle.Right = vRight

     DrawStyle.Bottom = vBottom

     nRet = SetWindowRgn(UserControl.hwnd, CreateFormRegion(1, 1, 0, 0, DrawStyle), True)

    End Sub

    Bây giờ thêm các Property phục vụ cho việc đồ họa:

    Public Property get hWnd() as Long

      hWnd=Usercontrol.hWnd

    End Property

    Public Property get hDC() as Long

      hDC=Usercontrol.hDC

    End Property

    Để di chuyển SC (không có TitleBar)  dễ dàng, bạn phải "capture" chuột và mô phỏng việc nhấn và rê chuột trái (khai báo thêm 2 API là ReleaseCapture và SendMessage ở phần khai báo các hàm API):

    Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)

      ReleaseCapture

      SendMessage UserControl.hwnd, &HA1, 2, 0&

    End Sub

    Ngoài ra, để có thêm nhiều sự kiện (Event) như MouseMove, MouseUp, Resize,... bạn khai báo các sự kiện này ở đầu code:

      Public <Tên sự kiện> (Các đối số)

    Và khi UserControl xảy ra sự kiện nào thì bạn báo hiệu (RaiseEvent) sự kiện đó

    Phần Form

    Vấn đề kế tiếp là phải tạo các SC sao cho hợp lí vì các cạnh của SC (hay nói chính xác là các hình ellipse) mang các giá trị ngẫu nhiên (nếu không, các SC sẽ có cùng hình dạng).Trước hết tạo một hàm trả về giá trị ngẫu nhiên cho các cạnh:

    Private Function NgauNhien() As Long

     Randomize

     If Rnd < 0.3333 Then

      NgauNhien = -1

     ElseIf Rnd > 0.3333 And Rnd < 0.6666 Then

      NgauNhien = 0

     Else

      NgauNhien = 1

     End If

    End Function

    Hàm Rnd sẽ trả về một con số trong khoảng giá trị [0-1] và Randomize sẽ sinh một con số mới phục vụ cho việc lấy Rnd. Nếu không dùng Randomize thì mỗi lần form Load, Rnd sẽ trả về các con số y hệt cũ.

    Trở lại vấn đề tạo các cạnh sao cho hợp lí. Mời bạn xem hình 4. Ở đây tôi lần lượt đánh dấu các hình theo thứ tự Trái-Đỉnh-Phải-Đáy. Theo đó, hai SC xếp trên cùng hàng sẽ có Phải trước+Trái sau=0. Hai SC trên cùng cột sẽ có Đáy trên+Đỉnh dưới=0. Cách thuận tiện nhất là lưu các dữ liệu này vào mảng 2 chiều cấu trúc (mỗi phần tử mảng là 1 cấu trúc theo kiểu CauTruc), sau đó tạo các SC theo các phần tử này. Chúng ta khai báo lại kiểu CauTruc

    Private Type CauTruc

      Top as Long

      Bottom as Long

      Left as Long

      Right as Long

    End Type

    Dim a(100,100) As CauTruc 'mảng có tối đa 101*101 phần tử

    Sau cùng là chia hình ra thành nhiều mảnh nhỏ (thực chất mỗi mảnh nhỏ là một SC) và dùng hàm BitBlt để copy vùng ảnh bên ảnh đích (ảnh cần cắt) qua SC.

    Private Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, ByVal X As      Long, ByVal Y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal  hSrcDC As Long, ByVal xSrc As Long, ByVal ySrc As Long, ByVal dwRop As Long)  As Long

    Private Const SRCCOPY = &HCC0020

    - hDestDC là hDC của thiết bị nhận khối hình, X là hoành độ trên trái điểm bắt đầu nhận khối hình, Y là tung độ trên trái điểm bắt đầu nhận khối hình, nWidth là chiều rộng khối hình, nHeight là chiều cao khối hình (tất cả các đối số này là của SC).

    - hSrcDC là hDC của hình gốc, xSrc là hoành độ trên trái của khối hình truyền đi, ySrc là tung độ trên trái của khối hình truyền đi (của hình đích, cụ thể là một PictureBox).

    - dwRop là cờ xác định hoạt động quét, ở đây là chỉ cần kiểu quét y nguyên như cũ (SRCCOPY).

    Sub sau sẽ sinh ra các mảnh hình bất kì:

    Private Sub SinhHinh(ByVal wCount As Integer, ByVal hCount As Integer)

    wCount là số mảnh muốn cắt theo chiều ngang, hCount là số mảnh muốn cắt theo chiều dọc. picPicture là một PictureBox chứa hình đích. Nếu bạn nghĩ muốn chia tấm hình ra thành wCount*hCount mảnh, mỗi mảnh(hay SC control) có chiều dài = chiều dài tấm hình/wCount và chiều rộng = chiều rộng tấm hình/hCount, thì kết quả bạn đạt được sẽ không như ý muốn. Phần chúng ta nhìn thấy được sau khi SC đã tạo hình chỉ là hình chữ nhật trong cùng (= 5/9 chiều dài của SC) và các hình ellipse (nếu có). Do đó mỗi SC sẽ có chiều dài (hoặc rộng) thực = (9/5*chiều dài tấm hình)/wCount (hoặc /hCount). Vì ta không biết người sử dụng sẽ cắt ra bao nhiêu mảnh nên đầu tiên phải đưa lên form một SC và đặt index=0. Sau đó sẽ nạp các SC còn lại tương ứng với số mảnh. Hàm ChieuDai(sLen) sẽ trả về 0 nếu sLen<=0 (cụ thể ở đây là =-1,0), trường hợp khác trả về 1.

    Private Function ChieuDai(ByVal sLen) As Integer

     If sLen <= 0 Then

      ChieuDai = 0

     Else

      ChieuDai = 1

     End If

    End Function

    Sở dĩ có hàm ChieuDai này là vì khi copy mảnh hình từ picPicture ta không biết đưa khối hình vào SC ở tọa độ X nào cho hợp lí (vì hình ellipse trái khi có khi không do tạo ngẫu nhiên), do đó tọa độ X ở đây sẽ là tọa độ X của hình chữ nhật trong cùng (2/9*uW) trừ chiều dài hình ellipse trái (nếu có, cũng =2/9*uW), tương tự cho tọa độ Y của SC, tọa độ X,Y của picPicture. Còn chiều dài và cao phụ thuộc vào ellipse phải, trái, đỉnh, đáy, nên ngoài kích thước của hình chữ nhật trong cùng (5/9*uW) phải cộng thêm chiều dài các ellipse (nếu có).

    Bây giờ trò chơi cơ bản đã hoàn tất. Việc còn lại là thiết kế giao diện tùy vào thẩm mỹ của mỗi người. Điều căn bản của trò chơi này là bạn phải có một khu vực chính dành cho các SC để tạo lại hình, một PictureBox của hình nguồn làm nhiệm vụ hướng dẫn người chơi (hình này nhỏ hơn hình gốc) và một vùng chứa các hình đã cắt.

      THỦ TỤC SINH RA CÁC MẢNH HÌNH BẤT KÌ:  
     

    Private Sub SinhHinh(ByVal wCount As Integer, ByVal hCount As Integer)
    On Error Resume Next
    Dim i As Integer
    Dim j As Integer
    Dim uW As Long
    Dim uH As Long
    uW = (9 / 5) * (picPicture.Width / wCount)
    uH = (9 / 5) * (picPicture.Height / hCount)
     sc(0).Width = uW
     sc(0).Height = uH
    Static nCount
    nCount = -1
    For i = 1 To wCount * hCount - 1
     Load sc(i)
     sc(i).Visible=False
    Next
    For i = 0 To hCount - 1
     For j = 0 To wCount - 1
       a(i, j).Top = -a(i - 1, j).Bottom
       a(i, j).Left = -a(i, j - 1).Right
       If j = wCount - 1 Then
        a(i, j).Right = 0
       Else
          a(i, j).Right = NgauNhien
        End If
        If i = hCount - 1 Then
       

     

        a(i, j).Bottom = 0
       Else
          a(i, j).Bottom = NgauNhien
        End If
      Next
     Next
    For i = 0 To hCount - 1
     For j = 0 To wCount - 1
         nCount = nCount + 1
         BitBlt sc(nCount).hDc, _
         (2 / 9) * uW - (2 / 9) * uW * ChieuDai(a(i, j).Left), (2 / 9) * uH - (2 / 9) - (2 / 9) * uH * ChieuDai(a(i, j).Top), _
         (5 / 9) * uW + (2 / 9) * uW * ChieuDai(a(i, j).Right) + (2 / 9) * uW * ChieuDai(a(i, j).Left), _
         (5 / 9) * uH + (2 / 9) * uH * ChieuDai(a(i, j).Bottom) + (2 / 9) * uH * ChieuDai(a(i, j).Top), _
         picPicture.hDc, _
         (5 / 9) * uW * j - (2 / 9) * uW * ChieuDai(a(i, j).Left), _
         (5 / 9) * uH * i - (2 / 9) * uH * ChieuDai(a(i, j).Top), SRCCOPY
        sc(nCount).DrawShape a(i, j).Left, a(i, j).Top, a(i, j).Right, a(i, j).Bottom
        sc(nCount).Visible = True    
     Next j
    Next i
    End Sub

     

    Mẹo vặt

    1. Cũng như bao trò chơi khác, trò chơi của chúng ta cũng phải có "cheat". Trong ShapeControl, bạn tạo một Label và đặt vào chính giữa của SC với thuộc tính Visible=False. Mỗi khi sub SinhHinh trong Form được gọi thì bạn thêm vào sc(i).Label1.caption=nCount để đánh dấu. Mỗi khi gọi cheat (do bạn quy định) thì chỉ việc set thuộc tính Visible của Label thành True.

    2. Đối với những hình ảnh lớn (800x600), nếu không đủ diện tích màn hình thì bạn dùng hàm API CopyImage với thông số chiều rộng và chiều cao mới để tạo hình nhỏ hơn ban đầu cho picPicture và cũng dùng hàm này cho hình hướng dẫn.

    3. Khi nạp các SC trong Sub SinhHinh thì các SC này nằm chồng lên nhau, do đó bạn phải sắp xếp lại các SC này một cách ngẫu nhiên. Để tiết kiệm không gian thì các SC này nên nằm trong một PictureBox (PB), PB này phải đủ rộng để chứa tất cả các SC bạn tạo ra và PB này phải nằm trong một Container (1 PB khác, 1 Usercontrol khác,...) kết hợp với ScrollBar. Để hình dung việc này thì bạn cứ xem thanh TaskPane (Ctrl+F1) của bộ Office và làm theo. ScrollBar sẽ xác định tọa độ Top của PB chứa các SC.

    Về việc sắp xếp các SC ngẫu nhiên để không gây nhàm chán, bạn tạo 2 mảng một chiều. Giả sử chúng ta có 20 SC thì bạn sinh mảng a(0 to 19) và giá trị các phần tử = trị số (a(0)=0, a(1)=1,...), sau đó đổi chỗ ngẫu nhiên các phần tử thì ta có mảng ngẫu nhiên.

    Mảng b(0 to 19) sẽ mang lần lượt các giá trị của mảng a() (các giá trị này chính là thứ tự của các SC, b(0)=b(a(0))=6) rồi định lại tọa độ Top và Left. Muốn đưa các SC từ PB ra ngoài vùng sắp xếp chính thì dùng hàm SetParent <hWnd cũ>,<hWnd mới>.

    Bạn có thể tải về mã nguồn chương trình mẫu trên website của TGVT-PCW VN Online.

      FUNTION ĐẢM NHIỆM VIỆC TẠO HÌNH CHO SC:  
     

    Private Function CreateFormRegion(ScaleX As Single, ScaleY As Single, OffsetX As Integer, OffsetY As Integer, DrawStyle As CauTruc) As Long
    Dim HolderRegion As Long, ObjectRegion As Long, nRet As Long, Counter As Integer
        Dim uW As Long
        Dim uH As Long
        ResultRegion = CreateRectRgn(0, 0, 0, 0)
        HolderRegion = CreateRectRgn(0, 0, 0, 0)
        uW = UserControl.Width / 15
        uH = UserControl.Height / 15

    'Hình chữ nhật chính
        '22/99,22/99,77/99,77/99
        ObjectRegion = CreateRectRgn((2 / 9) * uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (2 / 9) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, (7 / 9) * uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (7 / 9) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY)
        nRet = CombineRgn(ResultRegion, ObjectRegion, ObjectRegion, RGN_COPY)
        DeleteObject ObjectRegion

    'Hình ellipse trái
     If DrawStyle.Left <> 0 Then
        ObjectRegion = CreateEllipseticRgn(0 * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (37 / 99) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, (4 / 9) * uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (7 / 11) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY)
        nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, RGN_COPY)
        nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 - DrawStyle.Left)
        DeleteObject ObjectRegion
     End If

      'Hình ellipse trên đỉnh
     If DrawStyle.Top <> 0 Then
        ObjectRegion = CreateEllipseticRgn((37 / 99) * uW * ScaleX * 15 / Screen.TwipsPerPixelX +

     

     OffsetX, 0 * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, uW * (7 / 11) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (4 / 9) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY)
        nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, RGN_COPY)
        nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 - DrawStyle.Top)
        DeleteObject ObjectRegion
     End If

     'Hình ellipse cạnh phải
     If DrawStyle.Right <> 0 Then
       ObjectRegion = CreateEllipseticRgn(uW * (5 / 9) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (37 / 99) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (7 / 11) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY)
        nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, RGN_COPY)
        nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 - DrawStyle.Right)
        DeleteObject ObjectRegion
     End If

     'Hình ellipse cạnh đáy
     If DrawStyle.Bottom <> 0 Then
       ObjectRegion = CreateEllipseticRgn(uW * (37 / 99) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (5 / 9) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, uW * (7 / 11) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY)
        nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, RGN_COPY)
        nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 -DrawStyle.Bottom)
        DeleteObject ObjectRegion
     End If
        DeleteObject HolderRegion
        CreateFormRegion = ResultRegion
    End Function

     

    Nguyễn Quốc việt
    Bamby084@hotmail.com        

    ID: A0407_118