第 2 回 シューティングゲームの作成

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

2-1. プログラム

src

MainActivity.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.graphics.Point;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Display;
public class MainActivity extends ActionBarActivity {
    private View view;
    private Thread mainThread;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
    @Override
    public void onStart(){
        super.onStart();
        Display display = getWindowManager().getDefaultDisplay();
        Point p = new Point();
        display.getSize(p);
        view = new View(this, p);
        view.init();
        setContentView(view);
    }
    @Override
    public void onResume(){
        super.onResume();
        view.start();
    }
    @Override
    public void onStop(){
        super.onStop();
        while (mainThread != null && mainThread.isAlive()) {
            try {
                view.shutdown = true;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
}

View.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Point;
import android.view.MotionEvent;
import android.view.SurfaceView;
import jp.ac.dendai.c.jtp.shootingsample.mono.Haikei;
import jp.ac.dendai.c.jtp.shootingsample.mono.Anata;
import jp.ac.dendai.c.jtp.shootingsample.mono.Mikata;
import jp.ac.dendai.c.jtp.shootingsample.mono.Mono;
import jp.ac.dendai.c.jtp.shootingsample.mono.Shootable;
public class View extends SurfaceView {
    final static long tic = 10;
    public volatile boolean shutdown;
    private int width;
    private int height;
    private DrawList drawList;
    private Mikata mikata;
    private HanteiList<Mono> tekiList;
    private HanteiList<Shootable> tamaList;
    private Context context;
    private DrawThread drawThread;
    private MoveThread moveThread;
    private Score score;
    private TekiLogic tekiLogic;
    private final Object lock;
    public View(Context context, Point p) {
        super(context);
        this.context = context;
        width = p.x;
        height = p.y;
        lock = new Object();
    }
    public void init() {
        drawList = new DrawList();
        score = new Score();
        drawList.addScore(score);
        drawList.add(new Haikei(context));

        tamaList = new HanteiList<>();
        mikata = new Anata(context, tamaList);
        mikata.set(width / 2, height * 3 / 4);
        drawList.add(mikata);

        tekiList = new HanteiList<>();
        tekiLogic = new TekiLogic(context, tekiList);

        drawList.addList(tekiList);
        drawList.addList(tamaList);

        destroyThread(drawThread);
        destroyThread(moveThread);
        drawThread = new DrawThread();
        moveThread = new MoveThread();
    }
    public void start(){
        shutdown = false;
        drawThread.start();
        moveThread.start();
    }
    private void destroyThread(Thread t) {
        if (t != null) {
            shutdown = true;
            while (t.isAlive()) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
            }
        }
    }
    private void draw() {
        synchronized (lock) {
            Canvas canvas = getHolder().lockCanvas();
            if (canvas == null) return;
            drawList.draw(canvas);
            getHolder().unlockCanvasAndPost(canvas); // 描画を終了
        }
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mikata.setDirection(event, width, height);
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                mikata.stop();
                if (shutdown) {
                    init();
                    start();
                }
                performClick();
                break;
        }
        return true;
    }
    @Override
    public boolean performClick() {
        super.performClick();
        return true;
    }
    class MoveThread extends Thread {
        @Override
        public void run() {
            double previous = (double) System.currentTimeMillis();
            double now;
            while (!shutdown) {
                Debug.append("tamasize", "" + tamaList.size());
                synchronized (lock) {
                    now = System.currentTimeMillis();
                    double tstep = (now - previous) / tic;
                    //    Debug.append("tstep",""+tstep);
                    drawList.step(tstep, width, height);
                    tekiLogic.step(tstep, width, height);
                }
                previous = now;

                for (Shootable s : tamaList) {
                    Mono m = tekiList.atari(s.getRect());
                    if (m != null) {
                        score.add(m.getScore());
                        s.dead();
                        m.dead();
                    }
                }
                synchronized (lock) {
                    drawList.update();
                }
                if (tekiList.atari(mikata.getRect()) != null) {
                    drawList.stop();
                    shutdown = true;
                    break;
                }
                try {
                    sleep((long) tic);
                } catch (InterruptedException e) {
                    shutdown = true;
                }
            }
        }
    }
    class DrawThread extends Thread {
        @Override
        public void run() {
            while (!shutdown) {
                draw();
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    shutdown = true;
                }
            }
        }
    }
}

Debug.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import java.util.HashMap;
import java.util.Map;
public class Debug {
    private static boolean mode = false;
    private static Map<String, String> pr;
    private static Paint paint = new Paint();
    private Debug() {
        init();
    }
    public static void draw(Canvas canvas) {
        if (mode) return;
        if (pr == null || canvas == null) return;
        paint.setColor(Color.WHITE);
        canvas.drawText(pr.toString(), 0, 100, paint);
    }
    public static void init() {
        pr = new HashMap<>();
    }

