是男人就下100层(简仿)

       近 来,事情不多,闲暇之时,就想写个简单的游戏练练手。太复杂了,不使用游戏引擎来做,是非常困难的。这里,其实也没有打算说,开发一款游戏上线,就是练习如何自定义一个View。我想啊想,终于想到一个简单的游戏,那就是类似是男人就下100层的游戏。我也不需要找素材,我就直接绘制简答的物体就好了,重点在于自定义视图和绘制嘛。请原谅我还不会录制漂亮的gif图,希望有人能在留言中教我,下面是我的游戏效果:




        这里,我并没有使用重力传感器来做控制,为什么呢?首先,我这里呢,就是演示一下自定义view,其次,重力感应并不是难点,我的自定义视图里,已经有左右移动的接口,加上重力感应,是分分钟的事情。但是最重要的原因,还是我太懒了!O(∩_∩)O哈哈~

        开始说说这个游戏的编写过程。这个游戏,我们先从感官上分析,需要绘制的内容:1.小人;2.逐渐上升的障碍物;3.得分版;4.菜单。然后,我们需要想到,我需要一个视图,来绘制这些内容,但是,我还要不同的类,来负责绘制不同的内容,比如,小人的类负责绘制小人,得分版负责绘制得分版,自定义视图,主要控制游戏逻辑。这样,我们拆开,一步一步实现。
        我的实现过程是这样的,我先谢了游戏自定义视图的逻辑,然后,写了障碍物绘制的类,我先成功绘制了会上升的障碍物,然后开始写绘制小人的类, 然后,让小人可以自由下落;接着,我又写了碰撞检测,让小球跟随障碍物上升;最后再调试成功。这是我的实现思路。
下面,按照我的风格,让大家从感官上了解主要代码结构:
package com.mjc.mendown.view;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

import com.mjc.mendown.R;
import com.mjc.mendown.util.PositionUtil;

import java.util.ArrayList;

/**
 * Created by mjc on 2016/3/3.
 */
public class GameLayout extends View {

    //当前视图(GameLayout)的长和宽
    private int mLayoutWidth;
    private int mLayoutHeight;
    //辅助绘制障碍物的对象
    private Barrier mBarrier;
    //辅助绘制人物的对象
    private Person mPerson;
    //面板绘制的对象
    private Score mScore;

    private Paint mPaint;
    //小人的圆形半径
    private int radius = 50;
    //不断绘制的线程
    private Thread mThread;

    private MyHandler myHandler;
    private int mBarrierMoveSpeed = 8;
    //人物是否自动下落状态
    private boolean isAutoFall;
    //游戏是否正在运行
    private boolean isRunning;
    //人物左右移动的速度
    private int mPersonMoveSpeed = 20;
    //需要绘制的小人
    private Bitmap bitmap;

    //画面中障碍物的位置信息
    private ArrayList<Integer> mBarrierXs;
    private ArrayList<Integer> mBarrierYs;
    //障碍物起始和产生障碍的间隔
    private int mBarrierStartY = 500;
    private int mBarrierInterval = 500;
    //障碍物的高度
    private int mBarrierHeight = 60;
    //人物所站立的障碍在画面中的index
    private int mTouchIndex = -1;

    //当小人自动下落瞬间,开始计时,单位毫秒
    private float mFallTime = 0;

    //重力加速度
    public static final float G = 9.8f;

    //总得分
    private int mTotalScore;
    //份数版块的文字大小
    private int mTextSize = 16;

    //失败后,弹出的菜单,按钮的位置
    private RectF mRestartRectf;
    private RectF mQuiteRectf;
    //按钮的宽度和高度,这里我省事没有转化为DP,都是直接用px,所以可能会
    //产生适配上的问题。
    private int mButtonWidth = 300;
    private int mButtonHeight = 120;
    private int Padding = 20;

    public GameLayout(Context context) {
        super(context);
        init();
    }

