Wednesday, October 4, 2017

Cách làm game trong Canvas

Bài này giải thích cách làm game trong canvas, thường bạn đọc sách làm theo một game nào đó xong thì tự suy ra, ở đây tôi viết mọi thứ cho rõ ràng, đơn giản.
Có rất nhiều cách làm game, như dùng các game engine, công cụ chuyên nghiệp để làm. Nhưng mới học ta không cần chúng, chỉ dùng Canvas cũng có thể làm được nhiều thứ.
Tạo một project bất kỳ, ta sẽ làm ở class Main. Tạo thư mục drawable, copy vài cái ảnh icon nhỏ vào đó.


Copy vào class Main một class nữa, cho vào trên ngoặc đóng cuối cùng.
public class gameview extends View {
                       
public gameview(Context context) {
super(context);        
}
@Override
protected void onDraw(final Canvas canvas) {
              // TODO Auto-generated method stub
super.onDraw(canvas);
             
}
}

Class gameview này có hàm onDraw là nơi vẽ nhân vật, điều khiển game.
Bây giờ ta vẽ nhân vật đầu tiên.
Copy dòng sau vào dưới dòng extends của class gameview.
private Bitmap mcuoi;
Copy dòng sau vào dưới super(context);
mcuoi = BitmapFactory.decodeResource(getResources(), R.drawable.cuoi);
Copy dòng sau vào dưới super.onDraw(canvas);
canvas.drawBitmap(mcuoi, 50, 100, null);
Đưa gameview vào class Main bằng cách khai báo đối tượng tên game lên trên dòng override của class Main.
gameview game;
Rào dòng setcontentView lại và thêm 2 dòng sau
game=new gameview(this);
setContentView(game);  

Chạy thử để thấy nhân vật đã ra màn hình.

Nền ứng dụng có vẻ chưa trắng lắm, thêm dòng sau game.setBackgroundColor(Color.WHITE);
vào trên dòng setContentView(game);
Ta vẽ nhân vật tại vị trí 50, 100, 50 là tọa độ x, 100 là y.


Dù bạn để màn hình ngang hay dọc thì chiều ngang của thiết bị luôn là x, dọc luôn là y. Nên lúc quay ngang thì x lớn hơn y.
Bây giờ để icon di chuyển, hãy khai báo biến int x=0; dưới dòng extends.
Sửa số 50 trong lệnh draw thành 50+x, thêm hai dòng
x=x+2;
invalidate();


Chạy thử để thấy icon đã di chuyển sang phải.
Tiếp tục thêm vào dưới dòng extends int y=0;
Sửa số 100 trong lệnh draw thành 100+y, thêm dòng
y=y+2;
Chạy thử để thấy icon di chuyển xuống dưới.


Bây giờ ta muốn icon di chuyển ngang qua màn hình, đi hết lại chui ra đi tiếp thì thế nào?
Khai báo thêm hai biến r, c dưới dòng extends int r, c;
Thêm vào dưới dòng super(context);
DisplayMetrics metrics = getResources().getDisplayMetrics(); 
r = metrics.widthPixels;
c = metrics.heightPixels;
Sửa lệnh draw thành canvas.drawBitmap(mcuoi, x, 100, null);
Thêm lệnh if
if(x>r){
x=0;
}
Ta để là nếu khi nào x tăng lên lớn hơn độ rộng màn hình thì nó quay về 0.
Để khi quay lại icon trông có vẻ chui ra từ bên trong lề trái, ta thay vào trong lệnh if
x=-mcuoi.getWidth();
Ta lấy số âm của độ rộng icon, tức là mép phải của nó vừa đúng bằng mép trái màn hình nên trông nó như đi vào từ bên trong.


Bây giờ ta muốn icon đi chạm lề phải thì đi ngược lại.
Khai báo một biến boolean phai=true;
Sửa code thành.
if(phai==true){
x=x+2;
}
else{
x=x-2;
}            
if(x>r){
phai=false;
}
if(x<0){
phai=true;
}


Bây giờ ta muốn icon đi sang phải khi có chạm tay vào màn hình, nếu không chạm nó tự lùi về bên trái.
Copy các dòng sau xuống dưới invalidate nhưng ở ngoài ngoặc.
@Override
public boolean onTouchEvent(MotionEvent event) {
    
     switch (event.getAction() & MotionEvent.ACTION_MASK) {
     //nhấc tay
     case MotionEvent.ACTION_UP:               
          phai = false;               
          break;
   //chạm tay
     case MotionEvent.ACTION_DOWN:
          phai=true;   
     }        
     return true;      
     }


Chạy thử, lúc icon chạy về trái hãy chạm tay vào, nó chạy sang phải, hết đường nó lại quay về dù vẫn đang chạm tay, đó là vì khi đó lệnh if(x>r)có hiệu lực do nó lại được gọi sau hành vi chạm tay.
if(x>r){
phai=false;
}
Khi đang chạy chương trình, hàm invalidate() liên tục cập nhật vị trí icon và các biến, do tọa độ x, y thay đổi nên icon đi chuyển, nếu có biến nào phụ thuộc x,y nó cũng thay đổi theo.
Bây giờ copy một icon hình kim cương vào drawable.
Khai báo Bitmap kimcuong, khởi tạo.
kimcuong = BitmapFactory.decodeResource(getResources(), R.drawable.kim);
Vẽ nó ra.
canvas.drawBitmap(kimcuong, 100, y, null);
Làm nó di chuyển xuống dọc màn hình
y=y+2;