    public static void append(String key, String value) {
        if (pr == null) init();
        pr.put(key, value);
    }
}

DrawList.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.graphics.Canvas;
import android.graphics.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import jp.ac.dendai.c.jtp.shootingsample.mono.Mono;
public class DrawList extends ArrayList<Mono> {
    private static final long serialVersionUID = 6330699380650402372L;
    private Score score;
    private List<HanteiList<? extends Mono>> list;
    public DrawList() {
        super();
        list = new ArrayList<>();
    }
    public void addScore(Score s) {
        score = s;
    }
    public void addList(HanteiList<? extends Mono> list) {
        this.list.add(list);
    }
    public void draw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        score.draw(canvas);
        for (Mono m : this) {
            m.draw(canvas);
        }
        for (HanteiList<? extends Mono> l : list) {
            for (Mono m : l) {
                m.draw(canvas);
            }
        }
        Debug.draw(canvas);
    }
    public void step(double t, int width, int height) {
        for (Mono m : this) {
            m.step(t, width, height);
        }
        for (HanteiList<? extends Mono> l : list) {
            for (Mono m : l) {
                m.step(t, width, height);
            }
        }
    }
    public void stop() {
        for (Mono m : this) {
            m.stop();
        }
        for (HanteiList<? extends Mono> l : list) {
            for (Mono m : l) {
                m.stop();
            }
        }
    }
    public void update() {
        Iterator<Mono> i = this.iterator();
        while (i.hasNext()) {
            if (!i.next().isAlive()) {
                i.remove();
            }
        }
        Iterator<HanteiList<? extends Mono>> j = list.iterator();
        while (j.hasNext()) {
            Iterator<? extends Mono> k = j.next().iterator();
            while (k.hasNext()) {
                if (!k.next().isAlive()) {
                    k.remove();
                }
            }
        }
    }
}

HanteiList.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.graphics.Rect;
import java.util.ArrayList;
import jp.ac.dendai.c.jtp.shootingsample.mono.Mono;
public class HanteiList<E extends Mono> extends ArrayList<E> {
    private static final long serialVersionUID = -3775499867397182898L;
    public HanteiList() {
        super();
    }
    public E atari(Rect r) {
        for (E m : this) {
            if (r.intersect(m.getRect())) return m;
        }
        return null;
    }
}

Score.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
public class Score {
    private int score;
    private Paint paint = new Paint();
    public Score() {
        paint.setColor(Color.WHITE);
    }
    public void decrease(int point) {
        score -= point;
    }
    public void draw(Canvas canvas) {
        String sc = "000000000" + score;
        canvas.drawText(sc.substring(sc.length() - 10), 0, paint.getTextSize(), paint);
    }
    public void add(int point) {
        score += point;
    }
}

TamaList.java


package jp.ac.dendai.c.jtp.shootingsample;
import java.util.ArrayList;
import jp.ac.dendai.c.jtp.shootingsample.mono.Mono;
public class TamaList extends ArrayList<Mono> {
    public TamaList() {
        super();
    }
}

TekiLogic.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.content.Context;
import jp.ac.dendai.c.jtp.shootingsample.mono.Mono;
import jp.ac.dendai.c.jtp.shootingsample.mono.Teki;
public class TekiLogic {
    private static double period = 1000;
    private final Context context;
    private final HanteiList<Mono> list;
    private double tic;
    public TekiLogic(Context context, HanteiList<Mono> list) {
        this.context = context;
        this.list = list;
        tic = 0;
        list.add(createTeki());
    }
    private Mono createTeki() {
        return new Teki(context, 200, 30);
    }
    public void step(double tstep, int width, int height) {
        tic += tstep;
        while (tic > period) {
            list.add(createTeki());
            tic -= period;
        }
    }
}

Vect.java


package jp.ac.dendai.c.jtp.shootingsample;
import android.graphics.Rect;
public class Vect {
    private double x;
    private double y;
    public Vect(double x, double y) {
        super();
        this.x = x;
        this.y = y;
    }
    public Vect() {
    }
    public void set(double x, double y) {
        this.x = x;
        this.y = y;
    }
    public void set(Vect v) {
        this.x = v.x;
        this.y = v.y;
    }
    public double getX() {
        return x;
    }
    public void setX(double x) {
        this.x = x;
    }
    public double getY() {
        return y;
    }
    public void setY(double y) {
        this.y = y;
    }
    public int getIntX() {
        return (int) x;
    }
    public int getIntY() {
        return (int) y;
    }
    public void add(Vect delta) {
        x += delta.x;
        y += delta.y;
    }
    public void add(double t, Vect delta) {
        x += delta.x * t;
        y += delta.y * t;
    }
    public void restrict(double c) {
        x = x < -c ? -c : x > c ? c : x;
        y = y < -c ? -c : y > c ? c : y;
    }
    public void reflectX() {
        x = -x;
    }
    public void reflectY() {
        y = -y;
    }
    public Vect front(Rect r) {
        Vect v = new Vect();
        v.set(this);
        v.setY(y - r.height());
        Debug.append("tamaheight", "" + r.height());
        return v;
    }
}

