C# – Viết chương trình ghép hình đơn giản-p1

Đây là một trờ chơi đơn giản khá quen thuộc, để tạo được một game dạng này đòi hỏi bạn phải có một chút kĩ năng, biết vận dụng những kiến thức C# và thuật toán căn bản.

Trò chơi là một khung ảnh hình vuông bao gồm các ảnh nhỏ ghép lại và một ô trống để bạn có thể di chuyển các ô khác.

Luật chơi khá đơn giản, bạn chỉ cần sắp xếp các ô vuông hình (đã bị xáo trộn) về lại hình ban đầu. Để sắp xếp bạn chỉ được di chuyển các ô vuông này theo 4 hướng (N-E-W-S) bằng cách thế với vị trí ô trống (ô có màu gray trong hình).

Thoạt nhìn bạn có thể hơi phức tạp, tuy nhiên bạn sẽ thấy nó khá dễ dàng sau khi đọc và hiểu hướng dẫn cách làm sau đây.

I)      Phân tích

Trong ví dụ này bạn có thể đoán được là ta sẽ tạo một mảng các PictureBox để chứa các phần ảnh nhỏ của bức ảnh lớn. Ngoài ra, bạn sẽ phải dùng một phương pháp nào đó để trộn các khung ảnh nhỏ này. Ở đây bạn có thể trộn theo một quy luật nào đó, lẽ dĩ nhiên điều này làm cho trò chơi trở nên kém hấp dẫn nếu chơi lại những lần sau. Vì thế có lẽ tốt hơn ta sẽ dùng cách trộn ngẫu nhiên.

Để di chuyển các khung ảnh nhỏ (từ đây ta sẽ gọi tắt là khung ảnh) trò chơi sẽ cung cấp 2 cách thức: một là nhấn vào mũi tên tương ứng trên form, hai là sử dụng 4 nút mũi tên trên bàn phím.

Với bước kiểm tra xem người chơi đã sắp xếp hoàn thành chưa, cách đơn giản nhất là lặp qua mảng các khung ảnh và so sánh vị trí của nó với vị trí chính xác lẽ ra nó phải đứng. Bạn có thể dùng một mảng số để lưu vị trí các khung ảnh này lại, tuy nhiên tại sao ta lại không tận dụng thuộc tính tag của đối tượng để lưu nhỉ. Bạn có thể chọn lựa 1 trong 2 cách này hoặc cách thứ 3 nếu bạn thích, tuy nhiên trong bài này ta sẽ lưu trong tag.

II)  Thiết kế:

1)      Giao diện:

Bạn đã tìm hiểu sơ qua cách thức hoạt động của chương trình. Bây giờ bạn hãy bắt tay thực hiện bằng cách mở một IDE hỗ trợ lập trình C# và tạo giao diện như trên.

Mô tả chức năng các control:

-mnOpen (Mở file ảnh)           : load file ảnh từ đĩa vào chương trình.

-mnStart (Bắt đầu)                  :Tiến hành xáo trộn các khung ảnh để bắt đầu

-mnReset (Chơi lại)                 : Khôi phục các khung ảnh về vị trí ban đầu.

-Các mũi tên trên form là các Label tương ứng với các hướng di chuyển

2)      Viết mã nguồn:

Trước hết hãy đọc và cố gắng nhớ những biến sau để chắc rằng bạn không phải quay lại tra lý lịch khi bắt gặp chúng xuất hiện đâu đó trên con đường bằng phẳng bạn sắp đi tới.

/// <summary>

/// ảnh gốc

/// </summary>

private Bitmap ImageFile;

/// <summary>

/// Kích thước của 1 khung ảnh

/// </summary>

private int cellSize;

/// <summary>

/// Số lượng khung hình theo chiều ngang và dọc của khung hình lớn(hình vuông)

/// </summary>

private const int CELL_COUNT = 5;

/// <summary>

/// Vị trí của khung hình trống

/// </summary>

private int emptyCellIndex = CELL_COUNT * CELL_COUNT – 1;

/// <summary>

/// Mảng các khung hình

/// </summary>

private PictureBox[] picCell = new PictureBox[CELL_COUNT * CELL_COUNT];

Tôi thường có thói quen cho các thuộc tính này lên đầu lớp và bao chúng lại với tên Properties, vậy nếu bạn đã nhớ chúng rồi thì hãy thêm 2 dòng in đậm sau vào trước và sau đoạn mã trên:

#region Properties

// ….

#endregion

Trong chương trình này bạn phải dùng một Panel để chứa các khung ảnh, hãy tạo ra các khung ảnh của bạn và thêm chúng vào panel