    public GameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth(10);
        //读取本地的img图片
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.img);
        //默认开始自动下落
        isAutoFall = true;
        myHandler = new MyHandler();
        //用来记录画面中,每一个障碍物的x坐标
        mBarrierXs = new ArrayList<>();
        //和上面的x对应的每个障碍物的y坐标
        mBarrierYs = new ArrayList<>();
        //将文字大小转化成DP
        mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, getResources().getDisplayMetrics());
        //启动游戏
        isRunning = true;
        startGame();
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //当前方法,是在onMeasure调用之后,进行回调,所以直接getMeasureWidth等
        //获取当前视图的宽和高
        mLayoutWidth = getMeasuredWidth();
        mLayoutHeight = getMeasuredHeight();
        //根据视图宽高,初始化障碍物的信息
        mBarrier = new Barrier(mLayoutWidth, mPaint);
        mBarrier.setHeight(mBarrierHeight);
        //创建人物绘制类对象
        mPerson = new Person(mPaint, radius, bitmap);
        mPerson.mPersonY = 300;
        mPerson.mPersonX = mLayoutWidth / 2;
        //初始化分数绘制对象
        mScore = new Score(mPaint);
        mScore.x = mLayoutWidth / 2 - mScore.panelWidth / 2;

        //菜单上重启按钮的左边坐标,mRestartRectf是重启按钮绘制区域
        int rX = mLayoutWidth / 2 - 20 - mButtonWidth;
        int rY = mLayoutHeight * 3 / 5;
        mRestartRectf = new RectF(rX, rY, rX + mButtonWidth, rY + mButtonHeight);
        //下面是菜单上退出按钮的区域
        int qX = mLayoutWidth / 2 + 20;
        int qY = mLayoutHeight * 3 / 5;
        mQuiteRectf = new RectF(qX, qY, qX + mButtonWidth, qY + mButtonHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制分数面板
        generateScore(canvas);
        //绘制障碍物
        generateBarrier(canvas);
        //如果小人正在下落,才检测是否碰撞
        if (isAutoFall)
            checkTouch();
        //根据是否下落,绘制小人的位置
        generatePerson(canvas);
        //如果没有结束,说明就是在运行
        //检查小人是否超出边界,判断游戏是否结束
        isRunning = !checkIsGameOver();
        //如果游戏结束
        if (!isRunning) {
            //绘制面板
            drawPanel(canvas);
            //绘制游戏结束数字
            notifyGameOver(canvas);
            //绘制两个按钮
            drawButton(canvas, mRestartRectf, "重来", Color.parseColor("#ae999999"), Color.WHITE);
            drawButton(canvas, mQuiteRectf, "退出", Color.parseColor("#ae999999"), Color.WHITE);
        }
    }

    /**
     * 绘制结束弹出框的背景区域
     * @param canvas
     */
    private void drawPanel(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setColor(Color.parseColor("#8e333333"));
        canvas.drawRoundRect(new RectF(mRestartRectf.left - Padding * 2, mLayoutHeight * 2 / 5 - Padding, mQuiteRectf.right + Padding * 2, mQuiteRectf.bottom + Padding), Padding, Padding, mPaint);
    }

    /**
     * 绘制Game over文字
     * @param canvas
     */
    private void notifyGameOver(Canvas canvas) {
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setTextSize(mTextSize * 1.5f);
        mPaint.setColor(Color.parseColor("#cc0000"));
        mPaint.setFakeBoldText(false);
        canvas.drawText("Game over", mLayoutWidth / 2, mLayoutHeight / 2, mPaint);
    }

    //绘制菜单按钮,下面的操作使得文字能够居中显示
    private void drawButton(Canvas canvas, RectF rectF, String text, int strokeColor, int textColor) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(strokeColor);
        canvas.drawRoundRect(rectF, 10, 10, mPaint);
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(textColor);
        mPaint.setTextAlign(Paint.Align.CENTER);
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float textHeight = fontMetrics.bottom - fontMetrics.top;
        int y = (int) (rectF.top + textHeight / 2 + (rectF.bottom - rectF.top) / 2 - fontMetrics.bottom);
        canvas.drawText(text, rectF.left + mButtonWidth / 2, y, mPaint);

    }

    /**
     * 绘制分数面板
     * @param canvas
     */
    private void generateScore(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.parseColor("#666666"));
        mScore.drawPanel(canvas);
        mPaint.setColor(Color.WHITE);
        mPaint.setFakeBoldText(true);
        mPaint.setTextSize(mTextSize);
        mScore.drawScore(canvas, mTotalScore + "");
    }

    /**据初始位置,生成障碍物,难点
     * 1.绘制时,每一个障碍物间的距离是一致的
     * 2.绘制时,都是从第一个障碍物开始绘制
     * 3.循环绘制,并把障碍物的x,y位置,分别保存在数组中
     * 4.障碍物逐渐上升,当障碍物超出边界时,我们删除数组中保存的
     *      第一个位置的x,但是保持原有下面已经出现过得障碍物x的位置
     *      并在最后添加新的障碍物的位置;y位置,每次都重新生成,重新
     *      保存在数组中
     * */
    private void generateBarrier(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.DKGRAY);
        //每次都清楚Y坐标信息,因为后面会重新生成
        mBarrierYs.clear();
        //死循环,有条件退出
        for (int i = 0; ; ) {
            //i小于数组中的长度,那么取出原有的x位置信息,绘制旧障碍物;
            // 否则就随机生成新的坐标信息添加到数组中
            if (i < mBarrierXs.size()) {
                mBarrier.mPositionX = mBarrierXs.get(i);
            } else {
                mBarrier.mPositionX = PositionUtil.getRangeX(mLayoutWidth);
                mBarrierXs.add(mBarrier.mPositionX);
            }
            //障碍物的y坐标
            mBarrier.mPositionY = mBarrierStartY + mBarrierInterval * i;
            mBarrierYs.add(mBarrier.mPositionY);
            //绘制到视图外,则不再进行绘制,退出循环
            if (mBarrier.mPositionY > mLayoutHeight) {
                break;
            }
            mBarrier.drawBarrier(canvas);
            i++;
        }
    }


    private void generatePerson(Canvas canvas) {
        //如果小人在自动下落
        if (isAutoFall) {
            //自动下落绘制
//            mPerson.autoFallY();
            mFallTime += 20;
            //根据重力加速度计算小人下落的位置
            mPerson.mPersonY += mFallTime / 1000 * G;
            mPerson.draw(canvas);
        } else {
            // 获取被挡住的障碍位置
            Log.v("@time", mFallTime / 1000 + "");
            //小人被挡住,下落的时间重置
            mFallTime = 0;
            //mTouchIndex表示的是小人在视图中被阻挡的的障碍物的位置
            //如果是小于0,表示没有阻挡,
            if (mTouchIndex >= 0) {
                //设置小人被阻挡的位置,被进行绘制
                mPerson.mPersonY = mBarrierYs.get(mTouchIndex) - 2 * radius;
                mPerson.draw(canvas);
            }
        }
    }

    /**
     *碰撞检测
     */
    private void checkTouch() {
        for (int i = 0; i < mBarrierYs.size(); i++) {
            //碰撞检测
            if (isTouchBarrier(mBarrierXs.get(i), mBarrierYs.get(i))) {
                mTouchIndex = i;
                isAutoFall = false;
            }
        }
    }

    private boolean checkIsGameOver() {
        return mPerson.mPersonY < 0 || mPerson.mPersonY > mLayoutHeight - 2 * radius;
    }

    /**
     * 碰撞检测
     * @param x 障碍物x坐标
     * @param y 障碍物y坐标
     * @return
     */
    private boolean isTouchBarrier(int x, int y) {
        boolean res = false;
        int pY = mPerson.mPersonY + 2 * radius;
        //在瞬间刷新的时候,只要小人的位置和障碍的位置,差值在小人和障碍物的瞬间刷新的最大值就属于碰撞
        //比如:小人下落速度为a,障碍物上升速度为b,画面刷新时间为t,瞬间刷新,会有个最大差值,这个值就是
        //临界值
        if (Math.abs(pY - y) <= Math.abs(mBarrierMoveSpeed + Person.SPEED + mFallTime / 1000 * G)) {
            if (mPerson.mPersonX + 2 * radius >= x && mPerson.mPersonX <= x + mBarrier.getWidth()) {
                res = true;
            }
        }
        return res;
    }


    public void startGame() {
        mThread = new Thread() {
            @Override
            public void run() {
                super.run();
                while (isRunning) {
                    //开始让障碍往上面滚动,障碍物的绘制,是跟mBarrierStartY相关的
                    mBarrierStartY -= mBarrierMoveSpeed;
                    //当第一个障碍物开始消失
                    if (mBarrierStartY <= -mBarrierInterval - mBarrierHeight) {
                        mBarrierStartY = -mBarrierHeight;
                        //删除刚消失的障碍物坐标信息
                        if (mBarrierXs.size() > 0)
                            mBarrierXs.remove(0);
                        //得分++
                        mTotalScore++;
                        //小球碰撞位置--
                        mTouchIndex--;
                    }
                    //这里应该是可以直接用postInvalidate()
                    myHandler.sendEmptyMessage(0x1);
                    try {
                        //每20毫秒刷新一次界面
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        mThread.start();
    }

    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 0x1) {
                invalidate();
            }

        }
    }

    //控制小人向左移动
    public void moveLeft() {
        int x = mPerson.mPersonX;
        int dir = x - mPersonMoveSpeed;
        if (dir < 0)
            dir = 0;
        mPerson.mPersonX = dir;
        //移动过程中,启动边界检测,设置isAutoFall为true
        checkIsOutSide(dir);
        invalidate();
    }

    /**
     * 类似moveLeft
     */
    public void moveRight() {
        int x = mPerson.mPersonX;
        int dir = x + mPersonMoveSpeed;
        if (dir > mLayoutWidth - radius * 2)
            dir = mLayoutWidth - radius * 2;
        mPerson.mPersonX = dir;
        checkIsOutSide(dir);
        invalidate();
    }

    private void checkIsOutSide(int x) {
        isAutoFall = true;
    }

    public void stop() {
        isRunning = false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //游戏正在运行,没有生成菜单
                if (isRunning)
                    break;
                //获取触摸位置信息
                float x = event.getX();
                float y = event.getY();
                //如果触摸到重启游戏的按钮,触发
                if (mRestartRectf.contains(x, y)) {
                    restartGame();
                } else if (mQuiteRectf.contains(x, y)) {//触摸到退出按钮
                    Toast.makeText(getContext(), "退出到主菜单", Toast.LENGTH_SHORT).show();
                }
                break;

        }
        return super.onTouchEvent(event);
    }

    /**
     * 重置游戏信息
     */
    private void restartGame() {
        mBarrierXs.clear();
        mBarrierYs.clear();
        mBarrierStartY = 500;
        mPerson.mPersonY = 300;
        mPerson.mPersonX = mLayoutWidth / 2;
        mTotalScore = 0;
        isAutoFall = true;
        mFallTime = 0;
        isRunning = true;
        startGame();
    }
}
       代码中的注释,都是我后来加上的,我自己写的时候,是大多没有注释,但是为了大家看起来方便,我都会写上详细的注释。
       这里主要重写了onDraw()方法,内容都是通过canvas绘制的。然后注意的是,获取视图的宽和高是在onSizeChanged()方法中进行的,因为这个方法是在onMeasure()后回调的,我们能够通过代码中的方式获取视图的宽和高。然后就初始化一些跟视图宽高有关的变量。
       在看源码时,我们从init()方法开始看,init()方法中调用了startGame()方法:
    public void startGame() {
        mThread = new Thread() {
            @Override
            public void run() {
                super.run();
                while (isRunning) {
                    //开始让障碍往上面滚动,障碍物的绘制,是跟mBarrierStartY相关的
                    mBarrierStartY -= mBarrierMoveSpeed;
                    //当第一个障碍物开始消失
                    if (mBarrierStartY <= -mBarrierInterval - mBarrierHeight) {
                        mBarrierStartY = -mBarrierHeight;
                        //删除刚消失的障碍物坐标信息
                        if (mBarrierXs.size() > 0)
                            mBarrierXs.remove(0);
                        //得分++
                        mTotalScore++;
                        //小球碰撞位置--
                        mTouchIndex--;
                    }
                    //这里应该是可以直接用postInvalidate()
                    myHandler.sendEmptyMessage(0x1);
                    try {
                        //每20毫秒刷新一次界面
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        mThread.start();
    }
        这个方法,相当于源动力,不断循环,不断修改相关绘制参数,不断重绘界面;然后引起了onDraw()的方法的回调,然后继续看onDraw()中的代码,就是一些绘制的方法,绘制各种需要绘制的内容,这样,我们就完成了视图的绘制,类似一个游戏的控件就写完了。 其中一些负责绘制的类,都没有贴出来,但是他们的作用大同小异,基本都是通过参数,绘制相应的内容,这里就不多讲,看了源码很容易理解。

        后面,因为我不打算写重力传感器的代码,所以我放了两个按钮在xml布局中,为了能够更好的控制,写了一个按住连续点击的效果。
package com.mjc.mendown.util;

import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by mjc on 2016/3/3.
 * 功能:使用这个类替代OnTouchListener,能够获得连续点击的效果
 */
public abstract class OnContinueClickListener implements View.OnTouchListener {
    private boolean isContinue;
    private Thread mThread;
    //单例模式,只创建一个Handler
    private volatile MyHandler mHandler;
    //不同事件,传入不同的what值,因为不同当前对象中,都只有一个实例
    private int what;
    public final static int interval = 20;
    private View view;

    public OnContinueClickListener() {
        //必须在主线程中调用
        if (mThread == null)
            mHandler = new MyHandler();
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        this.view = v;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isContinue = true;
                mThread = new Thread() {
                    @Override
                    public void run() {
                        super.run();
                        while (isContinue) {
                            mHandler.sendEmptyMessage(what);
                            Log.v("@msg-what", what + "");
                            try {
                                Thread.sleep(interval);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                    }
                };
                mThread.start();
                break;
            case MotionEvent.ACTION_UP:
                isContinue = false;
                mThread = null;
                break;
            case MotionEvent.ACTION_CANCEL:
                isContinue = false;
                mThread = null;
                break;
        }

        return true;
    }


    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            handleClickEvent(view);
        }
    }

    public abstract void handleClickEvent(View view);


}

        这个类写的是有瑕疵的,但是,使用起来非常方便,只需要在setOnTouchListener中,使用这个类替代元代的ontouchListener,如:
        left.setOnTouchListener(new OnContinueClickListener() {
            @Override
            public void handleClickEvent(View view) {
                mGameLayout.moveLeft();
            }
        });


        right.setOnTouchListener(new OnContinueClickListener() {
            @Override
            public void handleClickEvent(View view) {
                mGameLayout.moveRight();
            }
        });
          我不太习惯在外面一个个讲,我喜欢在代码中,进行注释,我知道,仍然会有人不理解,不过,大家可以给我留言,我会尽量给大家解答的。 个人思维,难免产生问题,请大家不吝指教。最后附上源码,CSDN不知道啥情况,上传的资源无法访问,500的错误,今天3.8妇女节,情有可原!

附:源码




  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值