Chạy thử để thấy nó đã đi xuống và mất tích luôn, kệ nó, để đơn giản ta kệ nó thế đã.
Bây giờ ta muốn khi có va chạm giữa hai icon thì phát ra âm thanh.
Thêm các dòng sau lên trên invalidate()
Paint paint = new Paint();
paint.setColor(Color.parseColor("#FF00FF"));
Rect touch1 = new Rect(x, 100,x+mcuoi.getWidth(), 100+mcuoi.getHeight());
Rect touch2 = new Rect(100, y,100+mcuoi.getWidth(), y+mcuoi.getHeight());
canvas.drawRect(x, 100,x+mcuoi.getWidth(), 100+mcuoi.getHeight(), paint);
canvas.drawRect(100, y,100+mcuoi.getWidth(), y+mcuoi.getHeight(), paint);
Ta vẽ các Rect hình chữ nhật bao lấy icon, kiểm tra vẽ đúng chưa bằng cách vẽ các hình tô màu đè lên chúng, chạy thử nếu thấy đúng vị trí khi icon di chuyển là được rồi.


Sau đó comment 2 dòng canvas.drawRectđi để ta còn nhìn thấy icon.
Nếu icon của bạn có các khoảng trắng bao quanh, hình chữ nhật trông sẽ to hơn vì nó bao toàn bộ cái hình đó. Nếu muốn nó bé lại, ta chỉnh x+mcuoi.getWidth()thành x+20 chẳng hạn, tùy bạn ước lượng cho vừa khít thì khi va chạm nó mới chuẩn.
Tránh trường hợp lệch quá, giả sử ta làm game bắn máy bay đạn chưa chạm tới thì máy bay đã nổ tung rồi nó vô lý.
Copy thêm các dòng sau vào.
if (Rect.intersects(touch1, touch2)) {
                  
try {
Uri notification = RingtoneManager
                                  .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
                        Ringtone r = RingtoneManager.getRingtone(
getApplicationContext(), notification);
                        r.play();
                   } catch (Exception e) {
                        e.printStackTrace();
                   }
              }
Ta dùng các Rect đã vẽ để lấy va chạm, tạo tiếng chuông báo.


Chạy thử, chạm tay điều chỉnh để icon chạm nhau, thay đổi tọa độ nếu khó chạm quá, phải có tiếng chuông khi va chạm mới đúng.
Bây giờ ta muốn nếu có va chạm thì icon mặt cười bắn vào giữa màn hình đứng im, và hiện chữ Game over to tướng ra màn hình.
Hãy tạo một biến boolean over=false;
Bạn tự biết phải để nó chỗ nào.
Trong lệnh set intersects, thêm dòng over=true;
Copy thêm các dòng sau lên trên invalidate()
if(over==true){
x=r/2;
paint.setTextSize(60);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("Game Over", r / 2, 100, paint);
}


Chạy thử để thấy kết quả.

Bây giờ ta lại muốn sau khi game over, nếu người dùng chạm tay vào màn hình thì chơi lại được từ đầu.
Copy đoạn sau vào trong case MotionEvent.ACTION_DOWN:
if(over==true){
over=false;
x=-mcuoi.getWidth();
y=-kimcuong.getHeight();
}

Ta dùng lệnh if, nếu over chuyển sang true tức là lúc game over ta set nó là là false, đồng thời cho tọa độ icon về khởi đầu, ta cho nó chui kín vào trong bằng trừ độ rộng, cao, nếu để bằng 0 nó sẽ xuất hiện lại ngoài mép.
Hiện lúc over icon kimcuong vẫn chạy xuống, muốn tất cả dừng lại, ta cho các lệnh làm chúng chạy vào trong lệnh      if(over==false){

Bây giờ thay vì chạm là over, ta muốn đó là ăn điểm, ghi ra màn hình.
Khai báo một biến int an=0; lên trên.
Trong lệnh intersects, rào dòng over=true; thành comment, thêm dòng này vào y=-100;
Copy các dòng sau lên trên invalidate()
if(y==-100){
an=an+1;
}
paint.setTextSize(33);
canvas.drawText("Score: "+an, r/2-10, 25, paint);


Mỗi khi va chạm, ta bật y về -100 để làm biến mất icon kimcuong, đồng thời trong lệnh if(y==-100) ta tăng biến an lên 1, ghi nó ra màn hình, lệnh invalidate() đảm bảo số điểm được cập nhật liên tục.
Chạy thử, chạm tay vào màn hình để icon va chạm nhau, mỗi lần lại thêm một điểm ghi ra Score.

Bài này ta đã thấy các yếu tố cơ bản để làm game trong canvas, để xem những tùy biến phức tạp hơn, hãy xem bài làm game Flappy bird.

No comments:

Post a Comment