for (int i = 0; i < picCell.Length; i++)

{

picCell[i] = new PictureBox();

panelPicture.Controls.Add(picCell[i]);

}

Chúng ta sẽ tiếp tục bằng hàm load ảnh từ file vào các khung ảnh, hãy xem đoạn mã sau

 

/// <summary>

/// Nạp ảnh từ file, tính toán kích thước và phân chia ảnh

/// </summary>

/// <param name=”fileName”></param>

void LoadPicture(string fileName)

{

if (!System.IO.File.Exists(fileName))

{

MessageBox.Show(“Không tìm thấy file \n” + fileName);

return;

}

try

{

mnStart.Enabled = true;

mnReset.Enabled = true;

// Tạo ảnh bitmap từ file

Bitmap bmp = new Bitmap(fileName);

// Kích thước lớn nhất của ảnh (width , height)

float maxSize;

float width,height;

maxSize = bmp.Size.Width > bmp.Size.Height ? bmp.Size.Width : bmp.Size.Height;

// Lấy kích thước ảnh

width = bmp.Size.Width;

height = bmp.Size.Height;

// hệ số để tăng giảm kích thước ảnh cho phù hợp

float heso=350/maxSize;

if (heso > 0)

{

// Thay đổi kích thước ảnh

width *= heso;

height *= heso;

}

// Nạp ảnh vào với kích thước mới được tính toán

ImageFile = new Bitmap(bmp,new Size((int)width,(int)height));

// Lấy kích thước lớn nhất của ảnh dựa vào chiều cao và chiều rộng

int imageSize = (ImageFile.Width < ImageFile.Height) ? ImageFile.Width : ImageFile.Height;

// Cắt ảnh cho vừa với số lượng khung hình

imageSize = imageSize – imageSize % CELL_COUNT;

// Tính độ rộng của 1 khung ảnh (khung ảnh có dạng hình vuông)

cellSize = imageSize / CELL_COUNT;

// Hiển thị các khung ảnh

mnReset_Click(null, null);

// Tính kích thước của panel chứa các khung ảnh

int size = cellSize * CELL_COUNT;

panelPicture.Size = new Size(size, size);

// Thay đổi kích thước form theo kích thước ảnh

this.Size = new Size(size+200, size+100);

}

catch (Exception ex)

{

MessageBox.Show(ex.ToString());

}

}

 

Các đoạn code tôi đã chú thích khá chi tiết nên có lẽ không cần giải thích nhiều, tuy nhiên nếu bạn suy nghĩ và đặt ra 1 câu hỏi nào đó, biết đâu nó có thể giúp chúng ta phát hiện ra vài điều lý thú ở đây.

 

Tiếp đến chúng ta sẽ viết lệnh để cắt bức ảnh gốc thành các khung ảnh nhỏ và thêm vào các PictureBox. Chúng ta sẽ cho thao tác này vào mnReset

// Khôi phục vị trí các khung ảnh về trạng thái ban đầu

private void mnReset_Click(object sender, EventArgs e)

{

// Phương pháp:

// cắt file ảnh gốc ra thành các phần nhỏ

// mỗi phần cho vào một khung ảnh (pictureBox)

// Xếp vị trí của khung ảnh trên form

// sử dụng thuộc tính tag của PictureBox để lưu lại số thứ tự (index) của khung ảnh

// Số thứ tự xác định vị trí thật sự của khung ảnh

// và là cơ sở để kiểm tra xem người chơi có xếp đúng vị trí ko

if (ImageFile != null)

{

// Số thự tự các khung ảnh

int cellIndex = 0;

for (int j = 0; j < CELL_COUNT; j++)

{

for (int i = 0; i < CELL_COUNT; i++)

{

try

{

// Tạo khung hình chữ nhật để cắt ảnh từ file ảnh gốc

// có vị trí cắt từ cột i dòng j

// và kích thước bằng với khung ảnh (cellSize)

Rectangle imageRect = new Rectangle(i * cellSize, j * cellSize, cellSize, cellSize);

// Xóa ảnh cũ của khung ảnh

picCell[cellIndex].Image = null;

// Không gán ảnh vào khung ảnh cuối cùng (khung ảnh trống dùng để di chuyển)

if (cellIndex != picCell.Length – 1)

// Gán ảnh vào khung ảnh, hàm Clone của đối tượng Bitmap sẽ cắt ra một phần

// ảnh dựa vào vị trí và kích thước khung chữ nhật ta truyền vào

picCell[cellIndex].Image = ImageFile.Clone(imageRect, PixelFormat.DontCare);

// Xếp vị trí cho khung ảnh, bằng vị trí cột, dòng

picCell[cellIndex].Location = new Point( i * cellSize, j * cellSize + 1);

// Tăng kích thước khung ảnh lên 1 để có đường biên giữa các khung ảnh

picCell[cellIndex].Size = new Size(cellSize + 1, cellSize + 1);

// Lưu số thứ tự của khung ảnh lại

picCell[cellIndex].Tag = cellIndex.ToString();

// Tăng số thứ tự

cellIndex++;

}

catch (Exception)

{

}

}

}

}

mnStart.Enabled = true;

// khung ảnh trống có vị trí cuối cùng

emptyCellIndex = CELL_COUNT * CELL_COUNT – 1;

}

