Wednesday, October 4, 2017

Flappy bird

Flappy bird là một game bùng nổ đặc biệt trên Play Store. Ta sẽ tái hiện nó bằng cách dùng Canvas để làm. Chú ý là Nguyễn Hà Đông có thể làm theo cách hoàn toàn khác cách ta làm tại đây.
Chuẩn bị một icon bé khoảng 30x30 có hình đầu con chim, có thể làm nó trong Word dùng insert Shape.

Chuẩn bị icon hình thanh chắn màu xanh, rộng 30, cao 125.

Tạo thư mục drawable dưới res, copy icon vào đó. Tạo thêm một thư mục tên raw, để một file âm thanh mp3 dài một giây, là tiếng va chạm khi con chim đụng phải cột.

Copy  hai dòng sau thêm vào khai báo class Main trong Androidmanifest.xml.
android:screenOrientation="landscape"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
Ta để màn hình full kín, để chiều màn hình là ngang.

Khi bạn quay ngang màn hình, chỉ số x sẽ là chiều dài nằm ngang của màn hình, y là chiều cao

Góc trên bên trái sẽ là gốc tọa độ 0,0. Một image được vẽ phía trên bên phải sẽ có chỉ số x lớn và y nhỏ, nếu vẽ nó sát biên luôn thì y=0.
Việc nhớ rõ tọa độ x, y giúp khi làm đỡ bị nhầm, nhưng ta sẽ có cách để kiểm tra xem tọa độ đúng chưa nên cứ làm rồi sẽ nhớ.
Tạo một class gameview bên dưới class Main, ta điều khiển game trong nó.
Copy những dòng sau xuống trên ngoặc đóng cuối cùng.
public class gameview extends View {
public gameview(Context context) {
              super(context);
}        
          @SuppressLint({ "NewApi", "DrawAllocation" })
          @Override
          protected void onDraw(final Canvas canvas) {
              // TODO Auto-generated method stub
              super.onDraw(canvas);
invalidate();
          }   
}
Copy hai dòng sau xuống ngay dưới mở ngoặc class Main
String kyluc = "";
private gameview game;
Rào dòng setContentView thành comment, copy thêm các dòng sau xuống dưới comment đó.
game = new gameview(this);
game.setBackgroundColor(Color.WHITE);
setContentView(game);