mono/Mono.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.graphics.Canvas;
import android.graphics.Rect;
public interface Mono {
    void set(int i, int j);
    int getScore();
    void draw(Canvas canvas);
    void stop();
    void move(int width, int height);
    void step(double t, int width, int height);
    Rect getRect();
    boolean intersect(Mono m);
    boolean isAlive();
    void dead();
    double getInterval();
    void setRect();
}

mono/Shootable.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import jp.ac.dendai.c.jtp.shootingsample.Vect;
public interface Shootable extends Mono {
    Shootable getInstance();
    void init(Vect p, Vect dp);
}

mono/Shooter.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import jp.ac.dendai.c.jtp.shootingsample.Vect;
public interface Shooter extends Mono {
    void shoot(Vect dp);
}

mono/Mikata.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.view.MotionEvent;
public interface Mikata extends Shooter {
    void setDirection(MotionEvent event, int width, int height);
}

mono/AbstractMono.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import jp.ac.dendai.c.jtp.shootingsample.Vect;
public abstract class AbstractMono implements Mono {
    protected final int width;
    protected final int height;
    protected Vect p;
    protected Vect dp;
    protected double period;
    protected Rect rect;
    protected boolean alive;
    private int cycle;
    private int clock;
    private Bitmap[] gazou;
    public AbstractMono(Context context, int[] ids) {
        p = new Vect();
        dp = new Vect();
        rect = new Rect();
        cycle = ids.length;
        gazou = new Bitmap[cycle];
        for (int i = 0; i < cycle; i++) {
            gazou[i] = BitmapFactory.decodeResource(context.getResources(), ids[i]);
        }
        width = gazou[0].getWidth();
        height = gazou[0].getHeight();
        clock = 0;
        period = 0;
        alive = true;
    }
    @Override
    public void set(int i, int j) {
        p.set(i, j);
    }
    @Override
    public int getScore() {
        return 0;
    }
    @Override
    public void draw(Canvas canvas) {
        int delta = (int) (period / getInterval());
        clock = (clock + delta) % cycle;
        period -= delta * getInterval();
        canvas.drawBitmap(gazou[clock], p.getIntX(), p.getIntY(), null);
    }
    @Override
    public double getInterval() {
        return 1;
    }
    @Override
    public void step(double t, int width, int height) {
        period += t;
        p.add(t, dp);
        move(width, height);
        setRect();
    }
    @Override
    public void stop() {
        dp.set(0, 0);
    }
    @Override
    public void setRect() {
        rect.set(p.getIntX(),
                p.getIntY(),
                p.getIntX() + width,
                p.getIntY() + height);
    }
    @Override
    public Rect getRect() {
        return rect;
    }
    @Override
    public boolean intersect(Mono m) {
        return rect.intersect(m.getRect());
    }
    @Override
    public boolean isAlive() {
        return alive;
    }
    @Override
    public void dead() {
        alive = false;
    }
}

mono/AbstractShootable.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.content.Context;
import jp.ac.dendai.c.jtp.shootingsample.Vect;
public abstract class AbstractShootable extends AbstractMono implements Shootable {
    public AbstractShootable(Context context, int[] ids) {
        super(context, ids);
    }
    @Override
    public void init(Vect p, Vect dp) {
        this.p.set(p.getX(), p.getY());
        this.dp.set(dp.getX(), dp.getY());
        setRect();
    }
}

mono/AbstractShooter.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.content.Context;
import jp.ac.dendai.c.jtp.shootingsample.HanteiList;
import jp.ac.dendai.c.jtp.shootingsample.Vect;
public abstract class AbstractShooter extends AbstractMono implements Shooter {
    private HanteiList<Shootable> list;
    private Shootable tama;
    public AbstractShooter(Context context, int[] ids, HanteiList<Shootable> list, Shootable tama) {
        super(context, ids);
        this.list = list;
        this.tama = tama;
        tama.setRect();
    }
    @Override
    public void shoot(Vect dp) {
        Shootable newtama = tama.getInstance();
        newtama.init(p.front(tama.getRect()), dp);
        list.add(newtama);
    }
}

mono/Haikei.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
        import jp.ac.dendai.c.jtp.shootingsample.R;
        import jp.ac.dendai.c.jtp.shootingsample.Vect;
        import android.content.Context;
        import android.graphics.Canvas;
