拼图小游戏开发
最近刚刚完成了一个Android小游戏的开发,在开发过程中还是用到了很多知识点的,所以在开发完成后做一个总结。
游戏源码(主要app)
游戏源码(提供数据库的app)
1、游戏的大致思路
拼图游戏本身界面的设计非常的简单,只需要把挑选好的图片按照33、44等方式切割成小方块随机乱序放在一个GroupView里面,选择一张小图片时,给予高亮,选择第二张小图片时,交换,直到所有的小方格排列在自己的位置上后,游戏结束。
2、游戏的实现思路
3、游戏的具体需求
- 两个app,一个存储数据,一个处理UI,数据通过provider在两个app间传输。
- 字符串提取到资源文件,方便修改,同时可以进行语言类别切换(中英文切换等)。
- 所有的数字提取到资源文件里面,可以随意修改。
- 界面里面要有计时器。
- 具备异常中断处理(来电接听5分钟后,功能正常)。
- 可以保存排行榜。
- 可以保存游戏进度(手机重启后可以继续上次游戏)。
- 需要在不同分辨率的手机上运行正常。
- 需要支持小部件。
- 需要有流程图展示。
4、系统总体流程图
5、游戏的具体界面
表5.1
游戏主界面 | 切换语言 |
---|---|
表5.2
游戏排行榜 | 游戏界面 |
---|---|
表5.3
选择图片 | 选择难度 |
---|---|
表5.4
游戏结束 | 桌面小部件 |
---|---|
6、游戏的具体实现
6.1、桌面小部件的实现
桌面控件是通过BroadcastReceiver的形式来进行控制的,因此每一个桌面控件都对应一个BroadcastReceiver。Android系统提供了一个AppWedgetProvider类,是上面类的子类,开发者只需要继承这个类即可。
由于本次开发中桌面小部件含有数据集,所以还需要一个类来处理对应组件的事务。Android提供了RemoteViewService类来帮助开发者。
由于小部件的实现类是继承了广播的,所以要在AndroidManifest文件里面进行注册,同时要在res\xml文件下添加appwidget_provider.xml文件,用来定义小部件的一些初始设置。
MyAppWidget.java
public class MyAppWidget extends AppWidgetProvider {
public static final String CHANGE_IMAGE = "com.example.gj.action.CHANGE_IMAGE";
//这个对象用来加载指定的页面布局
private RemoteViews mRemoteViews;
/**
* ComponentName,顾名思义,就是组件名称,这个类主要用来定义一个应用程序的组件,
* 通过调用Intent中的setComponent方法,我们可以打开同个应用以及不同应用中的组件。例如:Activity,Service。
*/
private ComponentName mComponentName;
private int[] imgs = ImageSoures.imageSours;
//更新桌面控件的方法
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
mRemoteViews = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
mRemoteViews.setImageViewResource(R.id.iv_test, R.drawable.widget);
mRemoteViews.setTextViewText(R.id.btn_test, "点击进入游戏");
Intent skipIntent = new Intent(context, MainActivity.class);
//延时进入MainActivity,FLAG_CANCEL_CURRENT参数是指如果描述的PendingIntent已经存在,则在产生新的Intent之前会先取消掉当前的
PendingIntent pi = PendingIntent.getActivity(context, 200, skipIntent, PendingIntent.FLAG_CANCEL_CURRENT);
// PendingIntent是指把Intent包装了一层, 并且把PendingIntent放入一个新的进程. 通过触发事件去触发这个PendingIntent.
mRemoteViews.setOnClickPendingIntent(R.id.btn_test, pi);
/**
* 设置ListView的适配器
* @param lvintent 对应启动ListViewService(继承RemoteViewsService)的intent
* 通过setRemoteAdapter将 ListView 和ListViewService关联起来
* 从而达到ListViewService 更新 ListView 的目的
*/
Intent lvIntent = new Intent(context, ListViewService.class);
mRemoteViews.setRemoteAdapter(R.id.lv_test, lvIntent);
/**
* 通过 setPendingIntentTemplate 设置intent模板
* 然后在处理该“集合控件”的RemoteViewsFactory类的getViewAt()接口中
* 通过 setOnClickFillInIntent 设置“集合控件的某一项的数据”
*/
Intent toIntent = new Intent(context, MyAppWidget.class);
toIntent.setAction(CHANGE_IMAGE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, toIntent, PendingIntent.FLAG_UPDATE_CURRENT);
mRemoteViews.setPendingIntentTemplate(R.id.lv_test, pendingIntent);
//更新小部件
mComponentName = new ComponentName(context, MyAppWidget.class);
appWidgetManager.updateAppWidget(mComponentName, mRemoteViews);
}
//重写这个方法,将组件当成广播来使用。
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if (intent.getAction().equals(CHANGE_IMAGE)) {
int position = intent.getIntExtra(ListViewService.INITENT_DATA, 0);
mRemoteViews = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
mRemoteViews.setImageViewResource(R.id.iv_test, imgs[position]);
mComponentName = new ComponentName(context, MyAppWidget.class);
AppWidgetManager.getInstance(context).updateAppWidget(mComponentName, mRemoteViews);
}
}
}
ListViewService.java
public class ListViewService extends RemoteViewsService {
public static final String INITENT_DATA = "extra_data";
/**
* 重写这个方法,会返回一个RemoteViewsFactory对象
* 它负责为RemoteView中的指定组件提供多个列表项
* @param intent
* @return
*/
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ListRemoteViewsFactory(this.getApplicationContext(), intent);
}
private class ListRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private Context mContext;
private List<String> mList = new ArrayList<>();
public ListRemoteViewsFactory(Context context, Intent intent) {
mContext = context;
}
@Override
public void onCreate() {
//初始化数组
for (int i = 1; i <= ImageSoures.imageSours.length; i++) {
mList.add("" + i);
}
}
@Override
public void onDataSetChanged() {
}
@Override
public void onDestroy() {
mList.clear();
}
@Override
public int getCount() {
return mList.size();
}
/**
* 这个方法的返回值控制各个位置所显示的RemoteView
* @param position
* @return
*/
@Override
public RemoteViews getViewAt(int position) {
RemoteViews views = new RemoteViews(mContext.getPackageName(), android.R.layout.simple_list_item_1);
views.setTextViewText(android.R.id.text1, "image_" + mList.get(position));
views.setTextColor(android.R.id.text1, mContext.getResources().getColor(R.color.white));
//这个intent用来传送数据
Intent changeIntent = new Intent();
changeIntent.putExtra(ListViewService.INITENT_DATA, position);
changeIntent.setAction(MyAppWidget.CHANGE_IMAGE);
views.setOnClickFillInIntent(android.R.id.text1, changeIntent);
return views;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return false;
}
}
}
6.2、数据库操作及内容提供者
这一部分比较基础,就没有写注释什么的,可以自己查阅工具书(contentProvider需要在manifest文件里注册)。
建表
final String CREATE_TABLE_SQL = "create table if not exists player (id integer PRIMARY KEY AUTOINCREMENT " +
"NOT NULL,name text,score integer)";
db.execSQL(CREATE_TABLE_SQL);
Toast.makeText(mcontext,"创建成功", Toast.LENGTH_SHORT).show();
自写一个Provider类
@Override
public boolean onCreate() {
myDatabaseHelper = new MyDatabaseHelper(this.getContext(), "play.db", 1);
uriMatcher.addURI(Words.AUTHORITY,"players",PLAYERS);
uriMatcher.addURI(Words.AUTHORITY,"player/#",PLAYER);
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
SQLiteDatabase sqLiteDatabase=myDatabaseHelper.getReadableDatabase();
return sqLiteDatabase.query("player",projection,selection,selectionArgs,null,null,sortOrder,null);
}
ListView的数据加载
private void setAdapter() {
Cursor cursor = sqLiteDatabase.rawQuery("select * from player", null);
List<Map<String, Object>> players = new ArrayList<Map<String, Object>>();
while (cursor.moveToNext()) {
HashMap<String, Object> playerHashMap = new HashMap<String, Object>();
playerHashMap.put("id", cursor.getString(0));
playerHashMap.put("name", cursor.getString(1));
playerHashMap.put("score", cursor.getString(2));
players.add(playerHashMap);
}
cursor.close();
if (!players.isEmpty()) {
MySimpleAdapter mySimpleAdapter = new MySimpleAdapter(
this,
players,
R.layout.player_list_items,
new String[]{"id","name","score"},
new int[]{R.id.pid,R.id.pname,R.id.pscore}
);
myListView.setAdapter(mySimpleAdapter);
}else{
Toast.makeText(MainActivity.this,"数据库为空",Toast.LENGTH_SHORT).show();
}
}
主要APP里的内容接收者
//插入数据
int score = 0;
ContentValues contentValues = new ContentValues();
contentValues.put("name", pname.getText().toString());
contentValues.put("score", mTime.getText().toString());
Cursor cursor = contentResolver.query(Uri.parse("content://org.gj.providers.playerProvider/players"), new String[]{"id", "name", "score"}, "name = ?", new String[]{pname.getText().toString()}, null);
if (cursor.getCount() > 0) {
while (cursor.moveToNext()) {
score = cursor.getInt(2);
}
if (Integer.parseInt(mTime.getText().toString().trim()) <= score) {
contentResolver.update(Uri.parse("content://org.gj.providers.playerProvider/players"), contentValues, "name = ?", new String[]{pname.getText().toString()});
}
} else {
contentResolver.insert(
Uri.parse("content://org.gj.providers.playerProvider/players"), contentValues
);
}
//查询数据
Cursor cursor = contentResolver.query(Uri.parse("content://org.gj.providers.playerProvider/players"), new String[]{"id", "name", "score"},null,null, "score asc");
List<Map<String, Object>> players = new ArrayList<Map<String, Object>>();
while (cursor.moveToNext()) {
HashMap<String, Object> playerHashMap = new HashMap<String, Object>();
playerHashMap.put("id", id);
playerHashMap.put("name", cursor.getString(1));
playerHashMap.put("score", cursor.getString(2));
id++;
players.add(playerHashMap);
}
6.3、游戏页面的实现
当一张图片按照3x3、4x4这样的格式切割后,会存放在一个List集合中,在通过随机函数将其位置乱序, 然后把乱序后的图片添加一个 tug标签,以i_j的形式组成,前面的i是乱序后图片的序号,j是切割时图片的顺序,当j的数字全部有序了后表示拼图成功。
ImagePiece
public class ImagePiece implements Serializable{
//这个属性表示
private int index;
private Bitmap bitmap;
public ImagePiece() {
}
public ImagePiece(int index, Bitmap bitmap) {
this.index = index;
this.bitmap = bitmap;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public Bitmap getBitmap() {
return bitmap;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
@Override
public String toString() {
return "ImagePiece [index=" + index + ", bitmap=" + bitmap + "]";
}
}
方法initBitmap用来切图和排序
private void initBitmap() {
// 这里设置图片
mBitmap = BitmapFactory.decodeResource(getResources(), ImageSoures.imageSours[PICTURENUM]);
// 按照指定图片和列数进行切图
mItemsBitmaps = ImageSplitterUtil.splitImage(mBitmap, mColumn);
tags = SpUtil.getStringArray(context, "tag");
// 使用 sort 使图片乱序的功能
if (tags.length <= 0) {
tags = new String[mColumn * mColumn];
Collections.sort(mItemsBitmaps, new Comparator<ImagePiece>() {
@Override
public int compare(ImagePiece a, ImagePiece b) {
return Math.random() > 0.5 ? 1 : -1;
}
});
} else {
List<ImagePiece> list = new ArrayList<>();
for (int i = 0; i < mColumn * mColumn; i++) {
int l = getImageIndex(tags[i]);
list.add(mItemsBitmaps.get(l));
}
mItemsBitmaps = list;
}
}
方法initItem用来设置item
private void initItem() {
mItemWidth = (mWidth - mPadding * 2 - mMargin * (mColumn - 1))
/ mColumn;
mGamePintuItems = new ImageView[mColumn * mColumn];
// 生成我们的Item, 设置Rule
for (int i = 0; i < mGamePintuItems.length; i++) {
ImageView item = new ImageView(getContext());
// 为每个Item 添加监听
item.setOnClickListener(this);
item.setImageBitmap(mItemsBitmaps.get(i).getBitmap());
mGamePintuItems[i] = item;
// 为每一个Item设置向对象的ID方便后面对每个Item使用Id进行排版
item.setId(i + 1);
// 在Item的tag中存储了index index里面的数字是过关时图片应有的index
if (tags[i] == null || tags[i].equals("")) {
item.setTag(i + "_" + mItemsBitmaps.get(i).getIndex());
tags[i] = i + "_" + mItemsBitmaps.get(i).getIndex();
} else {
item.setTag(tags[i]);
}
SpUtil.putStringArray(context, "tag", tags);
Log.d("gj", "initItem: " + tags[i]);
LayoutParams lp1 = new LayoutParams(
mItemWidth, mItemWidth);
// 设置Item间横向间隙,通过rightMargin
// 不是最后一列
if ((i + 1) % mColumn != 0) {
lp1.rightMargin = mMargin;
}
// 不是第一列
if (i % mColumn != 0) {
lp1.addRule(RelativeLayout.RIGHT_OF,
mGamePintuItems[i - 1].getId());
}
// 如果不是第一行
if ((i + 1) > mColumn) {
lp1.topMargin = mMargin;
lp1.addRule(RelativeLayout.BELOW,
mGamePintuItems[i - mColumn].getId());
}
addView(item, lp1);
}
}