Showing posts with label lập trình. Show all posts
Showing posts with label lập trình. Show all posts

Side project: Code2Image

Chào mọi người, thật lâu rồi đúng không. Nay rảnh rảnh không có việc gì làm ngó lại thấy cái blog từ hồi sinh viên vẫn có traffic nên hôm nay tôi lên một bài blog vớ vẩn chia sẻ cái tool side project tôi vừa code đợt rồi. Vào bài thôi!!!

Bạn cần một cách nhanh chóng và dễ dàng để chia sẻ các đoạn mã dưới dạng hình ảnh không? Không cần tìm đâu xa, hãy đến với Code2Image, công cụ mới sáng tạo chuyển đổi code thành hình ảnh có thể tùy chỉnh dễ dàng để giúp bạn có thể chèn mấy cái ảnh code của mình vào bất cứ đâu.

🌐 Trang web: https://codetoimage.vercel.app

Với Code2Image, bạn có thể dễ dàng

  • Thay đổi chủ đề - Lựa chọn từ nhiều tông màu và hình nền khác nhau để thể hiện mã của bạn tốt nhất. Có chế độ sáng và tối.
  • Chuyển đổi ngôn ngữ lập trình - Hỗ trợ tất cả các ngôn ngữ chính bao gồm Python, JavaScript, C++ và nhiều ngôn ngữ khác. Tô sáng cú pháp tự động phát hiện ngôn ngữ.
  • Tùy chỉnh phông chữ - Chọn họ phông chữ, kích thước và khoảng đệm để tối ưu hóa khả năng đọc.
  • Xuất dưới dạng PNG hoặc SVG - Tải xuống hình ảnh mã của bạn hoặc lưu chúng vào bảng tạm để dễ dàng chia sẻ.

Công nghệ sử dụng

  1. ReactJS
  2. NextUI
  3. TailwindCSS
  4. Zustand thư viện state management dễ dùng hơn Redux

Một vài hình ảnh







Bài viết có vậy thôi, lâu không viết nên không biết viết gì :))
Bye, hẹn gặp lại các bạn.

Lập trình game với libGDX - Tìm hiểu về Animation




Chào mừng các bạn đã quay trở lại với blog, mấy hôm nay bận dọn phòng để chuẩn bị năm học mới với cả lười nên hôm nay mình mới có thời gian viết bài (thực ra thì 90% là do lười chứ không phải là không có thời gian :v).
Thôi không xàm nữa, chúng ta sẽ cùng điểm cả một số ý chính của bài hôm nay:

  • Tìm hiểu về Animation
  • Cách sử dụng trọng libGDX.
Ok, chúng ta bắt đầu tìm hiểu nhé :D.


I, Tìm hiểu về Animation:

Chắc hẳn ai trong chúng ta đều trải qua 1 tuổi thơ, có thể êm đềm, hoặc cũng có thể rất dữ dội =)), và dù êm đềm hay dữ dội đi nữa thì chắc hẳn phim hoạt hình cũng là một phần không thể thiếu trong tuổi thơ của chúng ta (nói vậy thôi chứ bây giờ lớn vẫn xem :v).
Ủa? Mình có lạc đề không ta =))

Thực ra mình đưa ra ví dụ về phim hoạt hình để cho các bạn dễ dàng liên tưởng nhất đến khái niệm Animation mà thôi.
Như các bạn đã biết, người ta làm phim hoạt hình bằng cách sử dụng nhiều khung hình và cho chạy liên tiếp để thu được các chuyển động của nhân vật. Và trong game thì khái niệm Animation (hoạt ảnh) cũng hoàn toàn tương tự thôi :D

Mình sẽ trích ra tư wiki trên github, khái niệm Animation cực kỳ đơn giản và dễ hiều :D

An animation consists of multiple frames which are shown in a sequence at set intervals.
 Các bạn có thể vào xem trên wiki, bài viết khá cụ thể, có ví dụ đi kèm
https://github.com/libgdx/libgdx/wiki/2D-Animation

Bây giờ chúng ta sẽ cùng nhau đi sang phần tiếp theo.

II, Cách sử dụng Animation trong libGDX:


Để có thể tạo animation thì libGDX cung cấp cho chúng ta một lớp có tên là Animtion, các bạn có thể đọc document của nó tại tại đây, khá đầy đủ và chi tiết:
https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/Animation.html

class này bao gồm 3 contructors, nhưng chúng ta sẽ sử dụng contructor thứ 2 (cái có nhiều tham số nhất ấy)

Animation(float frameDuration, Array<? extends TextureRegion> keyFrames)

Như các bạn thấy nó có chứa 2 tham số, ý nghĩa của từng tham số như sau:

  • frameDuration: đây chính là khoảng thời gian nghỉ giữ mỗi khung hình và được tính bằng giây.
  • keyFrames: mảng lưu các khung hình cần hiển thị, ở đây được vào bộ nhớ bởi các đối tượng TextureRegion mà chúng ta đã học ở bài trước.
Như đã thấy ở contructor trên thì chúng ta cần có một mảng các frame cần hiển thị, chúng ta có thể lấy tường bức ảnh đại diện mỗi frame rồi load vào mảng các frames đó, nhưng cách này không tối ưu, và cũng chẳng ai xài cách này đâu.
Nhớ lại bài hôm trước, chúng ta đã học cách sử dugnj Texture Packer để đóng gói ảnh lai đúng không nào, thông thương người ta sẽ dùng Texture Packer để đóng gói các frame nhỏ lại thành một tấm lớn, hoặc có thể dùng trình chỉnh sửa ảnh như Photoshop để làm điều này.

Và cái "bức ảnh lớn" mà mình thu được sau khi đóng gói lại sẽ được gọi là Sptite Sheet nha các bạn.

Rồi, bây giờ để mà test thử thì chúng ta cần có một sprite sheet, các bạn lên google gõ "sprite sheet" ra một đống luôn. Nhưng mình chắc là các bạn sẽ lười nên mình đã chuẩn bị sẵn một tấm ở đây rồi :)).
Ảnh này mình lấy trên github luôn.

Các bạn thấy ở sprite sheet trên thì các frame được sắp xếp giống như các phần tử trên một ma trân vậy, và việc duyệt các frame này cũng y chang việc các bạn duyệt các phần tử của một ma trân thôi. các bạn xem hình sau để hiểu rõ hơn:

Các frame sẽ được duyệt theo dòng từ trái qua phải và trên xuống, nếu sử dụng chế độ lặp lại thì khi chạy đến frame cuối cùng nó sẽ quay về và tiếp tục hiển thị frame đầu.

Bây giờ chúng ta sẽ code để cho nó ra chuyển động nha, việc này khá thú vị :)).
Các bạn nhớ nắm rõ cách thức hoạt động của nó vì animation trong game sử dụng khá nhiều đấy.

Không lan man nữa :)), bây giờ các bạn mở file code ra, rồi code như sau:


package com.blogspot.gameiter;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;

public class libGDXAnimation extends ApplicationAdapter {
 private static final int FRAME_COLS = 6;
 private static final int FRAME_ROWS = 5;

 Animation walkAnimation;
 Texture walkSheet;
 TextureRegion[] walkFrames;
 SpriteBatch spriteBatch;
 TextureRegion currentFrame;

 float stateTime;

 @Override
 public void create() {
  walkSheet = new Texture(Gdx.files.internal("animation.png"));
  TextureRegion[][] tmp = TextureRegion.split(walkSheet,
    walkSheet.getWidth() / FRAME_COLS, walkSheet.getHeight()
      / FRAME_ROWS);
  walkFrames = new TextureRegion[FRAME_COLS * FRAME_ROWS];
  int index = 0;
  for (int i = 0; i < FRAME_ROWS; i++) {
   for (int j = 0; j < FRAME_COLS; j++) {
    walkFrames[index++] = tmp[i][j];
   }
  }
  walkAnimation = new Animation(0.025f, walkFrames);
  spriteBatch = new SpriteBatch(); 
  stateTime = 0f; 
 }

