j3259の日記: Writing Tetris using Google Android (アンドロイドでテトリスを書く)
T-Mobile G1を入手したのを機に,Android を勉強するためにテトリスを書く。Ruby でテトリスを書くを参考にしつつ,多少改良を加えた。
main.xml, Tetrix.java (Activity), TetrixThread.java は Android サンプルの Lunar Lander を参考にして View を作ってるだけ。ジェスチャーの自動検知をセットアップして,TetrixThread にて fling(指をスライドさせる動作)のx,y秒速から角度を計算してそれぞれにアクションに割り当ててるのが Androidっぽいかも。
以下は特に Android と関係なくて Java のクラスをどうするかって話だけど,何でもかんでもボードに詰め込むんじゃなくて,Game というクラスを作って,Board はゲームのロジックに関知しないようにしたところすっきりした。Ruby のブロックを真似た Game.rotate にしようとしたけど,匿名クラスを毎回生成するっていう富豪なコードになってしまった。毎回 new することもないんだろうけど,いいでしょう。
public boolean rotate() {
return transform(new Transformable() {
@Override
public void transform() {
m_currentBlock.rotate(Math.PI / 2.0);
}
});
}
ブロックに関しても,char じゃなくて列挙型でちゃんと定義して,ブロックの初期形も列挙内で定義した。
T(0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f),
----------->8---------------
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<org.doubletype.gtetrix.TetrixView android:id="@+id/tetrix_view"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
/>
</LinearLayout>
----------->8---------------
Tetrix.java
package org.doubletype.gtetrix;
import android.app.Activity;
import android.os.Bundle;
public class Tetrix extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
----------->8---------------
TetrixView.java
package org.doubletype.gtetrix;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
public class TetrixView extends SurfaceView implements SurfaceHolder.Callback {
private TetrixThread m_thread;
public TetrixView(Context a_context, AttributeSet a_attrs) {
super(a_context, a_attrs);
// register our interest in hearing about changes to our surface
SurfaceHolder holder = getHolder();
holder.addCallback(this);
// create thread only; it's started in surfaceCreated()
m_thread = new TetrixThread(holder, a_context, new Handler() {
@Override
public void handleMessage(Message m) {
// mStatusText.setVisibility(m.getData().getInt("viz"));
// mStatusText.setText(m.getData().getString("text"));
}
});
setFocusable(true); // make sure we get key events
setLongClickable(true);
setGesture();
}
private void setGesture() {
final GestureDetector gestureDetector = new GestureDetector(
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
m_thread.addFling(velocityX, velocityY);
return true;
}
});
this.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
m_thread.setSurfaceSize(width, height);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// start the thread here so that we don't busy-wait in run()
// waiting for the surface to be created
m_thread.setRunning(true);
m_thread.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// we have to tell thread to shut down & wait for it to finish, or else
// it might touch the Surface after we return and explode
boolean retry = true;
m_thread.setRunning(false);
while (retry) {
try {
m_thread.join();
retry = false;
} catch (InterruptedException e) {
}
}
}
}
----------->8---------------
TetrixThread.java
package org.doubletype.gtetrix;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Handler;
import android.util.Log;
import android.view.SurfaceHolder;
public class TetrixThread extends Thread {
private final String TAG = "TetrixThread";
private SurfaceHolder m_holder;
private Handler m_handler;
private Context m_context;
private boolean m_isRunning;
private Paint m_black;
private long m_fps;
private int m_targetFps;
private int m_frameQuantum;
private int m_canvasWidth;
private int m_canvasHeight;
private Game m_game;
public TetrixThread(SurfaceHolder a_holder, Context a_context,
Handler a_handler) {
// get handles to some important objects
m_holder = a_holder;
m_handler = a_handler;
m_context = a_context;
// Resources res = a_context.getResources();
m_targetFps = 30;
m_frameQuantum = 1000 / m_targetFps;
m_black = new Paint();
m_black.setAntiAlias(false);
m_black.setARGB(255, 0, 0, 0);
m_game = new Game();
}
@Override
/**
*/
public void run() {
Log.i(TAG, "run");
long diffs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
long diff;
int diffPos = 0;
int frame = 0;
while (m_isRunning) {
long start = System.currentTimeMillis();
Canvas canvas = null;
try {
canvas = m_holder.lockCanvas(null);
synchronized (m_holder) {
if (frame++ == 0) {
m_game.tick();
} else {
if (frame == m_targetFps) {
frame = 0;
} // if
} // else
doDraw(canvas);
}
} finally {
// do this in a finally so that if an exception is thrown
// during the above, we don't leave the Surface in an
// inconsistent state
if (canvas != null) {
m_holder.unlockCanvasAndPost(canvas);
} // if
} // try-finally
diff = System.currentTimeMillis() - start;
if (diff < m_frameQuantum && m_isRunning) {
try {
Thread.sleep(m_frameQuantum - diff);
} catch (InterruptedException e) {
// e.printStackTrace();
}
} // if
diff = System.currentTimeMillis() - start;
if (++diffPos > 9) {
diffPos = 0;
} // if
diffs[diffPos] = diff;
long total = 0;
for (long value : diffs) {
total += value;
} // for
m_fps = 10000 / total;
}
}
/**
*/
private void doDraw(Canvas a_canvas) {
a_canvas.drawRect(0, 0, m_canvasWidth, m_canvasHeight, m_black);
m_game.draw(a_canvas);
}
public void setRunning(boolean b) {
m_isRunning = b;
}
public void setSurfaceSize(int a_width, int a_height) {
// synchronized to make sure these all change atomically
synchronized (m_holder) {
m_canvasWidth = a_width;
m_canvasHeight = a_height;
} // synchronized
}
/**
* @param a_vx
* The velocity of this fling measured in pixels per second along
* the x axis.
* @param a_vy
* The velocity of this fling measured in pixels per second along
* the y axis.
*/
public void addFling(float a_vx, float a_vy) {
int theta = (int) Math.toDegrees(Math.atan2(a_vy, a_vx));
if (theta < 0) {
theta += 360;
} // if
double hypot = Math.hypot(a_vx, a_vy);
Log.i(TAG, "fling: (" + Integer.toString(theta) + ", "
+ Double.toString(hypot) + ")");
if (theta < 45 || theta >= 315) {
synchronized (m_holder) {
m_game.moveBy(1, 0);
} // synchronized
return;
} // if
if (theta >= 45 && theta < 135) {
synchronized (m_holder) {
m_game.drop();
} // synchronized
return;
} // if
if (theta >= 135 && theta < 225) {
synchronized (m_holder) {
m_game.moveBy(-1, 0);
} // synchronized
return;
} // if
if (theta >= 225 && theta < 315) {
synchronized (m_holder) {
m_game.rotate();
} // synchronized
return;
} // if
}
}
----------->8---------------
Game.java
package org.doubletype.gtetrix;
import android.graphics.Canvas;
import android.graphics.Point;
public class Game {
public static final int WIDTH = 9;
public static final int HEIGHT = 20;
private interface Transformable {
void transform();
}
enum GameMode {
NEW,
ACTIVE,
GAME_OVER
}
private GameMode m_mode;
private Board m_board;
private Board m_mini;
private Block m_currentBlock;
private Block m_nextBlock;
public Game() {
m_mode = GameMode.NEW;
m_board = new Board(new Point(WIDTH, HEIGHT), new Point(10, 10));
m_mini = new Board(new Point(5, 5), new Point(200, 10));
loadNextBlock();
loadNewBlock();
}
public void draw(Canvas a_canvas) {
m_board.draw(a_canvas);
m_board.drawBlock(a_canvas, m_currentBlock);
m_mini.draw(a_canvas);
}
public boolean loadNextBlock() {
m_nextBlock = new Block();
m_nextBlock.moveTo(m_mini.getSize().x / 2,
m_mini.getSize().y - 3);
m_mini.clear();
if (!m_mini.isBlockInBoundary(m_nextBlock)
|| !m_mini.isBlockCollide(m_nextBlock)) {
return false;
} // if
return m_mini.load(m_nextBlock);
}
public boolean loadNewBlock() {
m_currentBlock = m_nextBlock;
loadNextBlock();
m_currentBlock.moveTo(m_board.getSize().x / 2,
m_board.getSize().y - 3);
if (!m_board.isBlockInBoundary(m_currentBlock)
|| !m_board.isBlockCollide(m_currentBlock)) {
m_mode = GameMode.GAME_OVER;
return false;
} // if
return m_board.load(m_currentBlock);
}
public boolean moveBy(final int a_x, final int a_y) {
if (m_mode == GameMode.NEW) {
m_mode = GameMode.ACTIVE;
} // if
return transform(new Transformable() {
@Override
public void transform() {
m_currentBlock.moveBy(a_x, a_y);
}
});
}
public boolean rotate() {
return transform(new Transformable() {
@Override
public void transform() {
m_currentBlock.rotate(Math.PI / 2.0);
}
});
}
private boolean transform(Transformable a_tansformable) {
if (m_mode != GameMode.ACTIVE) {
return false;
} // if
boolean retval = true;
Block temp = new Block(m_currentBlock);
m_board.unload(m_currentBlock);
a_tansformable.transform();
if (!m_board.isBlockInBoundary(m_currentBlock)
|| !m_board.isBlockCollide(m_currentBlock)) {
m_currentBlock = temp;
retval = false;
} // if
m_board.load(m_currentBlock);
return retval;
}
public boolean tick() {
if (m_mode != GameMode.ACTIVE) {
return false;
} // if
if (moveBy(0, -1)) {
return true;
} // if
m_board.checkRows();
return loadNewBlock();
}
public boolean drop() {
while (moveBy(0, -1)) {
} // while
return false;
}
}
----------->8---------------
Board.java
package org.doubletype.gtetrix;
import java.util.ArrayList;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Paint.Style;
import android.util.Log;
public class Board {
public static final int CELL_WIDTH = 18;
public static final int CELL_PADDING = 2;
private static String TAG = "Board";
private static Paint s_framePaint = null;
private static Paint s_blockPaint = null;
private static Paint s_redPaint = null;
private static Paint getFramePaint() {
if (s_framePaint == null) {
s_framePaint = new Paint();
s_framePaint.setAntiAlias(true);
s_framePaint.setARGB(255, 100, 100, 100);
s_framePaint.setStyle(Style.STROKE);
} // if
return s_framePaint;
}
private static Paint getBlockPaint() {
if (s_blockPaint == null) {
s_blockPaint = new Paint();
s_blockPaint.setAntiAlias(true);
s_blockPaint.setARGB(255, 100, 100, 100);
} // if
return s_blockPaint;
}
private static Paint getRedPaint() {
if (s_redPaint == null) {
s_redPaint = new Paint();
s_redPaint.setAntiAlias(true);
s_redPaint.setARGB(255, 200, 100, 100);
} // if
return s_redPaint;
}
private Point m_location;
private Point m_size;
private int m_cellCount;
private ArrayList<Rect> m_rectangles;
private BlockType [] m_cells;
private Rect m_frame;
public Board(Point a_size, Point a_location) {
m_size = a_size;
m_location = a_location;
m_cellCount = a_size.x * a_size.y;
initRects();
m_cells = new BlockType[m_cellCount];
clear();
}
/**
* initializes m_rectangles.
*/
private void initRects() {
m_rectangles = new ArrayList<Rect>();
for (int y = 0; y < m_size.y; y++) {
for (int x = 0; x < m_size.x; x++) {
Point coord = toCoord(x, y);
m_rectangles.add(new Rect(coord.x, coord.y,
coord.x + CELL_WIDTH, coord.y + CELL_WIDTH));
} // for x
} // for y
Point coord = toCoord(m_size.x, -1);
m_frame = new Rect(m_location.x, m_location.y,
coord.x, coord.y);
}
private Point toCoord(int a_x, int a_y) {
return new Point(a_x * (CELL_WIDTH + CELL_PADDING) + CELL_PADDING + m_location.x,
(m_size.y - a_y - 1) * (CELL_WIDTH + CELL_PADDING) + CELL_PADDING + m_location.y);
}
private boolean isCellEmpty(int a_i) {
return m_cells[a_i] == BlockType.EMPTY;
}
private boolean isCellEmpty(Point a_pos) {
return getCell(a_pos) == BlockType.EMPTY;
}
private boolean isCellEmpty(int a_x, int a_y) {
return m_cells[a_y * m_size.x + a_x] == BlockType.EMPTY;
}
private BlockType getCell(Point a_pos) {
return m_cells[a_pos.y * m_size.x + a_pos.x];
}
private boolean isCellInBoundary(Point a_pos) {
return (a_pos.x >= 0
&& a_pos.x < m_size.x
&& a_pos.y >= 0
&& a_pos.y < m_size.y);
}
private void setCell(Point a_pos, BlockType a_value) {
m_cells[a_pos.y * m_size.x + a_pos.x] = a_value;
}
public void clear() {
for (int i = 0; i < m_cellCount; i++) {
m_cells[i] = BlockType.EMPTY;
} // for i
}
public void draw(Canvas a_canvas) {
for (int i = 0; i < m_cellCount; i++) {
if (!isCellEmpty(i)) {
a_canvas.drawRect(m_rectangles.get(i), getBlockPaint());
} // if
} // for i
a_canvas.drawRect(m_frame, getFramePaint());
}
public void drawBlock(Canvas a_canvas, Block a_block) {
for (Point cell: a_block.getCells()) {
a_canvas.drawRect(m_rectangles.get(cell.y * m_size.x + cell.x), getRedPaint());
} // for cell
}
public boolean load(Block a_block) {
for (Point cell: a_block.getCells()) {
if (!isCellEmpty(cell)) {
return false;
} // if
setCell(cell, a_block.getType());
} // for cell
return true;
}
public boolean unload(Block a_block) {
for (Point cell: a_block.getCells()) {
setCell(cell, BlockType.EMPTY);
} // for cell
return true;
}
public boolean isBlockInBoundary(Block a_block) {
for (Point cell: a_block.getCells()) {
if (!isCellInBoundary(cell)) {
return false;
} // if
} // for cell
return true;
}
public boolean isBlockCollide(Block a_block) {
for (Point cell: a_block.getCells()) {
if (!isCellEmpty(cell)) {
return false;
} // if
} // for cell
return true;
}
public void checkRows() {
for (int y = m_size.y - 1; y >= 0; y--) {
if (isRowFilled(y)) {
removeRow(y);
} // if
} // for y
}
private boolean isRowFilled(int a_y) {
for (int x = 0; x < m_size.x; x++) {
if (isCellEmpty(x, a_y)) {
return false;
} // if
} // for x
return true;
}
private void removeRow(int a_y) {
Log.i(TAG, "About to remove row " + Integer.toString(a_y));
for (int i = a_y * m_size.x; i < m_cellCount - m_size.x; i++) {
m_cells[i] = m_cells[i + m_size.x];
} // for i
for (int i = m_cellCount - m_size.x; i < m_cellCount; i++) {
m_cells[i] = BlockType.EMPTY;
} // for i
}
public Point getSize() {
return m_size;
}
}
----------->8---------------
Block.java
package org.doubletype.gtetrix;
import android.graphics.Point;
import android.graphics.PointF;
public class Block {
public static int NUM_OF_CELLS = 4;
private PointF m_pos;
private BlockType m_type;
private PointF [] m_locals;
private Point [] m_cells;
public Block() {
m_pos = new PointF();
m_pos.x = 0;
m_pos.y = 0;
m_type = BlockType.random();
initPoints();
update();
}
public Block(Block a_block) {
m_pos = new PointF();
m_pos.set(a_block.m_pos);
m_type = a_block.m_type;
m_locals = new PointF[NUM_OF_CELLS];
m_cells = new Point[NUM_OF_CELLS];
for (int i = 0; i < NUM_OF_CELLS; i++) {
m_locals[i] = new PointF();
m_locals[i].set(a_block.m_locals[i]);
m_cells[i] = new Point(a_block.m_cells[i]);
} // for
}
private void initPoints() {
m_locals = new PointF[NUM_OF_CELLS];
m_cells = new Point[NUM_OF_CELLS];
for (int i = 0; i < NUM_OF_CELLS; i++) {
m_locals[i] = new PointF();
m_locals[i].set(m_type.getPoint(i));
m_cells[i] = new Point(0, 0);
} // for i
}
private void update() {
for (int i = 0; i < NUM_OF_CELLS; i++) {
m_cells[i].x = Math.round(m_locals[i].x + m_pos.x);
m_cells[i].y = Math.round(m_locals[i].y + m_pos.y);
} // for
}
public BlockType getType() {
return m_type;
}
public Point [] getCells() {
return m_cells;
}
public void moveBy(int a_x, int a_y) {
m_pos.x += a_x;
m_pos.y += a_y;
update();
}
public void moveTo(int a_x, int a_y) {
m_pos.x = a_x;
m_pos.y = a_y;
update();
}
public void rotate(double a_theta) {
for (PointF local: m_locals) {
float x = local.x;
float y = local.y;
local.x = (float) (x * Math.cos(a_theta) - y * Math.sin(a_theta));
local.y = (float) (x * Math.sin(a_theta) + y * Math.cos(a_theta));
} // local
update();
}
}
----------->8---------------
BlockType.java
package org.doubletype.gtetrix;
import java.util.Random;
import android.graphics.PointF;
public enum BlockType {
T(0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f),
X(-0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f),
L(0.0f, 0.0f, 0.0f, 1.0f, 0.0f, -1.0f, 1.0f, -1.0f),
J(0.0f, 0.0f, 0.0f, 1.0f, 0.0f, -1.0f, -1.0f, -1.0f),
B(0.0f, -1.5f, 0.0f, -0.5f, 0.0f, 0.5f, 0.0f, 1.5f),
S(-0.5f, 0.0f, 0.5f, 0.0f, -0.5f, 1.0f, 0.5f, -1.0f),
Z(-0.5f, 0.0f, 0.5f, 0.0f, -0.5f, -1.0f, 0.5f, 1.0f),
EMPTY(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
private static int NUM_OF_BLOCKTYPES = 7;
private static Random s_rand = new Random();
public static BlockType random() {
return valueOf(s_rand.nextInt(NUM_OF_BLOCKTYPES));
}
public static BlockType valueOf(int a_value) {
switch (a_value) {
case 0: return T;
case 1: return X;
case 2: return L;
case 3: return J;
case 4: return B;
case 5: return S;
case 6: return Z;
} // switch
return EMPTY;
}
private PointF [] m_points;
private BlockType(float a_x0, float a_y0,
float a_x1, float a_y1,
float a_x2, float a_y2,
float a_x3, float a_y3) {
m_points = new PointF[Block.NUM_OF_CELLS];
m_points[0] = new PointF(a_x0, a_y0);
m_points[1] = new PointF(a_x1, a_y1);
m_points[2] = new PointF(a_x2, a_y2);
m_points[3] = new PointF(a_x3, a_y3);
}
public PointF getPoint(int a_i) {
return m_points[a_i];
}
}
----------->8---------------
Writing Tetris using Google Android (アンドロイドでテトリスを書く) More ログイン