成功的路上一点也不拥挤,因为坚持的人太少了。
---简书上看到的一句话
未来请假三天顺带加上十一回家结婚,不得不说真是太坑了,去年婚假还有10天,今年一下子缩水到了3天,只能赶着十一办事了。
最近还在看数据结构,打算用java实现一遍,所以没着急写读书笔记,不过前段时间看了一个简单的五子棋游戏,记录一下。
整体效果如下,整个功能在一个自定义View里面实现:
由于主activity比较简单直接列出来:
public class MainActivity extends Activity {
private static String TAG ="MainActivity111";
private FiveView fiveView;
private Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext=this;
fiveView = (FiveView) findViewById(R.id.five);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId()==R.id.action_settings){
fiveView.setrestart();
}
return true;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.lly.simple_five.FiveView
android:id="@+id/five"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.lly.simple_five.FiveView>
</RelativeLayout>
上面只有一点 android:layout_centerInParent=”true” 设置自定义view居中,这样看起来比较美观。
主要代码在FiveView中实现,
考虑五子棋游戏一共有几个步骤:
1、画棋盘
2、根据用户选择在指定位置画棋子
3、判定输赢
4、需要有个重新开始的菜单,当确定输赢后重新开始游戏。
1、画棋盘
棋盘需要手动画出来,所以这里自定义了一个view
public class FiveView extends View {
public FiveView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(0x88000000);
setBackgroundColor(0x44440000);
mBlackList = new ArrayList<Point>();
mWhileList = new ArrayList<Point>();
}
...
}
然后重新他的两个参数的构造方法,为什么要写两个构造参数的方法呢,这是因为这里只需要用自定义view的布局,不需要自定义属性。
在构造方法里面我们初始化了画笔,棋盘的背景,并实例化保存棋子的list。
这里设置了这个view的背景颜色,以方便看出这个view的位置
小知识:
一般情况下view有三个构造方法,其中带一个参数的构造方法是在activity中new一个控件时调用的。如TextView tv = new TextView(mContext);
在xml中使用不带自定义属性的自定义控件时会调用两个参数的构造方法,如本例。
在xml中使用带自定义属性的自定义控件时,会调用带三个参数的构造方法。
这里想想,我们前面定义的view宽和高都是占满了整个屏幕,所以在手机上看到的就是一个长方行的布局,但是一般我们在现实中看到的棋盘都是正方形的,这也很好实现,
自定义view里面的测量方法能很好的解决这个问题:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Width = MeasureSpec.getSize(widthMeasureSpec);
Hight = MeasureSpec.getSize(heightMeasureSpec);
int WidthMode = MeasureSpec.getMode(widthMeasureSpec);
Width = Math.min(Width, Hight);
int measuredWidth = MeasureSpec.makeMeasureSpec(Width, WidthMode);
setMeasuredDimension(measuredWidth, measuredWidth);
}
如上代码 我们只需要取出测量的宽高,然后以宽高中较小的值作为棋盘的宽高,这样不管是横屏或竖屏都能得到一个正方形的棋盘了,
小知识
view中的测试模式有三种也就是上面getMode取出来的值,分别对应
match_parent
wrap_content
xxxxxdp
规范的操作在自定义测量方法里面要对这三种模式分别去出来,这里比较简单就不做出来了。
如上我们得到了棋盘的宽高。
在画棋盘之前还需要考虑下,我们棋盘应该怎么画 ,画多少条线,
这里我们在棋盘的大小发生改变时初始化一些初始化棋盘的操作
...
private static final int MAX_LINE = 10;
...
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBroad = Width;
mLineWidth = mBroad * 1.0f / MAX_LINE;
mBlackPiece = BitmapFactory.decodeResource(getResources(),
R.drawable.stone_b1);
mWhilePiece = BitmapFactory.decodeResource(getResources(),
R.drawable.stone_w2);
int dstpoint = (int)(mLineWidth*roation);
mBlackPiece = Bitmap.createScaledBitmap(mBlackPiece, dstpoint, dstpoint, false);
mWhilePiece = Bitmap.createScaledBitmap(mWhilePiece, dstpoint, dstpoint, false);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
上面代码 ,我们定义了棋盘的宽高为测量时的宽高,
定义了两条线之间的宽度,即10平分整个区域
另外初始化了黑白棋子这个在画棋子的时候具体说明。
接着就需要去画棋盘了
protected void onDraw(Canvas canvas) {
DrawBroad(canvas);
DrawPiece(canvas);
checkGameOver();
}
private void DrawBroad(Canvas canvas) {
for (int i = 0; i < MAX_LINE; i++) {
float startX = mLineWidth / 2;
float stopX = mBroad - (mLineWidth / 2);
float startY = (float) ((0.5 + i) * mLineWidth);
float stopY = (float) ((0.5 + i) * mLineWidth);
canvas.drawLine(startX, startY, stopX, stopY, mPaint);
canvas.drawLine(startY, startX, stopY, stopX, mPaint);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
可以发现在上面代码中,调用DrawBroad 去画棋盘,分别横竖画了十条线,组成了棋盘,这里发现在横竖开始的时候都是从0.5*mLineWidth开始的,这是为了留出上面一点空隙,可以使棋子可以显示完整,这里用来一个for循环就完成了横竖线,归功于这是一个正方形,x,y坐标互换一下就可以实现画横竖线了。
到这里棋盘就已经画好了
2、画棋子
这个比较复杂 我们在分几步实现;
1)、首先要实例化两种颜色的棋子
2)、想想在现实中下五子棋的时候每人手边有一个盒子装着自己的棋子,这里我们要有我们的盒子来保存我们的棋子,
3)、画棋子是根据客户手指按下的位置进行画的,所以要实现onTouchEvent方法
4)、画棋子
1)、实例和两种颜色的棋子
这个其实上在上面初始化变量的时候就已经做过了。
private static final float roation = 3*1.0f/4;
mBlackPiece = BitmapFactory.decodeResource(getResources(),
R.drawable.stone_b1);
mWhilePiece = BitmapFactory.decodeResource(getResources(),
int dstpoint = (int)(mLineWidth*roation);
mBlackPiece = Bitmap.createScaledBitmap(mBlackPiece, dstpoint, dstpoint, false);
mWhilePiece = Bitmap.createScaledBitmap(mWhilePiece, dstpoint, dstpoint, false);
这里的一个小技巧就是让棋子占每格的3/4,这样不管我们传多大棋子图片都能正确的显示出来,并且只占3/4的格子。
2)、实现黑白棋的棋盒
其实我们在构造函数里面已经实例化了两个盒子
mBlackList = new ArrayList<Point>();
mWhileList = new ArrayList<Point>();
只不过这里还没有棋子,这里我们的棋盒里面的棋子其实都是下在棋盘上的棋子。
3)、用户下棋也就是触发onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
if(IsGameOver) return false;
int x = (int) event.getX();
int y = (int) event.getY();
Point p = getPoint(x,y);
if(event.getAction()==MotionEvent.ACTION_UP){
if(mWhileList.contains(p)||mBlackList.contains(p)){
return false;
}
if(isWhile){
mWhileList.add(p);
}else{
mBlackList.add(p);
}
invalidate();
isWhile=!isWhile;
}
return true;
}
private Point getPoint(int x, int y) {
return new Point((int)(x/mLineWidth),(int)(y/mLineWidth));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
这里当用户下棋时,获取所下的位置的坐标,这里有一个小技巧
在保存所下棋子的x,y坐标时我们让x,y分别除每个格子的大小,这样所获得的位置一定在棋盘横竖的交界点上,这个可以去体会一下,
然后当用户抬起手指时我们去判断下这个位置是不是已经有棋子了,有的话直接return false;什么都不做,没有的话,看下当前该谁下了,把这个棋子添加到对应的棋盒里,并通知刷新棋盘
4)、画棋子
最后就是把现在棋盒里面的棋子画到棋盘中
private void DrawPiece(Canvas canvas) {
for(int i=0,n=mWhileList.size();i<n;i++){
Point WhilePiece = mWhileList.get(i);
canvas.drawBitmap(mWhilePiece, (WhilePiece.x+(1-roation)/2)*mLineWidth,
(WhilePiece.y+(1-roation)/2)*mLineWidth, mPaint);
}
for(int i=0,n=mBlackList.size();i<n;i++){
Point BlackPiece = mBlackList.get(i);
canvas.drawBitmap(mBlackPiece, (BlackPiece.x+(1-roation)/2)*mLineWidth,
(BlackPiece.y+(1-roation)/2)*mLineWidth, null);
}
}
这步就很简单了 循环遍历棋盒里面的棋子并把它画出来,唯一要注意的就是棋子的位置,
(WhilePiece.x+(1-roation)/2)*mLineWidth,
(WhilePiece.y+(1-roation)/2)*mLineWidth
看下图:
以第一个点的x坐标为例:
因为开始的时候设定的棋盘开始的x坐标离我们View的左边距是0.5*mLineWidth,然后在初始化棋子的时候棋子的大小是3/4*mLineWidth,现在我们要计算棋子的左边距到view左边距的距离
所以棋子的x位置就应该是(0.5-3/4/2)*mLineWidth,第二个点就是(1.5-3/4/2)*mLineWidht,其中0.5,1.5是我们下子的位置系数即WhilePiece.x,所以最后提取出来就是(WhilePiece.x+(1-roation)/2)*mLineWidth,,竖坐标也是同意道理,这个要好好理解一下
3、判定输赢
首先要确定在哪里判断是否游戏一方胜利,可以看出在画棋子的时候是最好的时机了 当游戏结束后就不再画棋子。
private void checkGameOver() {
boolean Whilewin = checkWhileFive(mWhileList);
boolean Blackwin = checkWhileFive(mBlackList);
if(Whilewin){
IsGameOver=true;
Toast.makeText(getContext(), "白旗胜利", Toast.LENGTH_SHORT).show();
}
if(Blackwin){
IsGameOver=true;
Toast.makeText(getContext(), "黑棋胜利", Toast.LENGTH_SHORT).show();
}
}
private boolean checkWhileFive(List<Point> points) {
for(Point p:points){
int x = p.x;
int y = p.y;
boolean Horizewin =checkHorizefive(x,y,points);
boolean verwin =checkverfive(x,y,points);
boolean leftwin =checkleftfive(x,y,points);
boolean reghitwin =checkreghitfive(x,y,points);
if(Horizewin||verwin||leftwin||reghitwin){
return true;
}
}
return false;
}
private boolean checkHorizefive(int x, int y, List<Point> points) {
int count=1;
for(int i=1;i<FIVE_WIN;i++){
if(points.contains(new Point(x+i,y))){
count++;
}else{
break;
}
}
if(count ==FIVE_WIN) return true;
for(int i=1;i<FIVE_WIN;i++){
if(points.contains(new Point(x-i,y))){
count++;
}else{
break;
}
}
if(count ==FIVE_WIN) return true;
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
上面代码首先在ondraw中调用checkGameover方法检查游戏是否结束
检查的方法就是分别去判断白棋和黑球是否达到五子连珠的效果。
上面贴出来了横向五子连珠的判断,取出当前棋子循环检查他的左边是否有五个相同颜色的棋子,有就返回游戏结束,没有的话在去检查右边是否有五个相同颜色的棋子,有的话返回游戏结束,这样横竖,左斜,右斜都判断后就可以确定游戏是否结束,
结束的话在onTouchEvent方法中直接返回false表示我们不需要这个事件了。
到此这个简单的五子棋差不多就完成了。
4、添加重新开始菜单
为了使它更完善一点我们加入了 重新开始的菜单键,
这个应该没什么难度,主要就是按下重新开始的时候
修改一些变量的初始值
public void setrestart() {
IsGameOver=false;
mWhileList.clear();
mBlackList.clear();
invalidate();
}
最后的最后当app异常退出的时候,发现下了半天的棋子没有保存,因此这里加入
private String INSTANCE = "instaNce";
private String INSTANCE_GEMEOVER = "instance_gameover";
private String INSTANCE_WHILEARRAY = "instance_whilearray";
private String INSTANCE_BLACKARRAY = "instance_blackarray";
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
bundle.putBoolean(INSTANCE_GEMEOVER, IsGameOver);
bundle.putParcelableArrayList(INSTANCE_WHILEARRAY, mWhileList);
bundle.putParcelableArrayList(INSTANCE_BLACKARRAY, mBlackList);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if(state instanceof Bundle){
Bundle bundle = (Bundle) state;
IsGameOver = bundle.getBoolean(INSTANCE_GEMEOVER);
mWhileList = bundle.getParcelableArrayList(INSTANCE_WHILEARRAY);
mBlackList = bundle.getParcelableArrayList(INSTANCE_BLACKARRAY);
super.onRestoreInstanceState(bundle.getBundle(INSTANCE));
return;
}
super.onRestoreInstanceState(state);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
处理异常退出的情况。
GAME OVER。。。。