 @Override
 public void render() {
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  stateTime += Gdx.graphics.getDeltaTime();
  currentFrame = walkAnimation.getKeyFrame(stateTime, true);
  spriteBatch.begin();
  spriteBatch.draw(currentFrame, 50, 50);
  spriteBatch.end();
 }
}


  • 2 hằng số đầu tiên: FRAME_COLS và FRAME_ROWS chính là số hàng và số cột của sprite sheet, ơ đây bao gồm FRAME_COLS * FRAME_ROWS = 6 * 5 = 30 frame.
  • Tiếp theo là khai báo các đối tượng cần sử dụng, bao gồm: walkAnimation để tạo hoạt ảnh, walkSheet là Texture dùng để load tấm ảnh sprite sheet vào bộ nhớ.
  • walkFrames là mảng lưu các frame sau khi được cắt ra từ walkSheet.
  • currentFrame là frame hiện tại để hiển thị theo mỗi khoảng thời gian.
  • stateTime: tạm gọi là các mốc trạng thái, chúng ta sẽ cần dựa vào nó để lấy ra frame từ mảng frames của chúng ta.
Trong phương thức create() các bạn thấy có một biến tmp, biến này là một mảng 2 chiều có chức năng lưu các frame được cắt ra từ sprite sheet.
Tiếp đó, trong vòng lặp ta sẽ "đổ" lần lượt các frame đã cắt ra vào mảng một chiều walkFrames.
Cuối cùng chúng ta tạo mới đối tượng Animation với hai tham số như đã nói lúc đầu và gán stateTime lúc đầu bằng 0.

Trong phương thức render() thì chúng ta sẽ cộng vào stateTime một khoảng thời gian bằng thời gian nghỉ giữa các frame.
Sau đó gán currentFrame với frame tương ứng stateTime bằng phương thức getKeyFrame(float stateTime, boolean looping), tham số thứ 2 biểu diễn việc lặp lại frame hay không.
Cuối cùng ta vẽ currentFrame lên màn hình, do currentFrame liên tục thay đổi nên ta sẽ thấy được chuyển động của nhân vật.
Kêt quả chạy lên như sau:


Cũng không mấy khó khăn đúng không các bạn :D.
Chúc các bạn thành công, bài hôm nay mình xin dừng ở đây, hẹn gặp lại các bạn trong các bài sau.

Lập trình game với libGDX - Hướng dẫn sử dụng Texture Packer - TextureAtlas - AtlasRegion

Chào mừng các bạn đã quay trở lại blog :)
Bài hôm trước chúng ta đã tìm hiểu một số đối tượng về đồ họa trong libGDX gồm có: Sprite, Texture, TextureRegion, SpriteBatch.
Bài hôm nay chúng ta sẽ cùng nhau tìm hiểu về một công cụ khá hữu ích đó là Texture Packer. Bây giờ ta sẽ điểm qua một số ý chính mà hôm nay chúng ta sẽ học:


  • Texture Packer là gì? Cách sử dụng?
  • TextureAtlas là gì? Cách sử dụng?



I, Texture Packer là gì? Cách sử dụng?


1, Texture Packer là gì?

Texture Packer đơn giản là một công cụ có khả năng đóng gói các file hình ảnh đơn lẻ lại thành một file hình ảnh bao gồm các ảnh nhỏ đó. Điều đặc biệt là sau khi đóng gói các ảnh vào bức ảnh lớn thì Texture Packer sẽ ghi ra một file text chứ một số thông số quan trọng, vậy các thông số đó là gì? Một lát nữa qua phần hướng dẫn sử dụng mình sẽ nói rõ hơn. Các bạn cứ hình dung như trò chơi ghép tranh vậy đó: mỗi mảnh ghép là một bức ảnh nhỏ, bức ảnh lớn sẽ là bức tranh mà ta ghép được :D.

2, Cách sử dụng Texture Packer:

Đầu tiên để mà sử dụng thì ta cần download nó về đã, các bạn vào link sau để download về nha:
https://code.google.com/archive/p/libgdx-texturepacker-gui/downloads
Các bạn cứ quất bản cao nhất về mà xài, bản 3.2.0 đấy.

Rồi, sau khi down về các bạn giải nén nó ra, sẽ được kết quả như sau:

Một file jar để chạy chương trình và một thư mục demo.
Các bạn chạy file jar đó lên để chạy chương trình:
Giao diện chương trình:

Các bạn nhấn New pack để tao mới.
Nhập tên rồi bấm Ok.

Làm theo hướng dẫn là xong, nói chung giao diện rất dễ sủ dụng :)), mình không cần hướng dẫn nhiều.

Chúng ta sẽ sử dụng thư mục demo này cho ví dụ luôn nha.
Bây giờ các bạn mở thư mục đó lên xem nó có gì trong đó.

Trong này có 2 thư mục là input và output.
Trong thư mục input sẽ chứa các ảnh ảnh mà ta cần đóng gói:


Trong thư mục output sẽ chứa bức ảnh đóng gói các bức ảnh nhỏ, cùng với một file có đuôi .pack (thực ra đuôi này bạn đôi thành .txt hay .abc cũng được, không quan trọng tên đuôi).

Bây giờ ta sẽ cùng nhau xem trong file test-me!.pack kia nó có cái gì trong đó nha:



Các thuộc tính quan trọng mình đã chú thích trong ảnh. Vậy file .pack này có ý nghĩa gì?
Câu trả lời sẽ có khi bạn đọc phần tiếp theo :)).

II, TextureAtlas là gì? Cách sử dụng?

1, TextureAtlas là gì?

Ở phần trên chúng ta đã tìm hiểu Texture Packer, thì output của Texture Packer sẽ là một file ảnh và một file văn bản chứa các thuộc tính của các bức ảnh nhỏ.
Đối tượng Texture Atlas sẽ có chức năng đọc dữ liệu từ file văn bản (ở đây là file .pack đó) rồi load các bức ảnh nhỏ dựa vào dữ liệu đọc được từ tấm ảnh lớn.

2, Cách sử dụng đối tượng TextureAtlas:

Giờ ta tạo mới một project để test thử nha, hoặc nếu lười thì các bạn lấy lại project hôm trước làm luôn cũng được.
Đầu tiên thì các bạn cần copy 2 file output gồm test-me!.pack và test-me!.png vào thư mục assets trong project android đã, sau đó ta sẽ có code như sau:

package com.gameiter.hoclibgdx;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureRegion;

public class LearningliGDXb extends ApplicationAdapter {
 private TextureAtlas atlas;
 private SpriteBatch batch;
 private AtlasRegion region;
 @Override
 public void create() {
  batch = new SpriteBatch();
  atlas = new TextureAtlas("test-me!.pack");
  region = atlas.findRegion("test01");
 }
 @Override
 public void render() {
  Gdx.gl.glClearColor(0, 0, 0, 1);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  batch.begin();
  batch.draw(region, 100, 100);
  batch.end();
 }
}

Các bạn có thấy "quen" không? :))
Đối tượng TextureAtlas khá giống đối tượng Texture và đối tượng AtlasRegion lại giống đối tượng TextureRegion đúng không :)), do đó code trên khá dễ hiểu.
Đầu tiều đối tượng TextureAtlas sẽ load dữ liệu từ file test-me!.pack, trong file này có chứa tên của bức ảnh lớn. Sau đó đối tượng AtlasRegion sẽ load ảnh từ đối tượng TextureAtlas dựa theo tên file (Các tên file cũng được ghi trong file .pack - cái này hồi nãy chúng ta đã tìm hiểu rồi).
Phương thức render() đơn giản là vẽ lên màn hình đối tượng AtlasRegion :)
Kế quả thu được:

Các bạn kiểm tra lại file ảnh test01.png trong thư mục input hồi nãy có đúng ảnh này không nha, tương tự để load các tấm ảnh kia thì ta sẽ thay tên file tương ứng là xong :).

Bài hôm nay đến đây thôi, bài hôm sau ta sẽ tìm hiểu về Animation, cám ơn các bạn đã quan tâm theo dõi, hẹn gặp lại trong các bài viết sau :D

Lập trình game với libGDX - Làm quen với Texture, TextureRegion, Sprite, SpriteBatch - Vẽ hình

