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 xdem và dem.
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();
}
}
}
Cảm ơn chia sẻ của ad!
ReplyDeleteMua loa keo keo bass đôi 4 tấc loại nào haytại khu vực TPHCM