Chạy thử để thấy màn hình trắng trơn.
Copy xuống dưới dòng public class gameview extends View {
private Paint paint;
private Bitmap chim, thanh, thanh2;       
int y;       
int xthanh, xthanh2, ythanh,xdem;
int dem;               
Boolean hit = false;        
Boolean cham = false;
Boolean chay = true;
int soky = 0;
private SoundPool soundPool;
int soundId;      
int bump = -1;
int r, c;    
Nhập các thư viện cần dùng vào.
Copy tiếp xuống dưới dòng super(context);
paint = new Paint();
paint.setColor(Color.parseColor("#FF00FF"));             
paint.setTextAlign(Align.CENTER);
paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
DisplayMetrics metrics = getResources().getDisplayMetrics();           
     r = metrics.widthPixels;
     c = metrics.heightPixels;   
chim = BitmapFactory.decodeResource(getResources(), R.drawable.ki);
thanh = BitmapFactory.decodeResource(getResources(), R.drawable.kb);
thanh2 = BitmapFactory.decodeResource(getResources(), R.drawable.kb);
xthanh = r ;
xthanh2 = r;
xdem=xthanh-200+thanh.getWidth()+chim.getWidth();;
y = c / 2;        
             
soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
bump = soundPool.load(context, R.raw.bump, 1);      

Nhập các thư viện cần dùng vào, đảm bảo tên các file icon và mp3 đúng như bạn đặt.
Ở đây ta đang khai báo và khởi tạo các biến. Paint để vẽ chữ Game Over, r, c để lấy chiều rộng, cao màn hình, chim, thanh là hình con chim, cái thanh chắn sẽ vẽ. xthanh là tọa độ của thanh chắn, y là tọa độ cao thấp của con chim, xdem dùng để đếm số lần qua thanh chắn. Soundpool để tạo âm thanh khi va chạm.
Để đơn giản, ban đầu ta chỉ làm 2 thanh chắn di chuyển về phía con chim, nếu chạm vào là game over.
Copy các dòng sau xuống trên ngoặc đóng dưới cùng.
public void docdong() {
          try {
          String FILE_NAME = "kyluc.txt";      
          FileInputStream fIS = openFileInput(FILE_NAME);     
          byte[] arrayData = new byte[fIS.available()];
          if ((fIS.read(arrayData)) != -1) {
          String fileContent = new String(arrayData);
          kyluc = fileContent;                 
          }        
          fIS.close();
          } catch (Exception e) {          
              // Log.e("Error", e.toString());
          }
     }
public void ghidong(String d) {
          try {
              FileOutputStream fos = openFileOutput("kyluc.txt",
                        Context.MODE_APPEND);
              OutputStreamWriter osw = new OutputStreamWriter(fos);
              osw.append(d);
              osw.flush();
              osw.close();
          } catch (FileNotFoundException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
     }
Đây là hai hàm để ghi lại số lần qua thanh tức Score, một cái để đọc file đã ghi lấy ra kết quả.
Copy các dòng sau xuống dưới super.onDraw(canvas);
Paint paint2 = new Paint();
paint2.setColor(Color.BLACK);
paint2.setTextAlign(Align.CENTER);
paint2.setTextSize(25);          
paint2.setTextAlign(Align.CENTER);
                            
canvas.drawBitmap(thanh, xthanh, 0, null);
canvas.drawBitmap(thanh2, xthanh2, 340, null);
             
Rect touch=new Rect(200, y,200 + chim.getWidth(),y+chim.getHeight());
Rect touch1 = new Rect(xthanh, 0, xthanh + thanh.getWidth(), thanh.getHeight());
Rect touch2 = new Rect(xthanh2, 340, xthanh2 + thanh.getWidth(), 340+thanh.getHeight());
if (chay == true) {
     docdong();
if (kyluc.length() > 0) {
     soky = Integer.parseInt(kyluc);
     }
     xthanh = xthanh - 7;
     xthanh2 = xthanh2 - 7;      
     xdem = xdem-7;                            
     canvas.drawText("Record: " + soky + " - " + "Score: " + dem,
                                  r / 2, 20, paint2);         

          if (cham == true) {                  
          y=y-5;                      
     float rotation = -10.0f;                       
          Matrix matrix = new Matrix();
          float px = 200;
          float py = y;                    
          matrix.postRotate(rotation);
          matrix.postTranslate(px, py);
          canvas.drawBitmap(chim, matrix, null);                   
          } else {
          y=y+5;
          float rotation = 10.0f;                   
          Matrix matrix = new Matrix();
          float px = 200;
          float py = y;                        
          matrix.postRotate(rotation);
          matrix.postTranslate(px, py);
          canvas.drawBitmap(chim, matrix, null);
         }
          if (Rect.intersects(touch, touch1)) {                    
          hit=true;
          soundPool.play(bump, 0.9f, 0.1f, 1, 0, 0.7f);            
          }
          if (Rect.intersects(touch, touch2)) {                    
          hit=true;
          soundPool.play(bump, 0.9f, 0.1f, 1, 0, 0.7f);                
          }                 
          }   
Chạy thử để thấy ta đã có hình con chim và 2 cái thanh nhưng con chim rơi ngay xuống không điều khiển được.


Bây giờ xem code một chút.
Ta khai báo thêm paint2 để set nó chữ nhỏ, màu đen, dùng để vẽ cái chữ Record và Score trên đầu.
Dòng canvas.drawBitmap(thanh, xthanh, 0, null);
Vẽ ra cái thanh bên trên, lúc khởi tạo dưới super(context); ta để xthanh = r ; tức nó sẽ vẽ ở mép ngoài bên phải màn hình,  số 0 là tọa độ y, tức nó vẽ ngay từ mép trên màn hình.
canvas.drawBitmap(thanh2, xthanh2, 340, null);
Thanh 2 ta vẽ cũng từ mép ngoài bên phải, y = 340 tức vẽ nó xuống cách thanh 1 để có khoảng trống cho con chim chui qua, bạn nên để rộng dễ chạy thử.
Ba dòng Rect touch để tạo ra các hình chữ nhật bao quanh con chim và thanh, các hình này dùng để xác định va chạm của chúng.
Nhìn tiếp xuống code trong lệnh if, chay == true  tức là khi game đang chạy, chưa Over.
Quan trọng nhất là chỗ
xthanh = xthanh - 7;
xthanh2 = xthanh2 - 7; 
Đây chính là cái làm cho thanh chắn di chuyển, ta vẽ nó với tọa độ x ở ngoài biên nên trừ dần đi thì nó chạy dần vào bên trái màn hình.
Tiếp đến là lệnh if (cham == true) { tức là khi có chạm tay của người dùng vào màn hình.
Ta muốn con chim bay lên khi có chạm nên có dòng y=y-5;vì y trên cùng bằng 0 nên khi giảm y đi thì con chim bay lên.
Ngược lại trong lệnh else bên dưới ta để y=y+5; tức là bỏ tay ra thì tăng y nên con chim trôi xuống.
Để con chim bay lên thì hơi ngóc đầu lên ta dùng đoạn.
float rotation = -10.0f;         
Matrix matrix = new Matrix();
float px = 200;
float py = y;                    
matrix.postRotate(rotation);
matrix.postTranslate(px, py);
canvas.drawBitmap(chim, matrix, null);
Để nó bay xuống thì chúi đầu xuống, trong lệnh else ở dưới ta sửa float rotation = -10.0f;   thành float rotation = 10.0f;    
Ta vẽ con chim tại x=200 tức cách mép trái 200dp, y là biến số để khi tăng giảm y con chim sẽ lên xuống.
Nếu ta muốn con chim lúc nào cũng bay ngang, ta dùng dòng sau ngay dưới chỗ vẽ thanh, thanh2.
canvas.drawBitmap(chim, 200,y, null);
Lúc đó ta phải xóa lệnh vẽ chim trong if (cham == true) nếu không sẽ bị trùng.
Bây giờ đến lệnh if (Rect.intersects(touch, touch1)) {
Đây là để set cho việc gì sẽ xảy ra khi có va chạm. Ta cho biến hit=true; và bật âm thanh lên, touch là hình chữ nhật bao con chim, touch1 là thanh 1, touch2 là thanh 2.
Khi mới học ta dễ lẫn lộn các tọa độ x, y. Để chắc chắn các hình chữ nhật đã bao đúng đối tượng, ta kiểm tra bằng cách thêm các dòng sau xuống dưới chỗ vẽ rect touch.
canvas.drawRect(200, y,200 + chim.getWidth(),y+chim.getHeight(),paint);
canvas.drawRect(xthanh, 0,xthanh + thanh.getWidth(),thanh.getHeight(),
                   paint);      
canvas.drawRect(xthanh2, 340, xthanh2 + thanh.getWidth(), 340+thanh.getHeight(),paint);    

Chạy thử để thấy các cột đã được bao kín màu hồng, cả hình chữ nhật nhỏ chỗ vị trí con chim tức là ta đã set đúng vị trí.
Xong thì rào chúng lại thành comment, chúng chỉ dùng để kiểm tra việc set rect cho chuẩn.
Bây giờ copy đoạn sau xuống dưới chữ invalidate() một dấu ngoặc đóng }
@SuppressLint("ClickableViewAccessibility")    
          @Override
          public boolean onTouchEvent(MotionEvent event) {             
              switch (event.getAction() & MotionEvent.ACTION_MASK) {        
              case MotionEvent.ACTION_UP:
              cham = false;
              break;       
              case MotionEvent.ACTION_DOWN:
              cham=true;   
              if (chay == false) {
              chay = true;
              hit=false;
              y=c/2;
              dem = 0;
              xthanh = r - thanh.getWidth();
              xthanh2 = r - thanh.getWidth();
              xdem=xthanh-200+thanh.getWidth()+chim.getWidth();
              }                           
              break;
              }
              return true;
          }

Lúc trước ta chạy thử thì không điều khiển được con chim, vì ta chưa bắt hành động chạm tay vào màn hình. Đây chính là các dòng code đó.
Khi người dùng chạm tay, đó là case MotionEvent.ACTION_DOWN:
Ta cho biến cham=true;,ở trên ta đã có code nếu cham=true;thì cho con chim bay lên rồi.
Tiếp theo là lệnh if (chay == false) { tức là nếu Game đã Over thì ta khởi tạo lại các biến như ban đầu.
Trường hợp người dùng nhấc tay khỏi màn hình là case MotionEvent.ACTION_UP: ta cho biến cham = false;
Ở trên ta cũng đã có lệnh điều khiển rồi, trong cú pháp else ta đã cho con chim bay chúi xuống.
Chạy thử để thấy đã điều khiển được con chim, đâm vào cột đã có âm thanh nhưng cái cột chỉ đi qua có một lần.
Bây giờ ta muốn cái cột quay lại, va chạm thì game over, hãy thêm các dòng sau vào ngay trên chữ invalidate()
if (xthanh < -thanh.getWidth()) {
     xthanh = r;
     xthanh2 = r;
     xdem=xthanh-200+thanh.getWidth()+chim.getWidth();;
     }
if (y >c) {
     chay=false;
}
if (y <0) {
     y=0;
}
if(xdem<0){
     xdem=r;
     dem=dem+1;
     }
if (hit == true) {
cham = false;
chay=false;
y = y + 10;
float rotation = 30.0f;                   
Matrix matrix = new Matrix();
float px = 200;
float py = y;                    
matrix.postRotate(rotation);
matrix.postTranslate(px, py);
canvas.drawBitmap(chim, matrix, null);
}        
if (chay == false) {
if (dem > soky) {
deleteFile("kyluc.txt");
ghidong(String.valueOf(dem));
paint.setTextAlign(Align.CENTER);
paint.setTextSize(30);
canvas.drawText("New record: " + dem + " ", r / 2, 30,
                                  paint);
          }
  paint.setTextSize(60);
                  
canvas.drawText("Game Over", r / 2, 100, paint);             
canvas.drawText("Tap to replay!", r / 2, c / 2 + 70, paint);
                   paint.setTextSize(35);
canvas.drawText("Score: " + dem, r / 2, 160, paint);              
              }
Lệnh if đầu tiên, là khi cái thanh chạy vào hết rồi, ta cho nó = r tức lại cho ra ngoài biên phải.
Lệnh if (y >c) { là để nếu con chim rơi xuống qua mép dưới điện thoại thì game over
Lệnh if (y <0) { là để không cho con chim lên quá mép trên, tức là nó chỉ kịch đến đó thôi.
Lệnh if (hit == true) { là để khi có va chạm, ta cho các biến chạy = false để game over, tăng tốc độ rơi xuống của con chim bằng dòng y = y + 10;
Bên dưới ta cho con chim chúi xuống một góc 30 độ cho rõ là nó đang rơi.
Lệnh if (chay == false) { là lúc game over, ta kiểm tra nếu số lần qua thanh lớn hơn các lần chơi trước, tức lớn hơn số kỷ lục thì ta gọi hàm ghidong() để ghi số đó vào, hiện thông báo kỷ lục mới.
Để biết kỷ lục của lần chơi trước, khi mới vào game, tại lệnh if (chay == true){ ta đã gọi hàm đọc file ghi kỷ lục.
docdong();
if (kyluc.length() > 0) {
soky = Integer.parseInt(kyluc);
}
Như vậy lần đầu chơi đương nhiên soky =0.
Tiếp theo dùng dòng paint.setTextSize(60); để set font chữ to lên và ghi ra chữ Game Over thật lớn cùng dòng Tap to replay và số điểm đã ghi được.

Còn một vấn đề là làm sao để đếm được số lần con chim qua thanh. Ta khai báo hai biến xdemdem.
Bên trên lúc khởi tạo ta cho xdem=xthanh-200+thanh.getWidth()+chim.getWidth(); mục đích là để khi thanh chắn qua chỗ con chim là chỗ có x=200 thì xdem của ta đã về đến mép trái. Vì thanh chắn và con chim đều có độ rộng, nên để đảm bảo con chim qua toàn bộ thân mình ta cộng thêm vào độ rộng của nó và của thanh chắn.
Sau đó dùng lệnh
if(xdem<0){
xdem=r;
dem=dem+1;
}
Khi xdem vừa nhỏ hơn 0, ta lập tức cho xdem một giá trị lớn nào đó, đây là r, độ rộng màn hình, và đếm thêm một lượt.
Để xdem chạy song song với chỉ số x của thanh chắn, bên trên ta đã có lệnh xdem = xdem-7;  
Khi thanh chạy hết và quay về, ta cũng cho xdem trở lại giá trị ban đầu
xdem=xthanh-200+thanh.getWidth()+chim.getWidth();
Khi Game over người dùng chơi lại, ta cũng cho xdem đúng bằng giá trị đó, đồng thời set dem=0 tức là bắt đầu đếm lại từ đầu.
Để tránh việc bạn copy code vào những chỗ không đúng khiến nó không chạy, tôi copy toàn bộ code game Flappy Bird xuống dưới, bạn copy nguyên xi vào là có một project chạy được.
public class MainActivity extends Activity {
     String kyluc = "";
     private gameview game;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          // setContentView(R.layout.activity_main);
          game = new gameview(this);
          game.setBackgroundColor(Color.WHITE);
          setContentView(game);
     }
     public class gameview extends View {
          private Paint paint;
          private Bitmap chim, thanh, thanh2;
          int y;
          int xthanh, xthanh2, ythanh, xdem;
          int dem;
          Boolean hit = false;
          Boolean cham = false;
          Boolean chay = true;
          int soky = 0;
          private SoundPool soundPool;
          int soundId;
          int bump = -1;
          int r, c;
          public gameview(Context context) {
              super(context);
               paint = new Paint();
              paint.setColor(Color.parseColor("#FF00FF"));
              paint.setTextAlign(Align.CENTER);
     paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
          DisplayMetrics metrics = getResources().getDisplayMetrics();
              r = metrics.widthPixels;
              c = metrics.heightPixels;
     chim = BitmapFactory.decodeResource(getResources(), R.drawable.ki);
     thanh = BitmapFactory.decodeResource(getResources(), R.drawable.kb);
     thanh2 = BitmapFactory.decodeResource(getResources(), R.drawable.kb);
              xthanh = r;
              xthanh2 = r;
              xdem = xthanh - 200 + thanh.getWidth() + chim.getWidth();
              y = c / 2;

              soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
              bump = soundPool.load(context, R.raw.bump, 1);
          }
          @SuppressLint({ "NewApi", "DrawAllocation" })
          @Override
          protected void onDraw(final Canvas canvas) {
              // TODO Auto-generated method stub
              super.onDraw(canvas);
              Paint paint2 = new Paint();
              paint2.setColor(Color.BLACK);
              paint2.setTextAlign(Align.CENTER);
              paint2.setTextSize(25);
              paint2.setTextAlign(Align.CENTER);

              canvas.drawBitmap(thanh, xthanh, 0, null);
              canvas.drawBitmap(thanh2, xthanh2, 340, null);

     Rect touch = new Rect(200, y, 200 + chim.getWidth(), y
                        + chim.getHeight());
     Rect touch1 = new Rect(xthanh, 0, xthanh + thanh.getWidth(),
                        thanh.getHeight());
     Rect touch2 = new Rect(xthanh2, 340, xthanh2 + thanh.getWidth(),
                        340 + thanh.getHeight());
              // canvas.drawRect(200, y,200 +
              // chim.getWidth(),y+chim.getHeight(),paint);
              // canvas.drawRect(xthanh, 0,xthanh +
              // thanh.getWidth(),thanh.getHeight(),
              // paint);
              // canvas.drawRect(xthanh2, 340, xthanh2 + thanh.getWidth(),
              // 340+thanh.getHeight(),
              // paint);
              if (chay == true) {
                   docdong();
                   if (kyluc.length() > 0) {
                        soky = Integer.parseInt(kyluc);
                   }
                   xthanh = xthanh - 7;
                   xthanh2 = xthanh2 - 7;
                   xdem = xdem - 7;
          canvas.drawText("Record: " + soky + " - " + "Score: " + dem,
                             r / 2, 20, paint2);

                   if (cham == true) {
                        y = y - 5;
                        float rotation = -10.0f;
                        Matrix matrix = new Matrix();
                        float px = 200;
                        float py = y;
                        matrix.postRotate(rotation);
                        matrix.postTranslate(px, py);
                        canvas.drawBitmap(chim, matrix, null);
                   } else {
                        y = y + 5;
                        float rotation = 10.0f;
                        Matrix matrix = new Matrix();
                        float px = 200;
                        float py = y;
                        matrix.postRotate(rotation);
                        matrix.postTranslate(px, py);
                        canvas.drawBitmap(chim, matrix, null);
                   }
                   if (Rect.intersects(touch, touch1)) {
                        hit = true;
                        soundPool.play(bump, 0.9f, 0.1f, 1, 0, 0.7f);
                   }
                   if (Rect.intersects(touch, touch2)) {
                        hit = true;
                        soundPool.play(bump, 0.9f, 0.1f, 1, 0, 0.7f);
                   }
              }
              if (xthanh < -thanh.getWidth()) {
                   xthanh = r;
                   xthanh2 = r;
                   xdem = xthanh - 200 + thanh.getWidth() + chim.getWidth();
              }
              if (y > c) {
                   chay = false;
              }
              if (y < 0) {
                   y = 0;
              }
              if (xdem < 0) {
                   xdem = r;
                   dem = dem + 1;
              }
              if (hit == true) {
                   cham = false;
                   chay = false;
                   y = y + 10;
                   float rotation = 30.0f;
                   Matrix matrix = new Matrix();
                   float px = 200;
                   float py = y;
                   matrix.postRotate(rotation);
                   matrix.postTranslate(px, py);
                   canvas.drawBitmap(chim, matrix, null);
              }

              if (chay == false) {
                   if (dem > soky) {
                        deleteFile("kyluc.txt");
                        ghidong(String.valueOf(dem));
                        paint.setTextAlign(Align.CENTER);
                        paint.setTextSize(30);
          canvas.drawText("New record: " + dem + " ", r / 2, 30,
                                  paint);
                   }
                   paint.setTextSize(60);
                   canvas.drawText("Game Over", r / 2, 100, paint);
          canvas.drawText("Tap to replay!", r / 2, c / 2 + 70, paint);
                   paint.setTextSize(35);
                   canvas.drawText("Score: " + dem, r / 2, 160, paint);
              }
              invalidate();
          }
          @SuppressLint("ClickableViewAccessibility")
          @Override
          public boolean onTouchEvent(MotionEvent event) {
              switch (event.getAction() & MotionEvent.ACTION_MASK) {
              case MotionEvent.ACTION_UP:
                   cham = false;
                   break;
              case MotionEvent.ACTION_DOWN:
                   cham = true;
                   if (chay == false) {
                   chay = true;
                   hit = false;
                   y = c / 2;
                   dem = 0;
                   xthanh = r - thanh.getWidth();
                   xthanh2 = r - thanh.getWidth();
              xdem = xthanh - 200 + thanh.getWidth() + chim.getWidth();
                   }
                   break;
              }
              return true;
          }
     }
     public void docdong() {
          try {
              String FILE_NAME = "kyluc.txt";
              FileInputStream fIS = openFileInput(FILE_NAME);
              byte[] arrayData = new byte[fIS.available()];
              if ((fIS.read(arrayData)) != -1) {
                   String fileContent = new String(arrayData);
                   kyluc = fileContent;
              }
              fIS.close();
          } catch (Exception e) {
              // Log.e("Error", e.toString());
          }
     }
     public void ghidong(String d) {
          try {
              FileOutputStream fos = openFileOutput("kyluc.txt",
                        Context.MODE_APPEND);
              OutputStreamWriter osw = new OutputStreamWriter(fos);
              osw.append(d);
              osw.flush();
              osw.close();
          } catch (FileNotFoundException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
     }

}

1 comment: