目录
《Andriod平台应用与开发技术实验》
实验报告
第一章引言 – 1 –
第二章调研阶段 – 2 –
2.1项目背景 – 2 –
2.2前期调研 – 2 –
2.3开发必要性 – 2 –
2.4预期功能实现 – 2 –
第三章设计阶段 – 2 –
3.1页面设计 – 2 –
3.2角色设计 – 2 –
3.3元素、定位设计 – 2 –
3.4原画设计 – 2 –
第四章开发阶段 – 3 –
4.1总体功能实现概述 – 3 –
4.2页面分布图 – 3 –
4.3各个页面的功能实现 – 3 –
第五章项目利弊 – 4 –
5.1项目特色 – 4 –
5.2项目中出现的问题 – 4 –
5.3废弃方案 – 4 –
第六章结语 – 4 –
6.1对项目的 – 4 –
6.2对数独爱好者的 – 4 –
6.3对开发过程中帮助我的人 – 4 –
- 引言及成果展示
作为今年初识数独并爱上数独的游戏爱好者,在贫瘠的Java技术和充满对如今市面上可见的数独游戏思考的艰难抉择中,选择了自己尝试进行数独开发。
2022年5月,我在百无聊赖的复习时,通过广告推广下载了一款名为“数独九宫格”的游戏。其实在此之前数独对我来说是一种困难而带有圣洁意味的益智游戏(有类似的印象只是因为艾伦·图灵当年酷爱在泰晤士报上做数独游戏),但当试着接触后发现,它与“困难”“枯燥”“只有喜欢数学的人才会喜欢它”这样的刻板印象相差甚远,甚至,我在不断挑战更多高难度数独、对角数独、杀手数独、异性数独等等游戏中找到了极大的乐趣与成就感。
这时才缓缓发现,一杯午后的热茶,一幅街景,一份被画的毛糙的报纸和一个铅笔头这样富有情调的画面,竟然也可以发生在我的身上。(这种画面感真的很难得!)
于是我在网络上查询了别人编写数独游戏的思路,在他们使用的Canvas方法的基础上学习了许多与安卓有关的课外知识,确认开发可行之后,开始了长达三周的Andoird&java开发旅程。
App截图如下:
(狠狠地把sudoku写错了,,,忘记改了之后也懒得改了,请不要介意)
- 调研阶段
2.1项目背景
数独(Sudoku),是最早起源于瑞士,传播于美国,兴盛于日本的填数字游戏。因它被发表在一本日本的游戏公司杂志上,起名为「数字は独身に限る」(只能填写一个数字),故改名为“数独”。
在中国,数独也在益智游戏中占有很大的市场空间,每年在中小学、大学生以及成人之间,都举办着规模不等的数独锦标赛。除此之外,教育机构、各大院校也有老师学生自发组织数独社团,以及数独玩家交流比试的活动。
只是随着网络手游的兴起,益智游戏受到了猛烈的冲击,更多新的游戏机制映入人们的眼帘,如风靡多年的“开心消消乐”、大家在广告里可能见过的“水排序”,b站主播喜欢挑战的“异性拼图”,以及最近火爆的“羊了个羊”。
色彩、设计以及新奇的机制垄断了玩家选择益智游戏的方式,数独也在20年前后缓缓退出人们的视野,数独业余段位考试也因人数等问题不再举行了,零零散散的数独爱好者们散落进莫大的都市中,如点点金砂一般。
如何让数独游戏重新走进人们的视野,如何让大家爱上数独,我们还有很长的路要走。
2.2前期调研
2.2.1调查目的
了解目前市面上的数独app热度,游戏机制和一些玩家偏好,并了解不同类型的玩家对待当前数独app的看法。
2.2.2调查对象
1.APP Store上国内外现有的数独软件(3个主流app,3个冷门app);
2.身边18-22岁年龄段的大学生、以及数独爱好者。
2.2.3调查时间
2022.10.3-2022.10.9
2.2.4调查方法
对app进行下载测试,对人群进行随机采访。
2.2.5调查内容
App调研:
- 题目范围(A.随机生成B.题库)
- 题目类型(A.普通数独为主B.混合型)
- 是否有广告(A.yes B.no)
- 怎样判断对错(A.逐字检验B.最后检查)
- 怎样填数(A.通过单选框选择B.通过点击下面的文字框选择)
- 以什么机制鼓励答题?
人群调研:
- 以前是否玩过数独游戏?
- 一个词概括你对数独游戏的印象(A.无聊B.有趣C.困难D.其他)
- 你认为阻碍你玩数独游戏的原因是什么?
(A.太难了,不会玩
B.太无聊,没意思
C.没有纸片人有意思
D.广告太多
E.其他)
2.3开发必要性
先从人群的调研结果讲起,数据非常有趣。
在随机采访中有100%的人玩过数独,但有将近60%的人认为数独无聊,并有40%的人认为数独困难。并且,在第三题“什么原因阻碍你玩数独游戏”的多选题中,100%的人都选择了“没有纸片人有意思”。
大部分人觉得因为数独的规则和玩法过于枯燥乏味,成就感少,并且没有绚丽的游戏画面,所以在难度逐级递增的过程中丧失了游玩的欲望,并感到无聊。
所以我认为应该在游戏玩法设计中加强横向创新,也就是在难度不变的基础上加入更多游戏模式,让大家更加了解到异性数独的趣味性。
同时,加强画面和故事设计,以及交互设计,让数独画面不再是枯燥的“报纸”,而是更加贴合当代游戏模式的故事推动主线、活动推动创新的游戏形式。
然后呢是对市面上的三款主流类数独游戏和三款媒体类数独游戏的调研。
主流类数独游戏,受众主要是数度爱好者,没有过度绚丽的画面,主要以方便填写、题库丰富为主,在此基础上,有一款游戏设立排行榜机制,三款都以玩法多样题库丰富为主。都没有广告,且都是在填完之后才设立检查功能。他们的鼓励活动相对较少,除了排行榜外,几乎只是一个设计简单的竞赛题库,没有定期大小活动等等。
媒体类数独游戏,主要应收依靠游戏推广,所以含有大量广告,受众主要是普通用户,主要以操作简单、操作台功能多样为主。他们的答题界面包含撤回、一键填数、提示等等功能,这些是主流类数独不会有的。但相应的,每个功能都以看广告为代价,题库以普通数独为主,多是由算法自动生成的,不附带竞赛题库。但媒体类数独含有定期活动,如“每日挑战”“挑战杀手数独”“季度活动”等等,在完成规定的题目后获得奖杯,算是在一定程度上提高了积极性。
总结来说,数独游戏的改进,应当将市面上两种主流游戏的特质结合在一起,如设立既包含多种多样的题库,又能够兼容算法生成的普通数独题目,以及有定期维护的活动等等。
这绝对是一个无法短期做完的大工程。
不过,这样的游戏模式是市场所需要的,也是数独爱好者所需要的。
2.4预期功能实现
功能(远期)
1.数独每日挑战,活动副本;
2.数独题库,分为简单,普通,困难三种;
3.计时器,在每场游戏中;
4.用户界面,有用户名、头像、账号、密码、平均排名、活动排名、成就;
5.成就板块,有登录天数成就,做题天数成就,常规排名成就,活动排名成就,做题时间成就等;
6.主题颜色的更换;
7.排行榜,分为题库内的关卡时间排行榜,活动排行榜和平均时长总榜;
8.设置,包括主题设置,音乐音效开关和更改,数独偏好设置等。
3.设计阶段
3.1页面设计
3.2角色设计
3.3元素、定位设计
3.4原画设计
关卡选择:
简单:
普通:
困难:
胜利画面:
主页面画面:
等待界面:
引导界面:
- 开发阶段
4.1总体功能实现概述
如设计阶段所规定的一样,主要分为五个主要页面:
·MainPart:游戏主界面,包含用户信息、荣誉、活动入口、普通关卡入口;
·Store:商店界面,售卖相关题库,以及活动产品;
·Setting:设置界面,玩家可以选择自己的偏好以及对游戏的建议看法;
·LevelChoose:关卡选择界面,选择普通关卡的难度以及level;
·LevelRead:关卡读取界面,也就是游戏界面。
4.2页面分布图
4.3各个页面的功能实现
4.2.1读条界面StartGame
游戏的加载页面。
但因为加载太快了,所以将进度条改为了SeekBar,拖动到底,出现按钮。
功能实现就是通过调整可见性即可。
4.2.2主界面MainPart
主界面包含有三个界面的跳转按钮和进入的关卡选择按钮。其实还应该有用户登录界面和活动关卡进入界面,但苦于事件还没有完成。
在还未敲出来的活动关卡中,将会开发异形数独和竞赛数独的关卡。
4.2.3设置界面Setting
我对设置界面的设计最初有很多设想,但是苦于开发周期和技术受限,目前只实现了音乐开关(switch)功能。这个UI很简单,为switch申请一个onCheckChangeListener然后做if判断控制开关即可。为了全局播放音乐,并且不重复调用音乐播放器,我将音乐功能放置到了一个独立的类——mediaIntance中,作为一个单例模式使用。关于音乐播放的事,请见4.2.7音乐单例。
4.2.4商店界面Store
还没有来得及做出来,我的设想是用做一个流式布局做一个商品展示页面,然后点击后生成dialog弹窗,上面点击按钮可以购买,进行外部链接的跳转,点击空白处取消。
4.2.5关卡读取界面LevelChoose
关卡选择界面,在这个界面中通过按钮点击,将数字利用putExtra传入LevelRead中。
数字是哪里来的呢?实际上并不是预先存储的,而是直接获取到按钮上的数字,然后强转类型传入进去的。
代码如下:
//在点击按钮时获取该按钮的text值public void onClickLevel(View view){Button button = (Button) view;String text = button.getText().toString();int level = Integer.valueOf(text);intent intent=new Intent(levelChoose.this,LevelRead.class);intent.putExtra("level",level);startActivity(intent);}
还有一个功能就是通过下面的单选框RadioGroup选择关卡难度,同时,下面的图片也会发生变化。
这个通过设置图片可见性也可以完美解决。
就不做过多解释了如下所示:
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {@Overridepublic void onCheckedChanged(RadioGroup radioGroup, int i) {if(i==R.id.easy_choose){easy.setVisibility(View.VISIBLE);normal.setVisibility(View.INVISIBLE);hard.setVisibility(View.INVISIBLE);}else if(i==R.id.normal_choose){easy.setVisibility(View.INVISIBLE);normal.setVisibility(View.VISIBLE);hard.setVisibility(View.INVISIBLE);}else{easy.setVisibility(View.INVISIBLE);normal.setVisibility(View.INVISIBLE);hard.setVisibility(View.VISIBLE);}}});
4.2.6游戏界面LevelRead
在这个界面中主要目的是得到LevelChoose界面获取到的关卡数,并传值到本活动的布局文件中用于读取关卡数据加载游戏地图。
因为之前LevelRead中已经通过强制转换关卡按钮text为int值将其存放在level中,所以只需要重载PlayGround中并允许它传进一个level值即可!
传入PlayGround后,我设定了两个String类型的变量,一个levelRead盛装关卡信息,一个levelAnswer盛装答案信息。
更细节的用法,还请我们在playground类中细说吧。
先看看代码:
LevelRead中如下:
Intent intent=getIntent();int level=intent.getIntExtra("level",1);setContentView(new PlayGround(this,level));
然后是LevelChoose中的方法是这样的(之前写过了,为了方便理解,再看看):
public void onClickLevel(View view){Button button = (Button) view;String text = button.getText().toString();int level = Integer.valueOf(text);Intent intent=new Intent(LevelChoose.this,LevelRead.class);intent.putExtra("level",level);startActivity(intent);}
再看看PlayGround方法此时:
public PlayGround(Context context,int level){super(context);levelMain=level;//哪个关卡levelRead=getLevelRead(levelMain);//读关卡信息levelAnswer=getLevelAnswer(levelMain);//读关卡答案}
4.2.6.1游戏场景playground
展示页面:
在我将PlayGround之前,烦请先移步4.2.6.2点阵,把那一章看完再回来!
在Dot中我定义了九阶矩阵中的每一个点的信息,看完更有助理解。
准备好了吗?
那我们开始讲解PlayGround类吧!
Playground是levelChoose活动的java布局文件,因为使用.xml文件的话,数据结构和数据的读写方面会存在很大限制,所以不如直接使用安卓工具用java写一个布局。
想法有了,那么开始做吧。
画布工具Canvas:
参考了许多安卓小游戏的做法,最终我学习了使用android.graphics.Canvas;中的Canvas来进行画布的绘制。
使用Canvas就涉及到了在View上绘制,要继承view或者view的子类surface。
然后就可以使用在paint、drawText、drawLine、drawRect方法在canvas上绘制了!
Paint是画笔工具,可以new一个画笔工具并设置相关属性,请看下面我绘制方格的画笔工具的例子。
Paint line =new Paint();Paint paint =new Paint();Paint lightLine=new Paint();//画布(背景)颜色c.drawColor(Color.LTGRAY);//设置line是空心画笔line.setStyle(Paint.Style.STROKE);line.setColor(0xFF000000);//黑色//对高亮线的设置lightLine.setStrokeWidth(10);lightLine.setStyle(Paint.Style.STROKE);lightLine.setColor(0xFF000000);
这里c是我对Canvas的定义。
然后再看看如何使用drawxxx,ctrl 点击看看它的方法,以下是我觉得比较好用的三个使用方法,之后绘制地图和绘制按钮时,我大多使用的都是这三个方法:
public void drawRect(float left, float top, float right, float bottom, @Nonnull Paint paint) {throw new RuntimeException("Stub!");}public void drawPosText(@NonNull String text, @NonNull float[] pos, @NonNull Paint paint) {throw new RuntimeException("Stub!");}public void drawPoint(float x, float y, @NonNull Paint paint) {throw new RuntimeException("Stub!");}
九阶矩阵matrix[][]:
想想这个矩阵是什么类型的。每一个点,我们都要能获取到它的x、y坐标,以便对它进行操作,同时能通过它的坐标对它对应的格子进行颜色的变化。所以,就使用我们自己写的Dot类型。
private Dot matrix[][];//九阶矩阵,存放棋盘格
就是这样!
但因为二维数组里的x、y坐标和我们dot中的x、y是反的,所以我还写了一个getDot方法,来直接通过x、y把dot和matrix联系在一起,看看如下代码:
//因为点的坐标和dot中储存的值一直是反的,所以可以用一个方法来把dot的值封装一下private Dot getDot(int x,int y){return matrix[y][x];}
一维数组sequence[]:
private boolean sequence[]=new boolean[9];//一维数组,存放选数区数字
因为打印的是1-9递增数列,也不需要改变数字,所以其实没什么必要设置int型的数组。我只需要知道当前哪一个被点击,所以设置一个boolean型的数组就行!
学会基础的绘制方法和数字的储存结构后,我们来考虑一下这个游戏的框架。
游戏加载:
当开始思考制作数独时,我们就要思考一下每次加载地图时的,加载各种元素的顺序。
先来梳理一下我们需要加载的元素:
·建立九阶矩阵并初始化
·绘制9×9方格
·读levelRead(关卡数据)
·读levelAnswer(关卡答案)
·绘制关卡数字
·绘制选字区
·……
看起来很多对吧!!!
先别急,我们来分类一下,什么是与绘制有关的,什么是与数据有关的。
与数据有关的:
·读levelRead(关卡数据)
·读levelAnswer(关卡答案)
·建立九阶矩阵并初始化
与绘制有关的:
·绘制9×9方格
·绘制关卡数字
·绘制选字区
·绘制检查按钮
OK!!!目前就这么多了,可以着手试着做了。
首先,先写与数据有关的刷新方法initGame():
//游戏初始化private void initGame(){//把所有点设为未点击的状态for (int i=0;i<ROW;i )for (int j=0; j<COL; j ){matrix[i][j].setClick_status(Dot.Click_status.Normal);//初始化,将所有的宫中的值设为null状态}//将下面的数字栏的全置为0,表示都是未选中状态for(int i=0;i<9;i ){sequence[i]=false;}}
为了方便起见,再设置两个单独的对矩阵和数组分别刷新的方法。
之后会用到:
//只重置选数字的这个框private void initSequence(){for(int i=0;i<9;i ){sequence[i]=false;}}//只重置数独的框private void initMatrix(){for (int i=0;i<ROW;i )for (int j=0; j<COL; j ){matrix[i][j].setClick_status(Dot.Click_status.Normal);//初始化,将所有的宫中的值设为null状态}}
做到这儿,初始化就完成一半了!!!完美!!!!
下一步是地图绘制,
redraw方法:
绘制的方法一共三部,定义画笔、设置属性、划线。
所以redraw方法基本就是在重复这三部,话不多说,直接看代码。
//界面绘制private void redraw() throws InterruptedException {//canvas是一个安卓中的绘图工具,以此创建画布来对界面操作Paint line =new Paint();Paint paint =new Paint();Paint lightLine=new Paint();//画布(背景)颜色c.drawColor(Color.LTGRAY);//设置line是空心画笔line.setStyle(Paint.Style.STROKE);line.setColor(0xFF000000);//黑色//对高亮线的设置lightLine.setStrokeWidth(10);lightLine.setStyle(Paint.Style.STROKE);lightLine.setColor(0xFF000000);//设置格子颜色for(int i=0;i<ROW;i )for(int j=0;j<COL;j ){Dot one=getDot(j,i);//Normal为没有操作下的格子,Click为点击到的格子,Related为Click格子的所在横纵宫的格子switch (one.getClick_status()){case Normal:{paint.setColor(0xFFFFFFFF);//填充颜色白色break;}case Click:{paint.setColor(0xFFCAE1FF);//填充颜色蓝色break;}case Related:{paint.setColor(0xFFF0F8FF);//填充颜色浅蓝色break;}default:break;}//RectF方法规定了每一个图形的摆放位置,在此按照预想的让其居中放置c.drawRect(new RectF(one.getX()*WIDTH WIDTH/2,one.getY()*WIDTH 3*WIDTH,(one.getX() 1)*WIDTH WIDTH/2,(one.getY() 1)*WIDTH 3*WIDTH), paint);c.drawRect(new RectF(one.getX()*WIDTH WIDTH/2,one.getY()*WIDTH 3*WIDTH,(one.getX() 1)*WIDTH WIDTH/2,(one.getY() 1)*WIDTH 3*WIDTH), line);}//给选字区的数字加上方块buttonSuddenlyCreated(c);//此时再绘制数字Paint normalTextPen=new Paint();normalTextPen.setTextSize((float) (WIDTH*0.9));normalTextPen.setStyle(Paint.Style.FILL);normalTextPen.setTextAlign(Paint.Align.CENTER);normalTextPen.setTypeface(Typeface.DEFAULT_BOLD);for(int i=0;i<9;i ){if (!(sequence[i])) {normalTextPen.setColor(0xFFFFFFFF);//白色}else {normalTextPen.setColor(0xFFFFFF00);//黄色}c.drawText(String.valueOf(i 1),(i 1)*WIDTH*0.9f WIDTH/2,3*WIDTH WIDTH/2 300 9*WIDTH,normalTextPen);}//设置检查按钮buttonCheckCreated(c);//绘制八条高亮的线段c.drawLine(WIDTH/2,3*WIDTH,WIDTH/2,12*WIDTH,lightLine);//左1c.drawLine(WIDTH/2 3*WIDTH,3*WIDTH,WIDTH/2 3*WIDTH,12*WIDTH,lightLine);//左2c.drawLine(WIDTH/2 6*WIDTH,3*WIDTH,WIDTH/2 6*WIDTH,12*WIDTH,lightLine);//左3c.drawLine(WIDTH/2 9*WIDTH,3*WIDTH,WIDTH/2 9*WIDTH,12*WIDTH,lightLine);//左4c.drawLine(WIDTH/2,3*WIDTH,WIDTH/2 9*WIDTH,3*WIDTH,lightLine);//上1c.drawLine(WIDTH/2,6*WIDTH,WIDTH/2 9*WIDTH,6*WIDTH,lightLine);//上2c.drawLine(WIDTH/2,9*WIDTH,WIDTH/2 9*WIDTH,9*WIDTH,lightLine);//上3c.drawLine(WIDTH/2,12*WIDTH,WIDTH/2 9*WIDTH,12*WIDTH,lightLine);//上4}
在此解释一下我使用drawRect(RectF,Paint)的原因——
public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {throw new RuntimeException("Stub!");}public RectF(float left, float top, float right, float bottom) {throw new RuntimeException("Stub!");}
起先是在看教程时,其中解释说,new rectf可以让方格按照预想的一样居中放置。
在网上查询rect与rectf的区别时,也解释说因为rect中的变量right、left、top、bottom是int型,而rectf是float型,可能会更加精确,但是ctrl 点击查看这两个传入的参数,可见,都是float型,不加new rectf应该是没有变化的。
后面使用drawRect时,我使用的就都是drawRect(float 左右上下)了。
这里还有一个要设置的是不同状态时,方块、选字框还有数字的颜色,所以,我在setColor方法上结合力switch-case,先判定一下当前状态再进行上色。
每次数据变化只需要进行一次redraw即可刷新地图,细节请参照上面的代码。
接下来是如何读取数字的问题。
在这个问题上我思考了许多种方法,我的思路顺序大略是下面这样的:
方案1.用输入流输出流的方式读入文件数据。
方案2.写一个专门储存游戏数据的类,随时调用。
方案3.写在String.xml中调用
方案1让我呕心沥血了整整一周,因为读取文本文件不禁涉及到文件存放位置的问题,同时还有以什么方式读取的问题,全部读取还是逐字逐句读取,读入中的异常处理,写入的字符串的固定格式等等……本身对IO流就不熟练的我设计这个方案更是难上加难,在多次尝试无果后,我认真思考既然终究都是要被读进程序里的,为何不去繁就简,把数据写进代码中,下次新增关卡的时候直接改代码好了!
虽然听起来有点低效,而且维护成本大,不过对于小项目,还是在接受范围内的。
于是我开始着手做方案2。
写一个java类,定义数串并当读到关卡数的时候返回关卡值。但是写到一半的时候,我的同学建议我:
为什么不把关卡值存在String.xml里?
对哦!
为什么不呢?
这样比java类更简单,而且更容易修改。直接着手写:
<resources><string name="app_name">SudokuByFB</string><string name="level1_1">315097400420300070708060300064029000839074002072100090000910580050083900081045236</string><string name="answer1_1">315297468426358179798461325164829753839574612572136894643912587257683941981745236</string><string name="level1_2">340000009000005320090840060070104206000730418400290705000300602238069150600027903</string><string name="answer1_2">345612879186975324792843561873154296529736418461298735957381642238469157614527983</string><string name="level1_3">692083001050102380080497000004270035026000100073001800700910062000734008005020970</string><string name="answer1_3">692583741457162389381497256814279635526348197973651824738915462269734518145826973</string><string name="level1_4">391057000000300000002089034010900620503271008240500370100720500476090003920006087</string><string name="answer1_4">391457862684312758952689134817943625563271948249568371138724596476895213925136487</string><string name="level1_5">100000089003000425640590070000059201891620000005001096000402500260000734074386902</string><string name="answer1_5">157243689983167425642598173736859241891624357425731896319472568268915734574386912</string><string name="level1_6">200040010617003005050000003009036578065080042008415069000300090092070830830920057</string><string name="answer1_6">283547916617893425954162783149236578365789142728415369476358291592671834831924657</string><string name="level1_7">600008005038900000702041090017004350426730000005100420204806501090503260503010700</string><string name="answer1_7">649328175138957642752641893917284356426735918385169427274896531891573264563412789</string><string name="level1_8">600005000037906008185200900860079420009064380703020100024601009578002010000750040</string><string name="answer1_8">692845731437916258185237964861379425259164387743528196324681579578492613916753842</string></resources>
先写了简单模式的八关作为实验品。
命名方式为i_j,i表示难度(简单1,普通2,困难3),j表示关卡数1,2,3,4,…,n。需要填写的地方改为0,只要打印的时候把0的位置跳过就达到目的了。
Perfect!
然后继续思考初始化的问题,初始化关卡信息(写入数字)吧!
我在前面描述过对关卡信息的存储,字符串levelRead储存数独题目,字符串levelAnswer存储数独答案。
通过之前把level的传入,我们通过一个getString方法结合之前R.String.name就能很简单就能获取到我们在String.xml中输入的信息。
请看如下代码:
private String getLevelRead(int level){String levelRead=null;switch (level){case 1:levelRead = getResources().getString(R.string.level1_1);break;case 2:levelRead = getResources().getString(R.string.level1_2);break;case 3:levelRead = getResources().getString(R.string.level1_3);break;case 4:levelRead = getResources().getString(R.string.level1_4);break;case 5:levelRead = getResources().getString(R.string.level1_5);break;case 6:levelRead = getResources().getString(R.string.level1_6);break;case 7:levelRead = getResources().getString(R.string.level1_7);break;case 8:levelRead = getResources().getString(R.string.level1_8);break;}return levelRead;}private String getLevelAnswer(int level){String levelAnswer=null;switch (level){case 1:levelAnswer = getResources().getString(R.string.answer1_1);break;case 2:levelAnswer = getResources().getString(R.string.answer1_2);break;case 3:levelAnswer = getResources().getString(R.string.answer1_3);break;case 4:levelAnswer = getResources().getString(R.string.answer1_4);break;case 5:levelAnswer = getResources().getString(R.string.answer1_5);break;case 6:levelAnswer = getResources().getString(R.string.answer1_6);break;case 7:levelAnswer = getResources().getString(R.string.answer1_7);break;case 8:levelAnswer = getResources().getString(R.string.answer1_8);break;}return levelAnswer;}
不过我们在初始化题目时,还要多干一件事。
仔细思考,我们将九阶矩阵maxtria当作棋盘,里面每一个点都是Dot类型,具有自己的状态,那么我们在输入时同时需要让已经填上数字的格子和没有填写数字的格子区分开。
怎么办怎么办?其实一开始我们已经设定好了。也就是dot的Input_Status输入状态。
所以可以在getLevelReaad方法中多添加一句:
//给levelRead赋值顺便把填数字的格子的状态改变一下for(int i=0;i<levelRead.length();i ){if(levelRead.charAt(i)!='0'){int row=i/9;//行数int col=i%9;//列数getDot(i%9,i/9).setInput_status(Dot.Input_status.Full);}}
(不过改变状态的方法这样写其实不太稳定,如果想优化一下,可以把它封装起来,而且,状态的改变也可以在打印数字时在进行。
我的最初考虑是,提前将状态转换后,更稳定,在刷新地图时点不会轻易改变,如果在打印数字的时候改变状态的话,可能会因为我高耦合的代码而产生难以解决的问题!!!)
然后简单写个printf我们关卡数字的方法即可,我这里这个方法的名字叫public void levelOutput(String levelRead,Canvas c)
因为它的写法和redraw里的绘制方法相同,所以就不多做描述了。
这样初始化的任务我们就基本完成了,接着用回调函数加载布局:
getHolder().addCallback(callback);//将callback对象指定到getholder中
在重写与它有关的方法,主要用于加载页面和让页面更加适用屏幕。
SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {try {redraw();//利用回调函数在每次启动时重新加载页面} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {//让游戏界面适应屏幕WIDTH=i1/(COL 1);try {redraw();} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {}};
初始化大概设置的差不多了,我们就要思考一下交互设计了。
根据之前的调研,我最终敲定:
胜利判定机制:最终判定
提示机制:只做选中数字相应横行、纵行、同一宫的提示,不做更细节方面的判定,取而代之的是在游戏最开始的地方加入新手指引
选字机制:玩家通过下面的选字框选数字
【其实在地图绘制的时候这个思路已经体现出来了,最后敲定方案实际上是在绘制完地图后,第一次开发缺少经验,这个理应在设计阶段就定下来的。】
那么与玩家的交互便是玩家点击填数。涉及到onTouch的方法及其监听器,那就在playGround中加入onTouch监听器。
public PlayGround(Context context,int level){super(context);getHolder().addCallback(callback);//将callback对象指定到getholder中matrix=new Dot[ROW][COL];//先逐行再逐列嵌套,添加数据for(int i=0;i<ROW;i )for(int j=0;j<COL;j ){matrix[i][j]=new Dot(j,i);//在画出9x9矩阵后可以发现,一个点所在的x恰恰等于列数,y恰恰等于行数,所以i和j对应在x和y中是相反的。}setOnTouchListener(this);//设置用户点击的监听器levelMain=level;//哪个关卡levelRead=getLevelRead(levelMain);//读关卡信息levelAnswer=getLevelAnswer(levelMain);//读关卡答案initGame();//在从创建完所有点之后将它调用}
这样playGround也就基本成型了。
之后写onTouch中的方法。
其实思路非常简单,在此就只讲解了。
从玩家的点击事件中,我们其实并无法真正知道玩家要点击的数组坐标,而是会获得一个以整个手机屏幕为坐标系的点击坐标(x,y)。
既然获得了这个值,下面的事就很简单了,将整个游戏界面分区块,点击到哪个区块,就执行哪个功能,然后在方法内减去整个区域的xy偏移量,对区域里每一个格子进行操作(状态改变等等等等)。
主干如下,剩下的就是算法问题了,在此不多加赘述。
@Overridepublic boolean onTouch(View view, MotionEvent e) {if(e.getAction()==MotionEvent.ACTION_DOWN){int x,y,i,j;i= (int) e.getX();//全局坐标xj= (int) e.getY();//全局坐标yx= (int) ((i-WIDTH/2)/WIDTH);//二维数组坐标xy= (int) ((j-3*WIDTH)/WIDTH);//二维数组坐标y//这是棋盘部分if(x<COL && y<ROW && x>=0 && y>=0){}//这是选字区else if(j>3*WIDTH WIDTH/2 170 9*WIDTH && j<3*WIDTH WIDTH/2 342 9*WIDTH){}//这个是检查框else if(i>WIDTH*7 && i<WIDTH*9 && j>(WIDTH*1.0f) && j<(WIDTH*3-WIDTH/3*2)){}else{//防止数组越界异常initGame();}try {redraw();} catch (InterruptedException ex) {ex.printStackTrace();}}return true;}
不过这里有一个要点,就是点击时要慎防数组越界,也就是用户点击到了我们还没有定义的地方,从而发生异常,所以一定要做异常处理。【参照else中】
如果用户点击的地方未经定义,就初始化一次变量。(取消所有点击状态的变量)
再在代码优化上稍加处理后,我们的填写部分也基本完成了,随后也就是检查部分。
这里思路就很简单了,用户在每次改变方框中字符的时候,同时修改字符串levelRead中的值,最后再将levelRead和levelAnswer用charAt(i)比对即可。
对错误的数字进行input_Status状态的改变,如果没有错误的话即可退出游戏。
不过,特此强调一下退出的方法。
因为布局文件playground并不是活动,而且接受到的context并不是LevelRead(游戏界面的活动)的子类,调用不了finish方法。所以怎么退出?
怎么办怎么办怎么办怎么办?
对LevelRead寻根溯源,发现context是它的父父父父父父类。所以可以强制转换一下!
levelAct = (LevelRead)context;
然后在LevelRead里写一个方法把finish()封装起来调用即可。
LevelRead里的:
public void endActivity(){finish();}
Playground里调用的:
//返回int型的over值用来报不同的错误代码over=checkAnswer(levelRead,levelAnswer);if(over==9){levelAct.endActivity();}
over是我的错误代码,
over=0,没填完;
over=1,有错误;
over=9,胜利。
playground告一段落!
4.2.6.2点阵Dot
因为我们游戏中要让九阶矩阵完成每一个格子能变色,并且里面的数字可以改变。
所以,我要设定一个Dot对象,里面包含x、y、盛装数字的状态Input_Status、当前的点击状态Click_Status。
这是对Dot类中元素的定义:
int x,y,num;Input_status input_status;Click_status click_status;public enum Input_status {//NULL为没有填数字的格子,FULL为填入默认值的格子,HAVE为玩家填入的格子,False表示玩家填错的格子Null,Full, Have,False}public enum Click_status {//Normal表示没有被点击过的格子,FALSE为填入错误数字的格子,CLICK为被玩家选中状态,Related为与被选中格子相关的格子Normal,Click,Related}//创建一个类,指定xy坐标public Dot(int x, int y){super();this.x=x;this.y=y;input_status=input_status.Null;click_status= click_status.Normal;}
然后为了方便,给每一个量写get、set方法,可以通过AS自动生成也可以自己写,很简单直接看代码:
public int getX(){return x;}public int getY(){return y;}public void setX(){this.x=x;}public void setY(){this.y=y;}public Input_status getInput_status(){return input_status;}public void setInput_status(Input_status status){this.input_status=status;}public Click_status getClick_status(){return click_status;}public void setClick_status(Click_status status){this.click_status=status;}public void setXY(int x,int y){this.x=x;this.y=y;}
SetXY()这个方法是为了方便多写的一个(虽然最后没有什么用处又被playground里的getDot方法代替掉了,但那已经是后话了)
4.2.7音乐单例mediaInstance
总体来说光是写这个类就下了很大的决心。因为在朋友推荐之前,我还从未使用过甚至已经淡忘了名字的知识点。
单例模式,字如其名,就是类中只存在也只能存在一个实体的类。而且在程序中使用时,作用对象始终是同一个。
它因为设计思路和应用场景的不同,主要被分为两种,懒汉式和饿汉式。主要就看是“需要时调用”还是“随时创建”。
它的格式如下:
public mediaInstance getInstance(){if(instance==null){instance=new mediaInstance();}return instance;}private void initMediaPlay() {}
只要保证每次调用时只会出现一个且仅一个实例即可。因为我没有多线程的功能,所以不需要担心我的单例模式出现建立多线程的问题,所以在这里就采取双重锁的结构了。
然后因为个人Java基础太差问题,卡了我许久的是类之间传递数据的方法。浅浅整理一下,Java的类之间传递数据一共有三种方法。
- 对于不变的量,用interface接口即可。
- 对于获取别的类中的属性,用this.x=a.x即可。
- 将文件写入xml、txt、数据库文件中读取(我的关卡数据就是这么做的)。
虽然道理是这样子,我对第二种方式还是抱着很大的疑惑的。如果这个对象里没有明确的属性怎么办,我想调用里面的方法怎么办?然后在朋友坚持不懈的讲解和csdn中安卓音乐播放器代码的启发下,我终于学会使用静态类方法来进行方法的调用。
我对静态类的理解是——为了减少不必要的对象的加载,所以可以只将这个对象的常用方法设为静态类,以便于调用,而不用在每次加载之前还要将对象实例化一遍。
然后我完善了代码:
对于点击“start”按钮的:
btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {int musicId = getResources().getIdentifier("music_one","raw",getPackageName());file = getResources().openRawResourceFd(musicId);mediaInstance mediaInstance = com.example.sudokubyfb.mediaInstance.getInstance(file);Intent intent=new Intent(StartGame.this,MainPart.class);startActivity(intent);}});
对于单例模式的方法的:
public static mediaInstance getInstance(AssetFileDescriptor file){if(instance==null){instance=new mediaInstance();}if(instance.isInit)return instance;instance.initMediaPlay(file);return instance;}
音乐播放器的initMediaPlay()方法和书上讲的类似,在此就不做细说。
然后只需要分别写一下play和stop方法来控制音乐开关,再在设置页面的switch中调用即可。
4.3 出现bug
若说起在开发过程中遇到的bug,最多的还属两类:
·数组溢出类
·构建失败类
(因为出错时忘记截图,所以就浅浅口述一下了)
先说第一类,其实并不算难找,run中找到error的红字,再看一遍就很快发现了。这个问题主要出现于我的九阶矩阵的错误赋值或者是onTouch方法中用户点中了未提前设置的位置。
解决方法就是重新估算距离,或者做好错误处理就ok。
最头疼的是第二类bug。
无需多言,上字段。
Launching 'app' on emulator-5554.
Installation did not succeed.
The application could not be installed.
出现这个问题,就能很快知道是虚拟机出问题了。但是到底是什么问题呢?
查询了许多博客,大多数人的此类报错后都会带有虚拟机出错的原因。
但是很可惜,我的是没有的。。。
有人说要在Gralde文件夹的local.properties文件中加入android.injected.testOnly=false,但经过我的尝试后,没有用。
还有人说要换虚拟机重试,这个方法不算太好,不过出九次bug能好七次,也还算管用(就是starting时间太长了)。
最后我学会了cold boot和wipe data,一套组合拳解决问题。
由此可知,这个问题的原因应该是虚拟机的内存不足。其实在有些版本的AS中是有提示的。好奇的朋友可以在csdn上查询到。
不过没有归没有,KO掉就是胜利(·`ω·`)y
4.4机制优化
一、填数刷新
在进行bata测试的时候,朋友们建议我说:为了方便考虑,能不能在输入完数字后自动取消点击状态。听起来简单,这个问题困扰了我很长时间。
用户的填数操作无疑只有两类。
- 点击选字栏
- 点击九阶矩阵位置
最开始为了保留每次用户点击后的位置信息以及数字信息的存储,我定义了两个用于存储位置和数字的变量。
那么我就为这两个事件的结果开设两个存储空间。
- Dot coo
- Int point
然后设为类内的private变量,每次点击位置改变时,就给它赋值,然后一旦填数成功,就把它清空。
可以将其理解为一个深度为1的栈,将值置入栈顶,使用时就弹出即可。
之所以没能刷新成功,是在如何刷新的算法上出现了问题。因为没有很好的对coo和point的封装方式,所以初始化的很乱,很容易出现数组溢出等等的错误,苦不堪言。
之后我又想起了我的initGame方法、initSequence方法以及initMaxtria方法,为什么不直接在初始化信息时将他们一起初始化?
于是我更新了这两个方法:
//只重置选数字的这个框private void initSequence(){for(int i=0;i<9;i ){sequence[i]=false;}point=-1;}//只重置数独的框private void initMatrix(){for (int i=0;i<ROW;i )for (int j=0; j<COL; j ){matrix[i][j].setClick_status(Dot.Click_status.Normal);//初始化,将所有的宫中的值设为null状态}coo=null;}
问题完美解决!
- 填数过程中突然闪退
Bata测试中出现了一个扑朔迷离的问题,“填数过程中,已经填写一半了,在点击九宫格时,突然闪退到主界面”。
为什么为什么为什么为什么?
我只能猜测这应该是矩阵Maxtria填数错误发生了溢出,但在自己的测试中始终找不到问题出在了哪。
然后查询run中的信息,……Integer.valueOf…
发现是int类型的数组溢出,报错在point和coo的位置,发现point没有初始化数字,赋初值以后,解决。
- 增加难度
为了增加另外几个难度(普通、困难),所以更新了一下LevelChoose中的代码。只需要向传入的intent中多加一个difficulty的int型变量并传给PlayGround即可!
直接上代码:
intent.putExtra("difficulty",difficulty);
这样子,在改变一下playground的传入数字的格式,把difficulty放进去就好了。
- 项目利弊
5.1项目特色
本次虽然时间紧张,但还是一鼓作气把界面和游戏算法给做出来了。
游戏中目前包含开始画面、游戏界面,大量对游戏的错误处理机制。填写方便,检查机制更符合竞赛规则。
5.2项目中出现的问题
- 主要是时间过于仓促,用户界面和计时器,更多活动玩法还没有做出来,非常的遗憾,但一切远期功能,我依然会继续完善的。
- 算法问题:先点击选字区,再填写数字,再点一下选字区另一个数字。格子里的数会切换一次。之后则不再会切换。
- 实验总结
6.1对项目的
这是我第一次从构思到机制设计到算法,完全由自己一个人完成的项目。虽然看起来麻烦冗长,但敲代码的过程是非常非常快乐专注的,而且学习的主观能动性非常强,自己查询学习了许多课内外的知识,增长了见识,而且,最重要的一点是,在自己计算、思考、改bug的过程中增长了大量的编程经验。
我遇到的太多bug主要都与我的编程习惯有关。在开始脑中和纸上都没有画过类图,在写方法时不注意private和public变量的使用,也没有提前计划好如何更好的封装我的类,所以许多功能都混杂在了一起,在出问题后,我对此使用替换查找,找到关键词充满了代码的各个角度,头疼!
其实最开始我只设置了一种状态,将input和click状态混杂在一起。这时就出现了问题——一个格子可能具有很多状态,但在我切换状态时,这些状态互相顶替,功能还有不同程度的重合,给编程带来了极大的困难,几乎是到了编码中期,我才将这两种Status封装起来,这才让app变得稍微有些层次感。
所以在接下来的编程中,我就更加注重避免耦合,将更多可以独立出来的功能封装进独立的方法中。只是下次编程前,我会封装更多的类,把主类和其余的对象分离开。
我认为这次编程是成功的,Android不仅仅是一门课,它给予我一个练习java和应用开发的平台,让我更加熟悉编程模式和软件工程知识,并且与最近正在学习的信息系统知识也更好的结合在了一起。
纸上得来终觉浅,绝知此事要躬行。此话真的不假。
6.2对数独爱好者的
数独在近几年已经逐步走向消亡,退出人们的视野中,跻身于中小学生的智力竞赛里,也只有青少年的竞赛活动中,还能看到它的身影。学校中的数独社团也越来越少……
这着实令人痛心。
我希望能通过我的努力实践,更加了解这个游戏的机制,以及玩法。有时候学会不以为着懂得,这样的实践也让我的理解进一步深入,我觉得下一次,我就可以挑战制作6×8数独、对角数独等等……
各位数独人,需共勉,再接再厉!
6.3对开发过程中帮助我的人
感谢我的室友们与我的同学们在我的开发过程中提出了许多指导意见,并协助我进行测试工作,这给予我编程莫大的帮助与鼓励。
包括但不限于将答案存放在String.xml,封装finish(),在我很多困难的时候伸出援手,带我走出黑暗的改bug和调试生活。
同时也感谢我的老师,能给予我这个千载难逢的机会,能让我以作业的理由进行我喜爱的事情。
再之,也感谢一直坚持下去的我!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。