是男人就下100层(简仿)
2016-03-08 13:52
423 查看
近来,事情不多,闲暇之时,就想写个简单的游戏练练手。太复杂了,不使用游戏引擎来做,是非常困难的。这里,其实也没有打算说,开发一款游戏上线,就是练习如何自定义一个View。我想啊想,终于想到一个简单的游戏,那就是类似是男人就下100层的游戏。我也不需要找素材,我就直接绘制简答的物体就好了,重点在于自定义视图和绘制嘛。请原谅我还不会录制漂亮的gif图,希望有人能在留言中教我,下面是我的游戏效果:
这里,我并没有使用重力传感器来做控制,为什么呢?首先,我这里呢,就是演示一下自定义view,其次,重力感应并不是难点,我的自定义视图里,已经有左右移动的接口,加上重力感应,是分分钟的事情。但是最重要的原因,还是我太懒了!O(∩_∩)O哈哈~
开始说说这个游戏的编写过程。这个游戏,我们先从感官上分析,需要绘制的内容:1.小人;2.逐渐上升的障碍物;3.得分版;4.菜单。然后,我们需要想到,我需要一个视图,来绘制这些内容,但是,我还要不同的类,来负责绘制不同的内容,比如,小人的类负责绘制小人,得分版负责绘制得分版,自定义视图,主要控制游戏逻辑。这样,我们拆开,一步一步实现。
我的实现过程是这样的,我先谢了游戏自定义视图的逻辑,然后,写了障碍物绘制的类,我先成功绘制了会上升的障碍物,然后开始写绘制小人的类, 然后,让小人可以自由下落;接着,我又写了碰撞检测,让小球跟随障碍物上升;最后再调试成功。这是我的实现思路。
下面,按照我的风格,让大家从感官上了解主要代码结构:
这里主要重写了onDraw()方法,内容都是通过canvas绘制的。然后注意的是,获取视图的宽和高是在onSizeChanged()方法中进行的,因为这个方法是在onMeasure()后回调的,我们能够通过代码中的方式获取视图的宽和高。然后就初始化一些跟视图宽高有关的变量。
在看源码时,我们从init()方法开始看,init()方法中调用了startGame()方法:
后面,因为我不打算写重力传感器的代码,所以我放了两个按钮在xml布局中,为了能够更好的控制,写了一个按住连续点击的效果。
这个类写的是有瑕疵的,但是,使用起来非常方便,只需要在setOnTouchListener中,使用这个类替代元代的ontouchListener,如:
附:源码
这里,我并没有使用重力传感器来做控制,为什么呢?首先,我这里呢,就是演示一下自定义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妇女节,情有可原!
附:源码
相关文章推荐
- C#实现客户端弹出消息框封装类实例
- Delphi richedit获取选中文字
- 刷爆Github小绿点
- java Https post发送报文
- JNI分线程在Android上绘图刷新过快容易崩溃的处理
- C++函数重定义、重载、重写
- PostgreSQL 安装、配置、入门
- 【深度】想成为VR开发者前先问自己六个问题
- openSUSE集成工具yast使用方法图解
- iOS如何用代码控制以不同屏幕方向打开新页面?
- mongodb学习(二)
- iOS-监听键盘输入,视图整体上移或恢复-避免输入遮挡
- springMVC 常用注解
- dplyr高效数据处理包
- 方法重写
- Android 动态加载 (四)
- 在已经有xml文件的情况下快速生成javabean
- Spring MVC常用的注解
- Activity初步分析
- Tomcat内存溢出解决办法