Xin chào các bạn, chào mừng các bạn đã quay trở lại với blog trong series về lập trình game với libGDX :D.
Các bài trước chúng ta đã biết được khái niệm Game engine, cách tạo project libGDX, cấu trúc project và life cycle của nó.

Bài học nay chúng ta học gì?

  • Tìm hiểu một số đối tượng quan trọng không thể thiếu trong lập trình game với libGDX
  • Cách vẽ hình trong game.
Ok, bây giờ chúng ta sẽ bắt đầu nha.



I, Tìm hiểu một số khái niệm quan trọng:

1, Sprite là gì?

Chắc hẳn ai trong chúng ta cũng đã từng chơi qua một game, dù là khủng hay củ chuối đi nữa thì đều đã thấy được sprite.
Ồ, vậy sprite là gì?
Sprite đơn giản là một đối tượng hình vẽ được vẽ lên màn hình.

Hình trên là game Flappy Bird một thời làm mưa làm gió trên thế giới game moblie :D, thì trong hình bạn thấy đó, hình ảnh con chim là một sprite, cái ống nước cũng vậy.
Do đó, sprite là thành phần không thể thiếu trong game (Các bạn có thấy game nào không có hình ảnh chưa? :v).

Bất kể bạn lập trình game với cái gì đi nữa thì khái niệm sprite vẫn như vậy :D.

2, Một số khái niệm quan trọng:

Để quan lý sprite trong game thì libGDX sử dụng một số lớp quan trọng sau đây:

  • SpriteBatch
  • Texture
  • TextureRegion
  • Sprite
Giờ ta sẽ đi tìm hiểu chi tiết về các lớp này (Các trích dẫn lấy từ wiki trên github):

a, SpriteBatch:

SpriteBatch is given a texture and coordinates for each rectangle to be drawn. It collects the geometry without submitting it to the GPU. If it is given a texture different than the last texture, then it binds the last texture, submits the collected geometry to be drawn, and begins collecting geometry for the new texture.
Đây là lớp quan trọng có chức năng quản lý việc vẽ hình trong libGDX.
Nó bao gồm 2 phương thức quan trọng là begin() và end(). Bạn tưởng tượng begin() và end() giống như cặp đóng mở ngoặc nhọn {...} khi mình lập trình vậy, tất cả mọi họa động vẽ cần được đặt trong cặp begin() - end(), nếu không sẽ báo lỗi. Lát nữa chúng ta sẽ demo sau.

b, Texture:


The Texture class decodes an image file and loads it into GPU memory. The image file should be placed in the "assets" folder. The image's dimensions should be powers of two (16x16, 64x256, etc) for compatibility and performance reasons.
Texture thì có chức năng load hình ảnh vào bộ nhớ GPU, và các hình ảnh này sẽ được đặt trong folder "assets".
Lưu ý: các phiên bản cũ của libGDX yêu cầu Texture phải có kích thước là lũy thừa của 2, nhưng phiên bản hiện tại thì không bị chi phối bởi điều này, tuy nhiên mình vẫn khuyên các bạn nên sử dụng các ảnh có kích thước là lũy thừa của 2 để tăng hiệu năng của game lên.

c, TextureRegion:


Hiểu đơn giản TextureRegion là một bức ảnh nhỏ bạn cắt ra từ một bức ảnh lớn (chính là Texture).

d, Sprite:


Ảnh từ Texture sau khi được load lên ta có thể đưa vào Sprite để sử dụng một số thuộc tính đặc trưng của nó như: position (tọa độ), rotate (xoay), vv,...

Bây giờ, để hiểu rõ hơn chúng ta sẽ đi sang phần demo vẽ hình sử dụng các đối tượng vừa nên trên.

II, Cách vẽ hình trong game:


Đầu tiên đễ vẽ hình chúng ta cần có hình đã đúng không :)).
Mình kết chú chó nâu nên sẽ lấy hình nó vẽ, các bạn có thể lấy hình bất kỳ.



Lấy luôn project hôm trước các bạn tạo ra làm luôn nhé, đầu tiên thì các bạn đưa file hình ảnh vào đúng thư mục "assets" đã nhé.


Các bạn chỉ cần để vào thư mục assets của project android thì tự động bên project desktop nó cũng sẽ được load vào.

Bây giờ các bạn mở project core ra sau đó mở file có trong project ra:

Các bạn xem qua code trong đó:


package com.gameiter.hoclibgdx;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class LearningliGDXb extends ApplicationAdapter {
 SpriteBatch batch;
 Texture img;
 
 @Override
 public void create () {
  batch = new SpriteBatch();
  img = new Texture("badlogic.jpg");
 }

 @Override
 public void render () {
  Gdx.gl.glClearColor(1, 0, 0, 1);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  batch.begin();
  batch.draw(img, 0, 0);
  batch.end();
 }
}

Bây giờ để vẽ hình chú chó nâu thì bạn chỉ cần đổi tên file được truyền vào hàm khởi tạo Texture là được.
Giải thích qua một chú:
Gdx.gl.glClearColor(1, 0, 0, 1) - dòng code này sẽ set màu mà nền background khi ta chạy game, với ba tham số đầu tương ứng trong gam màu RGB, ở đây (1, 0, 0) có nghĩa là nền màu đỏ, còn ví dụ như (0, 1, 0) thì nên sẽ là màu lục, các bạn có thể tùy chỉnh tham số trong khoảng từ 0 - 1. Còn cái tham số cuối mình cũng chưa tìm hiểu :)) các bạn biết thì comment lại nhé ;), nhưng chủ yếu ta chỉ cần xài 3 tham số đầu, tham số cuối cứ để là 1.

Lưu ý: khác với hệ tọa độ ta học trong series lập trình game java cơ bản, hệ tọa độ trong libGDX giống hệt hệ tọa độ ta học hồi phổ thông, có nghĩa là trục tung hướng lên trên. các bạn xem kết quả sẽ rõ hơn.

Như ở trên chúng ta đã thực hiện việc vẽ hình thông qua đối tượng Texture luôn.
Bây giờ chúng ta sẽ thêm một đối tượng TextureRegion vào để hiểu hơn về nó.

package com.gameiter.hoclibgdx;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;

public class LearningliGDXb extends ApplicationAdapter {
 SpriteBatch batch;
 Texture img;
 TextureRegion region;
 
 @Override
 public void create () {
  batch = new SpriteBatch();
  img = new Texture("chonau.jpg");
  region = new TextureRegion(img, 150, 150);
 }

 @Override
 public void render () {
  Gdx.gl.glClearColor(1, 0, 0, 1);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  batch.begin();
  batch.draw(img, 0, 0);
  batch.draw(region, 300, 300);
  batch.end();
 }
}

Như các bạn thấy, mình đã thêm vào một đối tượng TextureRegion.
Trong phương thức create() mình khởi tạo đối tượng TextureRegion với ba tham số:

  • Tham số thứ nhất: đối tượng Texture để load ảnh vào GPU.
  • Tham số thứ hai và ba: là kích thức mình muốn cắt ra từ tâm ảnh nhỏ, tọa độ gốc (0, 0) nằm ở góc phía trên bên trái của tấm ảnh góc.
Bây giờ chạy lên chúng ta sẽ có như sau:
Bức ảnh nhỏ sẽ có kích thức 150x150 px như khi mình code.
TextureRegion sẽ được sử dụng nhiều trong việc tạo hoạt ảnh (animation), các bài sau mình sẽ hướng dẫn cụ thể về vấn đề này.

Giờ ta sẽ thử vẽ hình bằng đối tượng Sprite.

Các bạn xem đoạn code dưới đây:

package com.gameiter.hoclibgdx;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;

public class LearningliGDXb extends ApplicationAdapter {
 SpriteBatch batch;
 Texture img;
 TextureRegion region;
 Sprite sprite;
 
 @Override
 public void create () {
  batch = new SpriteBatch();
  img = new Texture("chonau.jpg");
  sprite = new Sprite(img);
 }

 @Override
 public void render () {
  Gdx.gl.glClearColor(1, 0, 0, 1);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  batch.begin();
  sprite.draw(batch);
  batch.end();
 }
}