Để hoán đổi vị trí các bức ảnh ta dùng cách hoán vị thuộc tính Image và Tag của hai PictureBox, như vậy các PictureBox vẫn giữ nguyên vị trí index trên panel, chỉ có index trong Tag thay đổi. Ở đây ta cho vào trong sự kiện của mnStart

// xáo trộn ngẫu nhiên vị trí các khung ảnh

private void mnStart_Click(object sender, EventArgs e)

{

mnStart.Enabled = false;

mnReset.Enabled = true;

// đối tượng bitmap tạm dùng cho việc hoán vị 2 khung ảnh

Bitmap bmp;

Random rnd = new Random();

// giá trị random lớn nhất bằng tổng số khung ảnh -1

// để ko xét đến khung ảnh trống

int maxValue = CELL_COUNT * CELL_COUNT-1;

// số lần xáo trộn khung ảnh = maxValue

for (int i = 0; i < maxValue; i++)

{

// index của khung ảnh nguồn và đích sẽ hoán vị

int indexSource = rnd.Next(maxValue);

int indexDest = rnd.Next(maxValue);

// nếu bằng nhau thì bỏ qua

if (indexSource == indexDest)

continue;

try

{

// lưu lại ảnh của khung ảnh nguồn

bmp = (Bitmap)picCell[indexSource].Image;

// lưu tag của khung ảnh nguồn

bmp.Tag= picCell[indexSource].Tag.ToString();

// bắt đầu hoán vị 2 khung ảnh

picCell[indexSource].Image = picCell[indexDest].Image;

picCell[indexSource].Tag = picCell[indexDest].Tag;

picCell[indexDest].Image = bmp;

picCell[indexDest].Tag = bmp.Tag;

}

catch (Exception)

{

}

}

}

Với phần mã để di chuyển các khung ảnh, bạn chỉ cần làm tương tự như phần hoán vị ảnh bên trên. Ví dụ khi người chơi nhấn mũi tên xuống thì công việc ta phải bắt chương trình làm là lấy vị trí khung ảnh phía trên ô trống và hoán vị nó với ô trống, tương tự với các hướng còn lại.

Để tạo phím tắt khi người chơi sử dụng bàn phím, bạn hãy override hàm ProcessDialogKey của đối tượng Form, phần lệnh đó có thể trông tương tự như sau:

 

protected override bool ProcessDialogKey(Keys keyData)

{

if (keyData == Keys.Up)

lblMove_Click(lblUp, EventArgs.Empty);

else if(keyData==Keys.Down)

lblMove_Click(lblDown, EventArgs.Empty);

else if (keyData == Keys.Left)

lblMove_Click(lblLeft, EventArgs.Empty);

else if (keyData == Keys.Right)

lblMove_Click(lblRight, EventArgs.Empty);

return base.ProcessDialogKey(keyData);

}

 

Để xác định hướng di chuyển khi người dùng nhấn phím, bạn hãy tạo ra một enum có tên là Direction với 4 hướng di chuyển.

 

public enum Direction { Left, Right, Up, Down };

Nhiều người có thói quen sử dụng sử dụng biến kiểu int hoặc string cho các mục đích dạng này, nhưng để là chương trình trong sáng và dễ dàng thay đổi, tốt nhất bạn hãy tạo 1 enum như trên.

Bạn cũng cần chú ý đến việc kiểm tra tính hợp lệ của việc di chuyển. Chẳng hạn nếu như ô trống ở hàng trên cùng thì hãy chắc chắn rằng việc người dùng nhấn mũi tên xuống không ném vào mắt họ một ngoại lệ nào đó.

 

Mọi việc có vẻ như đã hoàn tất, chúng ta sẽ kết thúc phần hướng dẫn thiết kế này bằng hàm kiểm tra người chơi đã sắp xếp xong chưa. Hàm này không phức tạp và bạn có thể tự viết lấy. Với mỗi nước di chuyển của người chơi, đừng bỏ lỡ việc kiểm tra họ đã sắp xếp hoàn thành bức tranh chưa.

 