public class Haikei extends AbstractMono {
    private static final int[] ids = {R.drawable.haikei};
    public Haikei(Context context){
        super(context,ids);
        p.set(0,12);
    }
    @Override
    public void move(int width, int height) {
    }
}

mono/Anata.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.content.Context;
import android.view.MotionEvent;
import jp.ac.dendai.c.jtp.shootingsample.Debug;
import jp.ac.dendai.c.jtp.shootingsample.HanteiList;
import jp.ac.dendai.c.jtp.shootingsample.R;
import jp.ac.dendai.c.jtp.shootingsample.Vect;
public class Anata extends AbstractShooter implements Mikata {
    private static final int[] ids = {R.drawable.anata};
    private final static double shootperiod = 500;
    private static final Vect tamadp = new Vect(0, -1);
    private double shoottic;
    public Anata(Context context, HanteiList<Shootable> tamalist) {
        super(context, ids, tamalist, new Tama(context));
        shoottic = 0;
    }
    @Override
    public void move(int width, int height) {
        if (p.getX() > width) p.setX(width);
        if (p.getX() < -this.width) p.setX(0);
    }
    @Override
    public void setDirection(MotionEvent event, int width, int height) {
        final double delta = 1;
        float px = event.getX();
        Debug.append("position", "" + width + " " + px);
        if (px < width / 2) {
            dp.setX(-delta);
        } else {
            dp.setX(delta);
        }
    }
    @Override
    public void step(double t, int width, int height) {
        super.step(t, width, height);
        shoottic += t;
        while (shoottic > shootperiod) {
            shoot(tamadp);
            shoottic -= shootperiod;
        }
    }
}

mono/Haikei.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.content.Context;
import jp.ac.dendai.c.jtp.shootingsample.R;
public class Haikei extends AbstractMono {
    private static final int[] ids = {R.drawable.haikei};
    public Haikei(Context context) {
        super(context, ids);
        p.set(0, 12);
    }
    @Override
    public void move(int width, int height) {
    }
}

mono/Tama.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.content.Context;
import jp.ac.dendai.c.jtp.shootingsample.R;
public class Tama extends AbstractShootable {
    private static final int[] ids = {R.drawable.tama1, R.drawable.tama2};
    private Context context;
    public Tama(Context context) {
        super(context, ids);
        this.context = context;
        dp.set(0, -1);
    }
    @Override
    public void move(int width, int height) {
        if (p.getX() > width) alive = false;
        else if (p.getX() < -this.width) alive = false;
        if (p.getY() > height) alive = false;
        else if (p.getY() < -this.height) alive = false;
    }
    @Override
    public Shootable getInstance() {
        return new Tama(context);
    }
    @Override
    public double getInterval() {
        return 10;
    }
}

mono/Teki.java


package jp.ac.dendai.c.jtp.shootingsample.mono;
import android.content.Context;
import jp.ac.dendai.c.jtp.shootingsample.R;
import jp.ac.dendai.c.jtp.shootingsample.Vect;
public class Teki extends AbstractMono {
    private static final int[] ids = {R.drawable.teki1, R.drawable.teki2};
    private int dpindex;
    private Vect[] dps = {new Vect(1, 1), new Vect(-1, 1)};
    private double dpcycle = 1000;
    private double dpcounter;
    public Teki(Context context) {
        super(context, ids);
    }
    public Teki(Context context, int x, int y) {
        super(context, ids);
        set(x, y);
        dp.set(dps[0]);
        dpindex = 0;
        dpcounter = 0;
    }
    @Override
    public void move(int width, int height) {
        if (p.getX() > width) p.setX(-this.width);
        else if (p.getX() < -this.width) p.setX(width);
        if (p.getY() > height) p.setY(-this.height);
        else if (p.getY() < -this.height) p.setY(height);
    }
    @Override
    public double getInterval() {
        return 23;
    }
    @Override
    public int getScore() {
        return 100;
    }
    @Override
    public void step(double t, int width, int height) {
        period += t;
        if (dpcounter + t > dpcycle) {
            p.add(dpcycle - dpcounter, dps[dpindex]);
            dpindex = (dpindex + 1) % dps.length;
            dpcounter = dpcounter + t - dpcycle;
            p.add(dpcounter, dps[dpindex]);
        } else {
            p.add(t, dps[dpindex]);
            dpcounter += t;
        }
        move(width, height);
        setRect();
    }
}

res

後述のファイルの他、res/drawable-hdpi フォルダ内に下記の画像ファイルを 入れる

  1. anata.png
  2. haikei.png
  3. tama1.png
  4. tama2.png
  5. teki1.png
  6. teki2.png

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科