Ở trên mình khởi tạo đối tượng Sprite bằng cách truyền vào một Texture.
Trong phương thức render() ta đơn giản là vẽ đối tượng Sprite ra màn hình.

Bây giờ chúng ta sẽ tìm hiểu qua một số phương thức của đối tượng này:

+ Phương thức flip(boolean x, boolean y): phương thức này có chức năng "lật" tấm hình của bạn, tham số thứ nhất đại diện cho chiều ngang (horizontal), tham số thứ hai đại diện cho chiều dọc (vertical). Bạn xem ví dụ sau và thử thay lần lượt các tham số vào sẽ hiểu ngay thôi. Giả sử mình muốn lật ngược tấm hình lại theo chiều dọc mình sẽ thêm vào cuối phương thức create() như sau: sprite.flip(false, true), kết quả như sau:

Nếu bạn muốn lật tấm ảnh theo chiều ngang thì tương tư: sprite.flip(true, fasle), kết qủa:

Đấy, khá thú vị đúng không :))

+ Phương thức rotate(float degrees): phương thức này sẽ set góc quay cho sprite của chúng ta, góc xoay sẽ chạy ngược chiều kim đồng hồ.
Để test, các bạn thêm dòng này vào cuối phương thức create():
sprite.rotate(45); // set góc quay ảnh 45 độ
Kết quả thu được như sau:


+ Phương thức setPosition(float x, float y): sẽ giúp chúng ta set tọa độ cho sprite.
Các bạn có thể thêm dòng này vào cuối phương thức create() để test thử:
sprite.setPosition(200, 150); // đặt tọa độ vẽ sprite (200, 150)
Kết quả như sau:


+ Các bạn cũng có thể tùy chỉnh lại màu của sprite bằng phương thức setColor(Color c).
sprite.setColor(Color.GREEN);
Kết quả:

Và còn một số phương thức khác nữa các bạn có thể tự vọc vạch tìm hiểu thêm nha :D.

Bài hôm nay mình xin kết thúc tại đây, cám ơn các bạn đã ghé thăm blog. Hẹn gặp lại các bạn trong các bài hướng dẫn sau :).


Lập trình game với libGDX - Chạy thử và tìm hiểu cấu trúc Project

Chào mừng các bạn đã quay trở lại với blog :).
Tiếp tục bài hôm trước bây giờ chúng ta sẽ cùng nhau đi tìm hiểu project mà hôm qua chúng ta đã tạo nha.
Và trước khi cùng nhau tìm hiểu thì ta sẽ chạy thử các project xem nó ra ngô ra khoai gì đã ha :D.

Bài hôm nay sẽ gồm một số ý:
  • Imrport project vào eclipse và chạy thử.
  • Tìm hiểu cấu trúc của các project
Ok, chúng ta bắt đầu nhé.



I, Import project vào eclipse và chạy thử:
1, Import project vào eclipse:

Để import các project này vào eclipse các bạn làm như sau:

+ Đầu tiê, bạn vào File/Import, hoặc tại cửa số Package Explorer  bấm chuột phải chọ Import, sau khi chọn thì cửa sổ Import hiện lên như sau:

 Các bạn chọn Existing Project into Workspace, cửa số tiếp theo hiện lên như sau:


Các bạn tiếp tục chọn Browse..., sau khi chọn thì bạn tìm đến thư mục mà các bạn đã tạo các Project thì sẽ được kết quả như sau:

Ví dụ của mình lưu ở đường dẫn: /mnt/Learn/libgdx-1.6.1/Project/LearnlibGDX.
Các bạn thấy sẽ có tất cả 3 project bao gồm:

  • Project android
  • Project core
  • Project desktop
Lưu ý: nếu lúc lựa chọn khi tạo Project bạn chọn iOS hay html thì nó sẽ có thêm các project này vào cho các bạn, bạn nào lỡ tạo mà không muốn import vào thì bỏ tick nha và lưu ý quan trọng là bạn phải import project core vào, vì mình sẽ viết code ở project này.
Bây giờ chúng ta chưa vôi quan tâm đến nó, chạy thử xem như nào đã ha :))

2, Chạy thử các project:

Để chạy thử thì ta sẽ Bấm chuột phải vào project cần chạy (ở đây có 2 project để chạy là android và desktop)

+ Để chạy project dành cho desktop bạn bấm chuột phải vào project desktop -> Run As -> Java Application, sẽ xuất hiện của sổ hỏi bạn chọn file main để thực thi:

Các bạn chọn DesktopLauncher rồi bấm ok. Kết quả thu được như sau:

Rất đơn giản đúng không các bạn :D.

Vậy bây giờ làm sao để chạy project android? Để chạy được project android bạn cần cài đặt một máy ảo (Bạn có thể dùng AVD hoặc Genymotion cũng được), hoặc nếu bạn có máy thật cắm vào chạy thì càng tốt.
À mà mình chắc là giờ project android cảu bạn đang báo lỗi đúng không?
Lỗi này là do ta chưa chọn phiên bản android để build. Để khắc phục các bạn làm như sau:
+ Chuột phải vào project android -> Properties, sẽ xuất hiện của sổ như sau:

+ Các bạn chọn mục Android và chọn một trong các phiên bản ở Project Build Target và bấm OK để hoàn tất.

Xong, giờ muốn chạy thì bạn làm tương tự như chạy project desktop:
+ Bấm chuột phải vào project android -> Run As -> Android Application, Android Emulator mặc định sẽ được bật lên.
+ Nếu báo lỗi thì bạn có thể Run Configurations -> Target, sau đó chọn như hình
Bây giờ bạn có thể chạy bằng máy ảo Genymotion hoặc chọn thiết bị thật đã được kết nối qua USB.

Hiện tại chúng ta chỉ cần test với project destop đã, cái android sau này làm game hoàn chỉnh ta sẽ tính sau ha.

II, Tìm hiểu cấu trúc project:

Thì như các bạn đã thấy, chúng ta có 3 project:
Bây chúng chúng ta sẽ tìm hiểu nha:
* Project core: đây là project chính, game của chúng ta hầu hết sẽ được code ở project này bằng java, các project còn lại khi chạy sẽ lấy code ở core, và đây là cái hay của libGDX: các thư viện trong các project android. desktop sẽ "dịch" code từ core ra để chạy. Bạn xem thử code file chứa trong project desktop xem như nào nhé:

package com.gameiter.hoclibgdx;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class LearningliGDX extends ApplicationAdapter {
 SpriteBatch batch;
 Texture img;
 
 @Override
 public void create () {
  batch = new SpriteBatch();
  img = new Texture("badlogic.jpg");
 }

 @Override
 public void render () {
  Gdx.gl.glClearColor(1, 0, 0, 1);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  batch.begin();
  batch.draw(img, 0, 0);
  batch.end();
 }
}

Ở các phiên bản libGDX cũ thì nó class LearninglibGDX sẽ được implements ApplicationListener, đây là một interface quan trọng, nó được coi là gốc của tất cả các game mà ta viết, nó thể Life cycle của chúng ta.
Thực ra class ApplicationAdapter nó cũng implements thằng ApplicationListener nên bạn viết kiểu nào cũng được.

Chúng ta cùng xem qua interface ApplicationListener xem nó có những gì nhé:


public abstract class ApplicationAdapter implements ApplicationListener {
 @Override
 public void create () {
 }

 @Override
 public void resize (int width, int height) {
 }

 @Override
 public void render () {
 }

 @Override
 public void pause () {
 }

 @Override
 public void resume () {
 }

 @Override
 public void dispose () {
 }
}

Nói sơ qua về các phương thức:
+ create(): sẽ được gọi khi game của chúng ta bắt đầu.
+ resize(int width, int height): phương thức được gọi khi chúng ta thay đổi kích thức màn hình (phóng to, thu nhỏ, vv).
+ render(): đơn giản là vẽ.
+ pasuse(): gọi khi game tạm dừng.
+ resume(): gọi khi game tiếp tục (Sau khi pause() được gọi).
+ dispose(): giúp giải phóng tài nguyên, gọi khi game kết thúc,

Dưới đây là Life cycle (hiểu đơn giản là vòng lặp như mình đã nói ở series lập trình game java cơ bản trước), ảnh mình lấy từ wiki trên github nhé:


Nhìn qua chúng ta cũng phần nào hiểu đước cách thức game vận hành ha :D.

* Còn hai cái project android với desktop kia, có gì trong đó?
Chúng ta cùng xem project desktop nhé, trong này sẽ có file DesktopLauncher, file này đơn giản chỉ là cấu hình để chạy trên desktop các bạn ạ.

import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.gameiter.hoclibgdx.LearningliGDXb;

public class DesktopLauncher {
 public static void main (String[] arg) {
  LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
  new LwjglApplication(new LearningliGDXb(), config);
 }
}

Nó chỉ cần vậy là chạy được rồi, vì mọi xử lý đã nằm ở lớp core, việc còn lại của các project kia đơn giản chỉ là cấu hình để chạy.

Mẹo: vì libGDX là open source nên các bạn thoải mái xem code các class nếu tò mò xem nó có gì, để làm điều này trên eclipse nhanh chóng, các bạn giữ Ctrl + chuột trái vào class cần xem nhé.

Còn đây là file AndroidLauncher trong project android, nó cũng chỉ là vài dòng code cấu hình thôi:

import android.os.Bundle;

import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import com.gameiter.hoclibgdx.LearningliGDXb;

public class AndroidLauncher extends AndroidApplication {
 @Override
 protected void onCreate (Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
  initialize(new LearningliGDXb(), config);
 }
}

À ngoài ra có một điều khá hay trong libGDX, đó là tất cả tài nguồn có trong game (ví dụ là âm thanh, hình ảnh, vv, ...) ta sẽ lưu vào thư mục assets, và thú vị ở chỗ là ta chỉ cần lưu vào thư mục project assets của android thì lập tức ở thư mục assets của desktop cũng sẽ tự động được cập nhật theo.

Các bạn xem hình sau sẽ rõ hơn (hình mình lấy trong Cuốn Learning LibGDX Game Development, 2nd Edition, các bạn có thể vào mục Sách hay của blog để down bản pdf nhé).


Bài hôm nay vậy mình thấy viết khá nhiều rồi, chúng cũng đã hiểu được phần nào cấu trúc project và life cycle rồi.

Bài hôm sau mình sẽ hướng dẫn các bạn vẽ hình nha. Hẹn gặp lại, các ơm các bạn đã quan tâm đọc bài :D.

Lập trình game java cơ bản - Hoàn thành game Lucky Box

Lập trình game java cơ bản - Hoàn thành game Lucky Box
Xin chào các bạn, chào mừng các bạn quay trở lại với blog.
Tiếp tục bài hôm trước thì hôm nay mình sẽ cùng các bạn làm "hoàn chỉnh" game hôm trước.
Bài hôm trước chúng ta đã cùng nhau setup các lớp cơ bản trong game, bài hôm nay chúng ta sẽ cài đặt lớp MainBoard - lớp này có chức năng xử lý và render hình ảnh.

Bây giờ chúng ta sẽ bắt tay vào làm nhé.
Giống những bài hôm trước, game của chúng ta sẽ sử dụng Timer, do đó chúng ta cần implements lại ActionListener, ngoài ra chúng ta cần sử dụng các thuộc tính chung nằm trong interface Common, nên ta sẽ implements nó luôn.

Trong game của ta sẽ có nhân vật chính là đối tượng Box, một danh sách các Enemy và danh sách các Item nên chúng ta sẽ có các thuộc tính cơ bản sau:


public class MainBoard extends JPanel implements ActionListener, Common {
 private ArrayList < item > items;
 private ArrayList < enemy > enemies;
 private Box box;
 private Timer timer;
 private boolean inGame;
 private int point;

 public MainBoard() {
  initBoard();
 }
}

Chúng ta thêm thuộc tính inGame để kiểm tra xem game đã kết thúc hay chưa và thêm thuộc tính point để tính điểm cho người chơi.

Phương thức khởi tạo ta sẽ tạo ra một phương thức initBoard() để cho code dễ nhìn hơn (về cơ bản thì nếu một công việc bạn chia ra cho nhiều người làm thì nó sẽ phần nào nhanh hơn đúng không) và sau này sửa code sẽ dễ dàng hơn :).

Ok, bây giờ chúng ta sẽ tạo ra phương thức initBoard() - phương thức này sẽ khởi tạo các đối tượng mà game của chúng ta sử dụng.

private void initBoard() {
  inGame = true;
  point = 0;
  addMouseMotionListener(new TAdapter());
  setFocusable(true);
  setBackground(Color.lightGray);
  setDoubleBuffered(true);
  box = new Box(INIT_BOX_X, INIT_BOX_Y);
  enemies = new ArrayList < enemy >();
  items = new ArrayList <item >();
  initEnemy();
  initItem();
  inGame = true;
  timer = new Timer(DELAY, this);
  timer.start();
 }
Mọi thứ đã quá quen thuộc rồi đúng không nào (Bạn nào chưa xem các bài trước thì có thể xem lại nhé) :D, ở đây tạm thời sẽ báo lỗi do bạn chưa tạo ra phương thức initEnemy(), initItem() và lớp TAdapter, chúng ta sẽ tạo lớp này nằm trong lớp MainBoard luôn ngay bây giờ.

Đầu tiền là phương thức initEnemy()

private void initEnemy() {
  int initX = (int) (Math.random() * (Common.WIDTH - 50));
  int initY = (int) (Math.random() * (Common.HEIGHT - 50));
  Enemy enemy = new Enemy(initX, initY);
  enemies.add(enemy);
 }
Đầu tiên là random tọa độ của enemy sau đó tạo mới và add nó vào danh sách các enemies.

private void initItem() {
  int initX = (int) (Math.random() * (Common.WIDTH - 50));
  int initY = (int) (Math.random() * (Common.HEIGHT - 50));
  Item item = new Item(initX, initY);
  items.add(item);
 }
Tương tự cho phương thức initEnemy().

Bây giờ là phương thức paintComponent(...) có chức năng render hình ảnh. phương thức này ta cần cài đặt như sau:

@Override
 protected void paintComponent(Graphics g) {
  super.paintComponent(g);
  if (inGame) {
   drawObject(g);
  } else {
   drawGameOver(g);
  }
 }
Hiểu đơn giản là: nếu game đang còn được chơi (Nhân vật chính chưa chết) thì ta sẽ vẽ lại các nhân vật trong trò chơi, ngược lại chúng ta sẽ vẽ ra màn hình Game Over :).
Bây giờ ta tiếp tục tạo 2 phương thức drawObject(Graphics g) và drawGameOver(Graphics g).

private void drawObject(Graphics g) {
  // Draw Box
  g.drawImage(box.getImage(), box.getX(), box.getY(), this);
  // Draw Enemies
  for (Enemy enemy : enemies) {
   g.drawImage(enemy.getImage(), enemy.getX(), enemy.getY(), this);
  }
  // Draw Item
  for (Item item : items) {
   g.drawImage(item.getImage(), item.getX(), item.getY(), this);
  }

  // Draw Point
  Font font = new Font("Arial", Font.BOLD, 12);
  g.setColor(Color.BLACK);
  g.setFont(font);
  g.drawString("Your point: " + point, 5, 15);
 }
Mình đã comment ở code rồi nên không cần giải thích nữa :))
Tương tự phương thức drawGameOver(Graphics g):

 
 private void drawGameOver(Graphics g) {
  Font font = new Font("Helvetica", Font.BOLD, 20);
  g.setColor(Color.BLACK);
  g.setFont(font);
  g.drawString("Game over!", 350, Common.HEIGHT / 2);
  g.drawString("Score: " + point, 370, Common.HEIGHT / 2 + 30);
 }
Lúc game over ta sẽ vẽ ra màn hình chữ "Game over!" cùng với điểm mà người chơi đạt được.

Do chúng ta implements ActionListener nên bây giờ chúng ta cần Override lại phương thức actionPerformed(...)

 
@Override
 public void actionPerformed(ActionEvent e) {
  inGame();
  // Update Object
  updateBox();
  updateEnemy();
  updateItem();
  // Check collision
  checkCollision();
  // Repaint
  repaint();
 }