/// <summary>

/// Hàm kiểm tra thắng

/// </summary>

/// <returns></returns>

private bool CheckFinish()

{

for (int i = 0; i < picCell.Length; i++)

{

// Nếu có ít nhất 1 ảnh sai ít nhất vị trí thì trả về fasle

if (picCell[i].Tag.ToString() != i.ToString())

return false;

}

return true;

}

I) Phần kết

Thật thú vị là bạn có thể dễ dàng tạo cho mình một trò chơi có ích và hấp dẫn, nếu sử dụng một chút sáng tạo, bạn có thể làm cho trò chơi thêm phần hấp dẫn bằng các tính năng cơ bản khác như cho phép thay đổi kích thước, số lượng khung ảnh, đếm giờ, lưu số điểm cao nhất…

Trong dịp tới, tôi sẽ trình bày cách tạo một trò chơi ghép ảnh khác bằng cách dùng chuột kéo thả. Và như bạn có thể đoán, cách thực hiện nó cũng bắt nguồn từ trò chơi này.

 

http://yinyang-it.tk

5/6/2009

/// <summary>
/// ảnh gốc
/// </summary>
private Bitmap ImageFile;
/// <summary>
/// Kích thước của 1 khung ảnh
/// </summary>
private int cellSize;
/// <summary>
/// Số lượng khung hình theo chiều ngang và dọc của khung hình lớn(hình vuông)
/// </summary>
private const int CELL_COUNT = 5;
/// <summary>
/// Vị trí của khung hình trống
/// </summary>
private int emptyCellIndex = CELL_COUNT * CELL_COUNT – 1;
/// <summary>
/// Mảng các khung hình
/// </summary>
private PictureBox[] picCell = new PictureBox[CELL_COUNT * CELL_COUNT];

27 thoughts on “C# – Viết chương trình ghép hình đơn giản-p1

  1. Yinyang này, cậu đã nghĩ đến đúng sai của thuật toán xáo trộn các mảnh ghép chưa? Nếu sử dụng hàm random như vậy liệu từ đó có thể sắp thành bức tranh hoàn chỉnh ban đầu không?
    Cám ơn về bài viết rất chi tiết và rõ ràng.

    Trả lời
  2. Phần xáo trộn ảnh chỉ đơn giản là hoán vị ngẫu nhiên Image của hai Picture Box, không thể có mất mát hay nhầm lẫn dữ liệu.
    Tuy nhiên nếu bạn nhận thấy có điểm nào bất hợp lý hoặc sai sót thì vui lòng chỉ ra để mình sửa và cập nhật lại.
    Cảm ơn bạn!

    Trả lời
    • Ý mình không phải vậy. Ví dụ đơn giản nhé : Chia bức tranh thành 4 phần, coi ô 4 là ô trống:
      1|2
      3|4
      Bây giờ đổi lại thành:
      2|1
      3|4
      Có thể thấy là không thể trở về được bức như ban đầu được.
      Tức là ta phải xem lại một chút về phần thuật toán xáo trộn ảnh.
      Thân!

      Trả lời
      • Cảm ơn bạn.
        Mình nghiên cứu thử phần bạn hướng dẫn ở trên (trình độ mình có hạn, nên mình có câu hỏi, hi vọng bạn không thấy phiền ^^). Mình muốn hỏi bạn về đoạn này:

        // hệ số để tăng giảm kích thước ảnh cho phù hợp
        float heso=350/maxSize;

        if (heso > 0)
        {

        // Thay đổi kích thước ảnh
        width *= heso;
        height *= heso;
        }
        Mình chưa hiểu đoạn này, tại sao lại sử dụng số 350? Thực ra ý tưởng của đoạn này để làm gì, mình nghĩ là mình hiểu (hi vọng không phải hiểu nhầm), tuy nhiên mình không hiểu lý do dùng số 350, cũng như tại sao lại phải lấy số đó chia cho maxSize? Ý nghĩa thực sự của giá trị heso là gì?
        Cảm ơn về bài viết của bạn, thực sự nó giúp mình khá nhiều! ^^

      • Code vài năm trước của mình đúng là khá … amateur. Thực ra số 350 này nên là một hằng với tên như MAX_SIZE (kích thước tối đa của ảnh). Theo đó biến heso được dùng để resize ảnh cho không vượt quá giá trị MAX_SIZE. Ngoài ra thì việc kiếm tra > 0 cũng ko cần thiết, hơi máy móc một chút.

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s