Các phương thức ở trong chúng ta sẽ tiến hành cài đặt ngay bên dưới:

 
private void inGame() {
  if (!inGame) {
   timer.stop();
  }
 }

Nếu game over rồi thì ta sẽ dừng timer lại.
Tiếp đến là phương thức updateBox()
 
 private void updateBox() {
  box.move();
 }

Việc update đơn giản chỉ là di chuyển nhân vật chính của chúng ta.
 
private void updateEnemy() {
  for (int i = 0; i < enemies.size(); i++) {
   Enemy enemy = enemies.get(i);
   if (enemy.isVisible()) {
    enemy.move();
   } else {
    enemies.remove(i);
   }
  }
 }

Ở đây cái isVisible cũng không cần thiết lắm, vì nếu enemy chạm nhân vật chính thì game over rồi nên cũng không cần xóa enemy khỏi List nữa. Các bạn có thể nâng cấp game chẳng hạn như: ăn item nào đấy thì sẽ cho nổ vài enemy chẳng hạn :)). ở đây game sẽ làm cơ bản nên không quan tâm nha :).
 
private void updateItem() {
  for (int i = 0; i < items.size(); i++) {
   Item item = items.get(i);
   if (!item.isVisible()) {
    items.remove(i);
   }
  }
 }

Ở đây mỗi lần chỉ tạo 1 item, còn nếu bạn muốn nâng cấp có thể cho thêm nhiều item vào nữa :).
Và cuối cùng là phương lớp TAdapter như đã nói ban đầu:
private class TAdapter extends MouseMotionAdapter {
  @Override
  public void mouseMoved(MouseEvent e) {
   box.mouseMoved(e);
  }
 }
Và để test thử game thì chúng ta cần tạo thêm một lớp LuckBox nữa nha:

 
public class LuckyBox extends JFrame implements Common{

 public LuckyBox() {
  setTitle("My first game");
  add(new MainBoard());
  setSize(Common.WIDTH, Common.HEIGHT);
  setResizable(false);
  setLocationRelativeTo(null);
  setDefaultCloseOperation(EXIT_ON_CLOSE);
 }
 
 public static void main(String[] args) {
  EventQueue.invokeLater(new Runnable() {
   
   @Override
   public void run() {
    new LuckyBox().setVisible(true);
   }
  });
 }

}
Vậy là hoàn thành xong cái game cơ bản :)), chúc các bạn vui vẻ, những bài tới mình sẽ giới thiệu qua một số engine/framework làm game và nếu có khả năng thì mình sẽ làm một series về lập trinhf game sử dụng libGDX framework (bạn có thể tìm hiểu trước cái này)
Chào và hẹn gặp lại các bạn :).

Lập trình game java cơ bản - Game LuckyBox (setup cơ bản)

Chào mừng các bạn đã quay trở lại với blog, mấy bài hôm trước mình đã giới thiệu qua một số kiến thức căn bản để áp dụng cho việc tạo ra một game cơ bản, bài hôm nay chúng ta sẽ cùng nhau tạo ra một trò chơi của riêng bản thân mình :D.
Trò chơi này sẽ áp dụng tất cả những giờ chúng ta đã học được trong những bài trước, và nó cũng là một trờ chơi rất đơn giản, haha =)).

Đầu tiên mình sẽ giới thiệu qua trò chơi của chúng ta, nó sẽ kiểu như này:


Ý tưởng và cách chơi rất đơn giản: nhiệm vụ của bạn là ăn cho thật nhiều hình trái tim đó, mỗi lần ăn trái tim sẽ xuất hiện ra một vật cản, chạm vào nó thì "tèo" :v. Chỉ vậy thôi.


Rồi, bây giờ làm sao thực hiện đây ta?

Nhìn vào game ta sẽ thấy sơ sơ có 3 đối tượng chính, đó: nhân vật chơi của chúng ta - chính là cái hộp - box(xin lỗi mình ko có hoa tay nên chỉ vẽ đồ họa 8-bit kiểu như trong video mà thôi :v). một đối tượng vật phẩm - item, một đối tượng quân địch - enemy.

Chúng ta nhận thấy rằng là: 3 đối tượng này sẽ có một số thuộc tính cơ bản giống nhau bao gồm:

  • Hình ảnh của đối tượng - sử dụng lớp Image trong java.
  • Tọa độ của đối tượng gồm hoành độ x và tung độ y.
  • Kích thước đối tượng gồm chiều dài (width) và chiều cao (height), thực ra lúc vẽ hình thì mình ko cần 2 thuộc tính này, nhưng chúng ta sẽ sử dụng nó để xét va chạm.
Nói về việc xét va chạm thì ta sẽ sử dụng cách dễ nhất đó là sử dụng vùng bao hình chữ nhật luôn cho tiện :D.

Ok, Vậy chúng ta sẽ đi xây dụng một lớp chung để cho các đối tượng trong game kế thừa lại nhằm tối ưu code của ta.
Ta sẽ đặt tên lớp này là Sprite, lớp Sprite sẽ được code như sau:


import java.awt.Image;
import java.awt.Rectangle;

import javax.swing.ImageIcon;

public class Sprite {
 protected int x;
 protected int y;
 protected int width;
 protected int height;
 protected boolean visible;
 protected Image image;
 
 public Sprite(int x, int y) {
  this.x = x;
  this.y = y;
  visible = true;
 }
 
 public void loadImage(String fileName) {
  ImageIcon ii = new ImageIcon(fileName);
  image = ii.getImage();
 }
 public void getImageDimention() {
  width = image.getWidth(null);
  height = image.getHeight(null);
 }

 public boolean isVisible() {
  return visible;
 }

 public void setVisible(boolean visible) {
  this.visible = visible;
 }

 public int getX() {
  return x;
 }

 public int getY() {
  return y;
 }

 public Image getImage() {
  return image;
 }
 
 public Rectangle getBound() {
  return new Rectangle(x, y, width, height);
 }
}

Phương thức getImageDimention() chúng ta sử dụng để lấy kích thước (width và height) của đối tượng và gán vào thuộc tính width, height luôn.
Phương thức getBound() sẽ dùng để trả về hình chữ nhật bao quan nhân vật chúng ta, ta sẽ dùng nó để xét va chạm.

Bây giờ ta sẽ đi xây dựng lớp Box - đây sẽ nhân vật chúng ta điều khiển :)


import java.awt.event.MouseEvent;

public class Box extends Sprite {
 private int dx;
 private int dy;

 public Box(int x, int y) {
  super(x, y);
  initBox();
 }

 private void initBox() {
  loadImage("images/box.png");
  getImageDimention();
 }
 
 public void move() {
  x = dx;
  y = dy;
 }
 
 public void mouseMoved(MouseEvent e) {
  dx = e.getX();
  dy = e.getY();
 }
}

Chúng ta kế thừa lại lớp Sprite để thừa hưởng các thuộc tính chung. Chúng ta sẽ điều khiển nhân vật bằng chuột nên sẽ set giá trị tọa độ của nhân vật bằng tọa độ chuột (dx, dy là tọa độ chuột).

Tiếp đến là lớp Item để biểu diễn vật phẩm mà nhân vật của ta cần thư thập, lớp này thì không có gì khó khăn cả:


public class Item extends Sprite {

 public Item(int x, int y) {
  super(x, y);
  initItem();
 }

 private void initItem() {
  loadImage("images/item.png");
  getImageDimention();
 }
 
}

Bây giờ là lớp Enemy để biểu diễn quân địch, ở video demo ở trên các bạn thấy quân địch của chúng ta tự động di chuyển theo phương ngang hoặc dọc, cái này mấy bài trước mình quên nói, bây giờ mình sẽ giới thiệu luôn.
Để quân địch tự di chuyển, thì trong mỗi vòng lặp ta sẽ tăng hoặc giảm tọa độ x hoặc y (tùy thuộc phương di chuyển, tăng giảm x nếu theo phương ngang và y nếu theo phương dọc).
Tiếp theo là phương di chuyển của nhân vật, thuật toán rất đơn giản:
- Lúc bạn khởi tạo nhân vật ra thì bạn sẽ gán cho nó một hướng di chuyển ban đầu, nếu phương ngang thì nhân vật sẽ có hai hướng di chuyển là LEFT hoặc RIGHT, nếu dọc thì sẽ có hai hướng di chuyển là UP hoặc DOWN.

Đễ dễ dàng quản lý thì ta sẽ tạo một lớp hoặc giao diện chứa các dữ liệu dùng trong game, ở đây mình sẽ tạo một giao diện Common để chứa nó:


public interface Common {
 public enum Status {UP, DOWN, LEFT, RIGHT}; // di chuyển ngang hoặc dọc
 public final int WIDTH = 800;
 public final int HEIGHT = 600;
 public final int ENEMY_SPEED = 2;
 public final int BOX_SPEED = 2;
 public final int DELAY = 10;
 public final int INIT_BOX_X = 50;
 public final int INIT_BOX_Y = 50;
}

Các bạn lưu ý cái enum Status, enum này sẽ là kiểu lưu các trạng thái của quân địch.

  • UP: di chuyển lên trên (giảm giá trị tung độ y)
  • DOWN: di chuyển xuống (tăng giá trị y)
  • LEFT: di chuyển qua trái (giảm hoành độ x)
  • RIGHT: di chuyển qua phải (tăng giá trị x)
Ngoài ra để quân địch không "chạy" luôn ra khỏi màn hình thì ta phải xét điều kiện chạm biên cho nó:

Các bạn xem hình sau sẽ rõ:
Tọa độ của nhân vật như ở mũi tên mình đã vẽ, muốn xem nó có chạm biên bên phải không thì ta phải xem hoành độ x của nó có lớn hơn hoặc bằng chiều rộng của màn hình trừ đi chiều rộng của nó không, tương tự với các biến khác.

Vậy ta sẽ có code lớp Enemy như sau:

public class Enemy extends Sprite implements Common {
 private Status status;

 public Enemy(int x, int y) {
  super(x, y);
  initEnemy();
 }

 private void initEnemy() {
  loadImage("images/enemy.png");
  getImageDimention();
  int rand = (int) (Math.random() * 4 + 1);
  switch (rand) {
  case 1:
   status = Status.UP;
   break;
  case 2:
   status = Status.DOWN;
   break;
  case 3:
   status = Status.LEFT;
   break;
  case 4:
   status = Status.RIGHT;
   break;
  }
 }

 public void move() {
  if (x <= 0)
   status = Status.RIGHT;
  if (x >= WIDTH - width)
   status = Status.LEFT;
  if (y <= 0)
   status = Status.DOWN;
  if (y >= HEIGHT - height)
   status = Status.UP;

  switch (status) {
  case UP:
   y -= ENEMY_SPEED;
   break;
  case DOWN:
   y += ENEMY_SPEED;
   break;
  case LEFT:
   x -= ENEMY_SPEED;
   break;
  case RIGHT:
   x += ENEMY_SPEED;
   break;
  }
 }
}
Để sử dụng kiểu Status ta phải implements Common.
Ok, vậy la ta đã setup xong cơ bản các đối tượng trong game rồi, bài hôm nay mình xin dưng ở đây, bài hôm sau ta sẽ hoàn thiện game này.
Cám ơn các bạn đã theo dõi. :)

Lập trình game java cơ bản - phát hiện va chạm (P2) - hình tròn và hình chữ nhật

Xin chào các bạn đã quay trở lại với blog.
Mấy hôm nay lười quá không viết bài mới :))

Hôm nay chúng ta tiếp tục tìm hiểu về việc xử lý va chạm trong game, bài hôm trước chúng ta đã thực hiện việc xử lý va chạm giữa hai hình chữ nhật với nhau mà không tốn tí công sức nào :v (vì nó có sẵn chỉ lấy ra xài thôi).

Bài hôm nay thì sẽ cần code một chút vì chúng ta cần xét va chạm giữa một hình tròn và một hình chữ nhật, cơ mà trong java nó lại không có lớp hình tròn sẵn, cho nên chúng ta sẽ đi xây dựng lớp hình tròn ngay bây giờ, việc này thì rất đơn giản:

Một hình tròn thì sẽ có 2 thuộc tính quan trọng là: tọa độ của tâm và bán kính, tọa độ tâm ta có thể dùng lớp Point có sẵn của java hoặc tự xây dựng 1 lớp Point cho riêng mình, nhưng ở đây ta cũng không cần xây dựng làm gì, cứ để tọa độ tâm là 2 biến kiểu nguyên cũng được, bán kính ta khai báo kiểu int luôn cho nó đơn giản nhé :v.
public class Circle {
private int x;
private int y;
private int radius;

public Circle(int x, int y, int radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
public int getRadius() {
return radius;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
Rồi, bây giờ chúng ta sẽ vẽ ra hình tròn này thử xem nhé.
các bạn thêm đoạn code sau vào class CollisionDetection, tại phương thức paintComponent(...) nhé:

g.drawOval( circle.getX() - circle.getRadius(), circle.getY()-circle.getRadius(), circle.getRadius() * 2,circle.getRadius() * 2);


Sau đó các bạn chạy thử xem, mình giải thích thêm:
Tại sao tọa độ (x, y) lại là circle.getX() - circle.getRadius(), circle.getY() và circle.getRadius()???
Các bạn lưu ý là phương thức drawOval(...) nó sẽ vẽ hình Oval nằm trong hình chữ nhật.
Để hiểu rõ phương thức drawOval(...) vẽ như nào các bạn tạm thời comment hết đoạn code trong phương thức paintComponent(...) lại rồi thêm đoạn này vào:
g.drawOval(100, 100, 200, 100);
g.drawRect(100, 100, 200, 100);

Bây giờ các bạn thử chạy lên xem, kết quả như sau:

Vậy nên để vẽ hình tròn chính xác theo tâm, ta sẽ trừ 1 đoạn bằng chính bán kính của nó.
Hai tham số sau có giá trị circle.getRadius() * 2 bởi vì ta vẽ hình tròn nên phương thức drawOval sẽ truyền vào chiều dài và rộng bằng nhau - hình oval nội tiếp hình vuông sẽ cho ra hình tròn :D.

Nói sơ qua về cách vẽ hình tròn với java thế thôi. Bây giờ là phần chính của bài hôm nay.
Bây giờ làm sao để ta có thể xét được va chạm giữa một hình tròn và một hình chữ nhật đây ta?

Chúng ta bắt đầu suy nghĩ và tìm thuật toán nhé.
Ý tưởng: để kiểm tra va chạm giữa hình chữ nhật và hình tròn ta sẽ tìm một điểm, gọi là điểm A(xA, yA) là điểm gần tâm hình tròn nhất thuộc hình chữ nhật, sau khi đã tìm ra được điểm A này rồi thì công việc còn lại là chỉ cần tính khoảng cách từ tâm hình tròn tới điểm A và so sánh với bán kính hình tròn là ok.
Các bạn xem qua hình vẽ này (ảnh mình lấy từ google và có sửa lại để dễ hình dung hơn):
Nhìn sơ thì ta cũng chỉ có mấy trường hợp như vậy thôi.
Ta quy ước tọa độ các đỉnh hình chữ nhật giống như hình vẽ mình đã chú thích luôn nhé, tọa độ tâm sẽ là C(xC, yC).
Đầu tiên ta cho tọa độ điểm A bằng với tọa độ tâm hình tròn luôn (vì điểm gần tầm nhất thì là nó luôn rồi :D), hay tọa độ A lúc này là (xA, yA) = (xC, yC).
Tiếp theo ta sẽ xét các trường hợp sau:
Nếu xC < rectLeft thì gán xA = rectLeft (như hình vẽ), ngược lại nếu xC > rectRight thì gán xA = rectRight.
Nếu yC < rectTop thì gán yC = rectTop, ngược lại nếu yC > rectBottom thì gán yC = rectBottom.
Trường hợp nữa là rectLeft < xC < rectRight thì xA = xC, rectTop < yC < rectBottom thì yA = yC (nhưng do đầu tiên ta đã gán tọa độ của A bằng tọa độ của C rồi cho nên ta không cần code đoạn này.

Bây giờ chúng ta sẽ thực hiện thuật toán bằng code, tròn class CollisionDetection các bạn tạo thêm 1 phương thức xét va chạm giữa hình tròn và hình chữ nhật như sau:
public boolean checkColiision(Rectangle rect, Circle cir) {
int rectLeft = (int) rect.getX();
int rectRight = (int) (rect.getX() + rect.getWidth());
int rectTop = (int) rect.getY();
int rectBottom = (int) (rect.getY() + rect.getHeight());

int xC = cir.getX();
int yC = cir.getY();
int xA = xC;
int yA = yC;

if (xC < rectLeft) {
xA = rectLeft;
} else if (xC > rectRight) {
xA = rectRight;
}

if (yC < rectTop) {
yA = rectTop;
} else if (yC > rectBottom) {
yA = rectBottom;
}
int dx = xA - xC;
int dy = yA - yC;
return (dx * dx + dy * dy) <= cir.getRadius() * cir.getRadius();
}

Code trên thực hiện y chang thuật toán đã nói ở trên rồi :)
Nhưng cái câu lệnh return (dx * dx + dy * dy) <= cir.getRadius() * cir.getRadius(); là sao nhỉ?

Công thức tính độ dài đoạn thẳng khi biết tọa độ 2 điểm là:
sqrt(deltaX^2 + deltaY^2)
Ở đây để đỡ thêm 1 bước tính căn bậc hai thì ta chỉ cần bình phương 2 vế lên là xong.

Trong phương thức actionPerformed(ActionEvent e) các bạn thêm dòng này vào nữa để test:
if (checkColiision(myChar.getBound(), circle)) {
System.out.println("Hinh chu nhat va cham hinh tron");
}

Kết quả chạy được như hình dưới:


Bài hôm nay dừng tại đây, cám ơn các bạn đã theo dõi :).

Lập trình game java cơ bản - phát hiện va chạm

Xin chào các bạn :D, hôm nay chúng ta túc tiệp với loạt bài về lập trình game java nhỉ :D.

Bài hôm nay, mình sẽ giới thiệu các bạn cách phát hiện va chạm hết sức đơn giản trong game.

Với những game như mấy game chạy nhảy, bắn súng chẳng hạn, thì việc phát hiện va chạm là rất quan trọng. Vậy hôm nay chúng ta sẽ làm việc này bằng một cách đơn giản nhất: đó là sử dụng luôn lớp Rectangle (mô tả Hình chữ nhật) trong java.awt.

Bây giờ chúng ta bắt đầu luôn nhé :)


Thông thường để bắt va chạm chuẩn xác nhất thì người ta sẽ phải xác định vùng bao của vật thể cần xét va chạm, mà vùng bao này thì muôn hình vạn trạng, có thể là hình tròn, hình đa giác, hay vô số hình phức tạp, đồng thời thuật toán để xét va chạm giữ các hình này thì không phải đơn giản (hiện tại mình mới làm theo kiểu vùng bao hình tròn, hình vuông, chữ nhật mà thôi :v).
Và thật may mắn khi lớp Rectangle trong java.awt đã cung cấp cho ta phương thức intersecters(...) để có thể kiểm tra va chạm giữa 2 hình chữ nhật với nhau, việc chúng ta cần làm là gọi nó ra và dùng mà thôi =)).

Để hiểu rõ hơn mình sẽ đi vào ví dụ cụ thể:
Mình sẽ tạo 2 đối tượng: MyCharacter - tượng trưng cho nhân vật chơi của chúng ta và Enemy - tượng trưng cho quân địch, 2 đối tượng này đơn giản sẽ chỉ là 2 hình chữ nhật, do đó nó sẽ có thuộc tính chung là tọa độ, chiều dài, chiều rộng, để code ngắn gọn ta sẽ tạo 1 lớp Sprite chứa các thuộc tính chung này sau đó cho 2 đối tượng kia kế thừa lại.

Ta có lớp Sprite như sau:

import java.awt.Rectangle;
public class Sprite {
protected int x;
protected int y;
protected int width;
protected int height;

public Sprite(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public Rectangle getBound() {
return new Rectangle(x, y, width, height);
}
 }


Phương thức getBound() sẽ dùng để xác định vùng bao của 2 nhân vật, ở đây vì mình làm 2 đối tượng là 2 hình chữ nhật nên vùng bao sẽ chính xác tuyệt đối.

Tiếp theo là lớp MyCharacter:

import java.awt.event.MouseEvent;
public class MyCharacter extends Sprite{
private int mx;
private int my;
public MyCharacter(int x, int y, int width, int height) {
super(x, y, width, height);
}
public void move() {
x = mx;
y = my;
}
public void mouseMoved(MouseEvent e) {
mx = e.getX();
my = e.getY();
}
}


Ta thêm 2 thuộc tính mx và my để biểu diễn tọa độ con trỏ chuột của chúng ta (bài hôm trước mình quên giới thiệu cách điều khiển nhân vật bằng chuột nên giờ mình sẽ dùng chuột điều khiển).
Phương thức mouseMoved(MouseEvent e) đơn giản là lấy tọa độ chuột gán vào cho mx và my.
Phương thức move() sẽ set lại tọa độ nhân vật bằng tọa độ chuột, hiểu đơn giản là di chuột tới đâu nhân vật sẽ chạy theo con trỏ chuột =)),  rất là chuối :)).

Tiếp đến là lớp Enemy:

public class Enemy extends Sprite {

public Enemy(int x, int y, int width, int height) {
super(x, y, width, height);
}

}

Chả có gì hết =)).

Xong, giờ tạo thêm lớp CollisionDetection như sau:

import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import javax.swing.JPanel;
import javax.swing.Timer;
public class CollisionDetection extends JPanel implements ActionListener {
private MyCharacter myChar;
private Enemy enemy;
private final int DELAY = 10;
private Timer timer;

public CollisionDetection() {
myChar = new MyCharacter(50, 50, 50, 50);
enemy = new Enemy(100, 100, 50, 50);
addMouseMotionListener(new MyAdapter());
timer = new Timer(DELAY, this);
timer.start();
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawRect(myChar.getX(), myChar.getY(), myChar.getWidth(),
myChar.getHeight());
g.drawRect(enemy.getX(), enemy.getY(), enemy.getWidth(),
enemy.getHeight());
}

@Override
public void actionPerformed(ActionEvent e) {
checkCollision();
myChar.move();
repaint();
}
public void checkCollision() {
Rectangle myCharBound = myChar.getBound();
Rectangle enemyBound = enemy.getBound();
if(myCharBound.intersects(enemyBound)) {
System.out.println("Va cham");
}
}
private class MyAdapter extends MouseMotionAdapter {
@Override
public void mouseMoved(MouseEvent e) {
myChar.mouseMoved(e);
}
}
}

Code này thì các bạn chắc đã hiểu hết vì bài trước mình đã giải thích hết rồi, ngoại trừ chỗ class MyAdapter, bài trước mình tạo class này bên ngoài, nhưng muốn tạo bên ngoài thì cần tham chiếu đối tượng myChar qua hàm tạo nữa, ở đây ta tọa luôn inner class - có nghĩa là class bên trong 1 class, và nó dùng luôn được thuộc tính class chứa nó (ở đây là dùng luôn myChar).
Phương thức checkCollision(): lấy ra 2 hình chữ nhật bao quanh 2 đối tượng, myCharBound.intersects(enemyBound), phương thức intersects(...) nói lên công dụng của nó :), phương thức này trả về true nếu 2 hình chữ nhật giao nhau và false cho trường hợp còn lại.

Các bạn chạy thử demo bằng cách tạo thêm class MainTest như sau:
import java.awt.Color;
import javax.swing.JFrame;
@SuppressWarnings("serial")
public class MainTest extends JFrame {
public MainTest() {
add(new CollisionDetection());
setTitle("Collision detection test");
setSize(500, 500);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setBackground(Color.WHITE);
setVisible(true);
}
public static void main(String[] args) {
new MainTest();
}
}



Vậy là xong bài hôm nay, bây nhiêu đó là hôm sau ta có thể bắt tay vào làm 1 game nhỏ rồi :D. Hẹn gặp lại các bạn vào bài sau.