Android游戏开发实践之NDK与JNI开发04
分类标签: NDK
p; } 这里再提一下,如上`JNIUtils.java`类中定义好了native方法,如何根据对象的方法签名生成对应的C/C++方法的声明。这部分内容在Android游戏开发实践(1)之NDK与JNI开发01 已经提到过,我们可以借助javah来根据编译后的.class生成对于的头文件。 普通做法是: 在AndroidStudio中可以: Tools-> Ext阅读全文 »
Android游戏开发实践之NDK与JNI开发03
分类标签: NDK
本文的目录如下: 1、环境搭建 2、创建一个支持C/C++的项目 2.1 新建项目 2.2 扩展现有项目 3、AndroidStudio与Gradle 3.1 project/build.gradle 3.2 project/settings.gradle 3.3 module/build.gradle 本文的目录如下: 1、环境搭建 2、创建一个支持C/C++的项目 2.1 新建项目 2.2 扩展现有项目 3、AndroidStudio与Gradle 3.1 project/build.gradle 3.2 project/settings.gradle 3.3 module/build.gradle 1、 环境搭建要让AndroidStudio支持NDK开发,除了需安装AndroidStudio2.2以上的版本。还得安装NDK、CMake、LLDB等工具。在AndroidStudio中选择Tools->Android->SDK Manager->SDK Tools。如图:这里简单介绍下:NDK:是Android开发本地C/C++代码的一套工具集。CMake:一套跨平台的构建工具,可以Gradle脚本配合使用来构建你的本地库。在AndroidStudio中,Google默认和推荐使用CMake进行编译,当然,你仍然可以继续使用ndk-build来编译你的本地代码。注意:32位的操作系统可能会装不上。LLDB: 一种调试器,Android Studio中可以使用它来调试本地代码。2、 创建一个支持C/C++的项目这里所说的创建一个支持C/C++的项目,可以理解为创建一个NDK项目,但又包含两种方式,分别是从零开始新建一个支持C/C++开发的项目和在原有项目的基础上让它支持C/C++开发。下面对这两种方式分别进行说明下。2.1 新建项目如果安装好了,上面介绍的几个工具(主要是NDK),并且AndroidStudio2.2以上的版本,新建项目的时候,会看到这个选项。如图:创建项目时,勾选C++支持。项目中所用的C++标准可以选择默认或者支持C++11,以及是否支持异常和rtti特性。创建完项目,会比一般的Android项目多出cpp目录以及CMakeLists.txt的文件。这里指定NDK的路径。即,上面环境搭建里安装的ndk,会下载到AndroidStudio根目录下的ndk-bundle文件夹中。make一下当前新创建的工程,默认会在build/cmake/debug/obj/下生成相应的动态库。2.2 扩展现有项目要让现有的Android项目能调用本地C/C++代码或者支持C/C++开发,只需要在原来项目的基础稍加修改即可。步骤如下:切换到project视图,打开module即上图的app模块,在src/main下右键New->Directory,填写一个文件名,例如:cpp。在刚建的cpp路径下,右键New->C/C++ Source File,输入文件名,若要一并生成相应的.h文件,勾选Create an associated header选项即可。注意,后面可以指定源文件的后缀,例如:.cxx,.hxx。在module的根目录即上图的app根目录,右键New->File,输入CMakeLists.txt。注意:文件名必须为CMakeLists.txt在module的根目录即上图的app根目录,选择Link C++ Project with Gradle,然后,找到刚创建的CMakeLists.txt文件。将CMakeLists.txt关联到项目中。注意,Build System仍可以选择ndk-build方式进行编译。当然,这步操作也可以手动完成,相当于在当前module下的build.gradle脚本中,添加了如下代码,XML/HTML代码 android { //指定使用CMake编译---------------- externalNativeBuild { cmake { path 'CMakeLists.txt' } } //-------------------------------- } 打开新建的CMakeLists.txt文件,输入如下代码:XML/HTML代码 cmake_minimum_required (VERSION 3.4.1) add_library (hellojni SHARED src/main/cpp/hellojni.cpp) 分别指定CMake要求的版本,add_library中参数分别是,指定生成库的名称,生成库的类型,默认是静态块,即:·a,源码的路径。这里实例只简单介绍下CMake的用法,后续会有专门一篇来介绍下CMake更多高级的用法。以上完毕,在make一下当前工程,或者rebuild一下,不出意外会在build/intermediates/cmake/debug路径下生成各种libhellojni.so文件。3、AndroidStudio与Gradle上面提到,将CMakeLists.txt关联到项目中,会在build.gradle脚本中,添加一段代码即可。可能刚接触AndroidStudio(特别是使用其它IDE开发的,例如:eclipse等)对Gradle不是很了解,这里就抛砖引玉下,简要讲述下gradle脚本的使用。首先,AndroidStudio是基于IntelliJ IDEA的IDE,在AndroidStudio中新创建的Android工程都形如如下结构:XML/HTML代码 MyApp ├── build.gradle ├── settings.gradle └── app ├── build.gradle ├── build ├── libs └── src └── main ├── java │ └── com.package.myapp └── res ├── drawable ├── layout └── etc. MyApp是项目名,app是模块名,一个项目下可以包含若干个模块。这与eclipse的结构不同,对应到eclipse中,app就相当于项目名,MyApp相当于工作空间。或者类似于VS中解决方案与项目的关系。以上目录结构关系,并不与实际磁盘上的目录结构对应。可以看到,在项目根目录下以及模块目录下,分别有三个.gradle文件。下面,就分别介绍这三个gradle脚本的用途,当然这里主要说的是在AndroidStudio下的gradle的相关使用。在AndroidStudio中android项目是基于gradle进行构建的(eclipse中可以使用ant来做类似的工作),而gradle是一种基于Groovy语言的DSL(domain-specific language,领域专用语言),而Groovy又是一种基于JVM的动态语言。所以,有java基础的话,理解Groovy会更容易。有关Gradle文档可以看看这个:https://docs.gradle.org/current/dsl/3.1 project/build.gradle该build.gradle位于项目的根目录,该文件是定义在这个工程下的所有模块的公共属性。默认如下:XML/HTML代码 // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } 以下只是从表象说明下,但实质是Groovy相应的数据结构(闭包,Map等)调用相应方法来动态控制整个构建过程。有关Groovy的讨论可以看看3.3 module/build.gradle。buildscript:定义了全局的相关属性。repositories:定义了远程仓库的源,即代表你的依赖包的来源。这里将jcenter()作为仓库。dependencies:定义了android gradle plugin的版本。allprojects:可以用来定义各个模块的默认属性,你可以不仅仅局限于默认的配置,未来你可以自己创造tasks在allprojects方法体内,这些tasks将会在所有模块中可见。task clean:执行相关的清理任务。3.2 project/settings.gradle该文件位于项目根目录下,也是项目的全局配置文件,该文件的内容如下:include ':app'如果,你的项目中有多个模块时,可以依次添加到该文件中。例如:include ':app',':librarys:Mylibrary'即在项目根目录下的librarys目录里有个Mylibrary库工程。3.3 module/build.gradle该文件位于当前模块的根目录下,通常情况下,该文件只对当前模块起作用。例如:XML/HTML代码 apply plugin: 'com.android.application' android { compileSdkVersion 24 buildToolsVersion "24.0.2" defaultConfig { applicationId "com.alphagl.test" minSdkVersion 19 targetSdkVersion 24 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } externalNativeBuild { cmake { path 'CMakeLists.txt' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.2.1' } 以上内容,要彻底弄清楚,得深究下Groovy语法。这里,只浅析下。apply plugin: 'com.android.application':在Groovy中的调用为project.apply([plugin: 'com.android.application']),plugin: 'com.android.application会被作为一个Map参数传递给apply的方法。这里是要将该模块构建为一个应用,若要将模块构建库形式,可以传参为plugin: 'com.android.library。在Groovy中花括号包含的部分称为一个闭包(Closure)。上面的主要两部分android和dependencies,分别对应project中的方法,而参数是相应的闭包结构。通过上面的结构,可以知道该闭包结构还有闭包嵌套和相应的方法。android:该方法包含了所有的Android属性,而唯一必须包含属性为compileSdkVersion和buildToolsVersion。compileSdkVersion:该方法包含编译该app时候,使用到的api版本。buildToolsVersion:该方法包含构建工具的版本号。defaultConfig:该方法包含app的核心属性,该属性会覆盖在AndroidManifest.xml中的对应属性。applicationId:该方法定义的属性会覆盖AndroidManifest文件中的包名package name属性。minSdkVersion:该方法定义的属性表示最小支持api版本。同AndroidManifest中对应的属性。targetSdkVersion:该方法定义的属性表示目标平台api版本。同AndroidManifest中对应的属性。buildTypes:该方法定义了构建不同版本的app相关的属性。release:配置release版本相关的属性。minifyEnabled:是否进行混淆。proguardFiles:定义混淆文件的位置。通过getDefaultProguardFile方法获取。externalNativeBuild:native使用cmake编译。dependencies:gradle默认的属性之一,定义了所有的依赖包。compile:编译相应依赖的jar包。组织名,包名,版本号的结构。以上只简单的列举了下部分属性,对gradle脚本有初步的了解。当然,Groovy在java领域还是有很多应用的。感兴趣的,可以深入了解下。Groovy文档:Groovy-Documentation作者:AlphaGL出处:http://www.cnblogs.com/alphagl/阅读全文 »
Android游戏开发实践之NDK与JNI开发02
分类标签: NDK
承接上篇Android游戏开发实践(1)之NDK与JNI开发01分享完JNI的基础和简要开发流程之后,再来分享下在Android环境下的JNI的开发,以及涉及到的NDK相关的操作。当然,本篇仍是以Eclipse作为开发IDE,虽然G阅读全文 »
Android游戏开发实践之NDK与JNI开发01
分类标签: NDK
NDK是Native Developement Kit的缩写,顾名思义,NDK是Google提供的一套原生Java代码与本地C/C++代码“交互”的开发工具集。而Android是运行在Dalvik虚拟机之上,支持通过JNI的方式调用本地C/C++动态链接库。C/C++有着较高的性能和移植性,通过这种调用机制就可以实现多平台开发、多语言混编的Android应用了。 NDK是Native Developement Kit的缩写,顾名思义,NDK是Google提供的一套原生Java代码与本地C/C++代码“交互”的开发工具集。而Android是运行在Dalvik虚拟机之上,支持通过JNI的方式调用本地C/C++动态链接库。C/C++有着较高的性能和移植性,通过这种调用机制就可以实现多平台开发、多语言混编的Android应用了。当然,这些都是基于JNI实现的。在游戏开发中,这种需求更是必不可少。 1、认识JNI JNI是Java Native Interface的缩写,也称为Java本地接口。是JVM规范中的一部分,因此,我们可以将任何实现了JVM规范的JNI程序在Java虚拟机中运行。这里的本地接口,主要指的是C/C++所现实的接口。因此,也使得我们可以通过这种方式重用C/C++开发的代码或模块。 具体关于JNI的详细介绍,可以参见JNI的官方文档。 Java Native Interface Specification: http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html 2、JNI的类型和数据结构 实现原生Java代码与本地C/C++代码,一个重要的环节是将原生Java的类型和数据结构映射成本地C/C++支持的相应的类型和数据结构。 (1)Java基本数据类型与原生C/C++类型对应关系如下: Java类型 本地类型 说明 boolean jboolean 无符号,8位 byte jbyte 无符号,8位 char jchar 无符号,16位 short jshort 有符号,16位 int jint 有符号,32位 long jlong 有符号,64位 float jfloat 32位 double jdouble 64位 void void N/A (2)Java引用数据类型与原生C/C++类型对应关系如下: Java类型 本地类型 Object jobject Class jclass String jstring Object[] jobjectArray boolean[] jbooleanArray byte[] jbyteArray char[] jcharArray short[] jshortArray int[] jintArray long[] jlongArray float[] jfloatArray double[] jdoubleArray 通过上面的对应关系可以发现,本地类型的命名基本上是在Java原生类型明明的前面加上了个j,组成j-type格式的新类型命名,还是很直观的。 (3)JNI引用类型的类关系图,如下: (上图源自:Java Native Interface Specification文档) 3、JNI函数的签名 在函数的声明中,由函数的参数,返回值类型共同构成了函数的签名。因此,将Java函数映射到本地C/C++中的对应也要遵循相应的规则。 (1)函数数据类型的签名关系如下: Java类型 类型签名 boolean Z byte B char C short S int I long J float F double D void V full-qualified-class(全限定的类) L [] [ boolean[] [Z byte[] [B char[] [C short[] [S int[] [I long[] [J float[] [F double[] [D 注意: 1. full-qualified-class(全限定的类):指的是引用类型,用L加全类名表示。 2. 数组类型的签名,只取中括号左半边。 (2)JNI函数签名格式比较 Java函数原型: return-value fun(params1, params2, params3) return-value:表示返回值 params:表示参数 对应函数签名格式为: (params1params2params3)return-value 注意: 1. JNI函数签名中间都没逗号,没有空格 2. 返回值在()后面 3. 如果参数是引用类型,那么参数应该写为:L加全类名加分号。例如:Ljava/lang/String; 根据这种规则,知道Java函数原型就能判断出对应的JNI函数的签名格式:Java代码 // 原型为: boolean isLoading(); // 签名格式为: ()Z Java代码 // 原型为: void setLevel(int level); // 签名格式为: (I)V Java代码 // 原型为: char getCharFunc(int index, String str, int[] value); // 签名格式为: (ILjava/lang/String;[I)C 4、JNI开发流程 1.简要开发步骤 JNI的具体开发流程总结起来分为这么几步: (1)在原生java类中声明native方法。native表明该方法为一个本地方法,由C/C++实现。 (2)使用javac命令将带有声明native方法的类,编译成class字节码。javac是jdk自带的一个命令,一般在javapath/bin(javapath为java安装目录)路径下。 (3)使用javah命令将编译好的class生成本地C/C++代码的.h头文件。同样,javah也是jdk自带的一个命令。 (4)实现.h头文件中的方法。 (5)将本地代码编译成动态库。注意,不同平台的动态库是不一样的。 (6)在java工程中引用编译好的动态库。 2.开发实例 按照上面的开发步骤作为指导,来一步步实现个简单的JNI的例子。 (1)新建名为HelloJNI的java工程,并新建一个声明了native方法的类。(这里就以Eclipse作为开发IDE举例了)Java代码 package com.hellojni.test; public class HelloJni { public native void printJni(); public static void main(String[] args) { } } (2)使用javac编译该HelloJni的类编译为.class文件。当然,这步也可以由Eclipse来完成即可。 (3)使用javah命令生成头文件。在命令行终端下输入如下命令: javah -classpath E:\workplace\java\HelloJNI\src com.hellojni.test.HelloJni classpath:是指定加载类的路径 com.hellojni.test.HelloJni:为完整类名。注意,不需要带java 具体javah的使用参数介绍,可以输入javah -help。 如果,执行成功,会在当前目录下生成com_hellojni_test_HelloJni.h的头文件。C++代码 /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_hellojni_test_HelloJni */ #ifndef _Included_com_hellojni_test_HelloJni #define _Included_com_hellojni_test_HelloJni #ifdef __cplusplus extern "C" { #endif /* * Class: com_hellojni_test_HelloJni * Method: printJni * Signature: ()V */ JNIEXPORT void JNICALL Java_com_hellojni_test_HelloJni_printJni(JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif 可以看到javah自动为我们生成了一个Java_com_hellojni_test_HelloJni_printJni的方法。格式是:Java_Packagename_Classname_Methodname。 首先,这里引入了jni.h的头文件。这个是jdk自带的一个头文件,一般在javapath/include(javapath为java安装目录)。 (4)打开VS新建一个Win32控制台应用程序,应用程序类型选择DLL(Win平台动态库为.dll)。并将生成的Java_com_hellojni_test_HelloJni_printJni.h头文件拷贝到该工程目录下。 然后,再将该头文件添加到工程中。如图: 编译生成一下。会提示找不到jni.h。因此,把jni.h拷贝到工程目录下,并加入到项目中。jni.h一般在javapath/include(javapath为java安装目录)路径下。 重新编译生成下,会提示找不到jni_md.h。这个文件在,javapath/include/win32路径下。拷贝该文件再加入工程。并修改Java_com_hellojni_test_HelloJni_printJni.h头文件。 将#include 修改为#include "jni.h",在当前目录下找jni.h头文件。 新建一个hellojni.cpp的源文件。如下:C++代码 #include "stdafx.h" #include <iostream> #include "com_hellojni_test_HelloJni.h" using namespace std; JNIEXPORT void JNICALL Java_com_hellojni_test_HelloJni_printJni(JNIEnv *env, jobject obj) { cout<<"Hello JNI"<<endl; } (5)再将工程重新生成下,成功的话,会在工程的Debug目录下生成一个HelloJni.dll的动态库。将HelloJni.dll所在的路径添加到环境变量,这样每次重新生成,在任意目录都能访问。 (6)在java工程中引用刚生成的HelloJni.dll。并加入如下代码:Java代码 package com.hellojni.test; public class HelloJni { public native void printJni(); public static void main(String[] args) { System.loadLibrary("HelloJni"); HelloJni hello = new HelloJni(); hello.printJni(); } } 调用System.loadLibrary来加载动态库。注意,动态库的名字不需要加.dll。 运行java工程,这时候会提示Exception in thread “main” java.lang.UnsatisfiedLinkError: no HelloJni in java.library.path。这时候,需要重启下Eclipse。因为,刚配置的环境变量。重启下,Eclipse才能识别。 重启完毕,运行java工程。控制台会输入: Hello JNI 表明整个JNI调用成功。 第一篇就介绍这么多,大体明白了JNI的整个开发流程及基本规则。下一篇将介绍下在Android NDK环境下的交叉编译及调用过程。阅读全文 »
Android游戏开发设计的步骤
分类标签: 游戏引擎
如今搭载Android操作系统的手机数量比iPhone多得多。据悉,Android设备平均每天激活40万台。但iOS对开发商来说依旧是个更加有利可图、更受欢迎的平台。原因是:Android无需花钱买应用;众多设备和应用商店使得Android市场呈分散状态。 1 手机游戏开发简介 游戏的本质就是在屏幕上不断地显示和更新图片,只不过不是胡乱地更新,而是根据程序逻辑来控制。 如今搭载Android操作系统的手机数量比iPhone多得多。据悉,Android设备平均每天激活40万台。但iOS对开发商来说依旧是个更加有利可图、更受欢迎的平台。原因是:Android无需花钱买应用;众多设备和应用商店使得Android市场呈分散状态。 1 手机游戏开发简介 游戏的本质就是在屏幕上不断地显示和更新图片,只不过不是胡乱地更新,而是根据程序逻辑来控制。一款完整的游戏需要多方面的知识,比如游戏的创意、背景、 故事情节、游戏音效,游戏风格、游戏类型、运行速度、适配机型等。而且,游戏的开发需要策划、美工、程序、测试的协同工作和默契配合完成的。 2 游戏框架设计 首先需要一个用于显示游戏界面的视图类,接着需要构建一个整个游戏逻辑类来控制当前屏幕显示哪个界面,甚至对界面进行一些逻辑上的处理。在创建和控制了视图显示之后,要让游戏能够动起来,需要开启一个线程来实时更新视图显示界面并刷新视图。 3 地图设计 通常游戏中的地图是多个小块组成的一个完整的大地图,而组成这些小块的数据一般可以使用一个二维数组来存储,然后通过程序以最快的方式将这些地图数据对应 的小块映射到屏幕上组成一幅完整的地图。当然,这些数据也不是我们从键盘上一个个地输入进去的,一般情况下先由程序员做一个地图编辑器,在这个地图编辑器 中用鼠标点击再保存,或者是从网络上下载一些成熟的编辑器,比如用mappy这样的工具生成地图,再用脚本语言为mappy写一个应该保持成什么格式的程 序。通常地图分为45度角、俯视角和侧视角。 4 主角设计 游戏中的主角在这里成为“精灵”,当然精灵包括的范围很广,不仅仅是主角,还有npc、道具等。既然是精灵,必然有很多动画,动画本身就是将图片一帧一帧 地连接起来,循环地播放每一帧形成的。同样可以使用自己编写的精灵编辑器去编辑精灵,将精灵拆成很多部分,然后再组合起来,这样可以节省大量的空间。精灵 类的特性,每次只能使用一个图像而不是多个图像来填充屏幕,可以有好几帧,但是一次只有一个显示。 5 图层管理器 只需要将所有图层(包括地图、主角)一起添加到图层管理器中,然后设置视图查看时的位置及大小,调用图层管理器的paint方法就可以绘制出图层。绘制的顺序是按添加的反顺序,既先添加的后绘制,以免图层被覆盖之后显示不出来。 6 游戏音效 首先我们将游戏中的音效分为如下几类:背景音乐、剧情音乐、音效(动作的音效、使用道具音效、辅助音效)等。背景音乐一般需要一直播放,而剧情音乐则只需要在剧情需要的时候播放,音效则是很短小的一段。 7 游戏存档 游戏存档就是将玩家当前游戏的进度等信息存储下来,在玩家再次进入游戏时可以通过读取上次的存档来接着上次的进度继续游戏。 (1).明确需要存储的数据 首先,为了再次游戏能够顺利地转载上次的进度,需要保存主角的一些属性(包括位置,生命,攻击,防御等),还需要保存当前地图的一些属性(比如行,列,当 前层数),同样还需要保存对话的相关内容,最后需要保存游戏的整个地图数据(每一层),还有当前的音乐状态。 (2).保存数据 获取存储的数据->将数据打包到properties中->将properties写入到文件中。 (3).装载数据 打开文件->将文件流装载进properties中->通过properties.get方法得到指定标签的数据-》将得到的数据赋值给应用程序中对应的变量。 在退出游戏时,不管玩家是否保存都将自动保存下来。阅读全文 »
Android游戏开发之打地鼠(五-终篇、游戏结束和数据存储)
分类标签: ListView
游戏的基本功能都已经实现了,最后来说一说排行榜的显示和游戏音效的添加。 排行榜的显示主要用的Android中一个比较重要的控件ListView。ListView的使用还是比较简单的,第一步在布局文件中建立一个ListView的节点,在代码中通过ID得到该控件。第二步给该控件设置一个适配器,适配器写一个类,该类继承BaseAdapter并实现未实现的方法,一共有4个为实现的方法 游戏的基本功能都已经实现了,最后来说一说排行榜的显示和游戏音效的添加。 排行榜的显示主要用的Android中一个比较重要的控件ListView。ListView的使用还是比较简单的,第一步在布局文件中建立一个ListView的节点,在代码中通过ID得到该控件。第二步给该控件设置一个适配器,适配器写一个类,该类继承BaseAdapter并实现未实现的方法,一共有4个为实现的方法,getCount()获得数据总数,getItem(int position)根据位置获得某条数据,getItemId(int position)根据位置获得某条数据id,getView(),得到相应位置的Item视图。可以通过contentView对ListView进行优化,如果contentView为空,通过inflate填充view,否则不填充,这样减少了填充次数,提高了效率。代码如下:Java代码 package cn.com.cyj.mouse.ui; import java.util.ArrayList; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.TextView; import cn.com.cyj.mouse.R; import cn.com.cyj.mouse.enity.Gamer; /** * 显示玩家排行榜 * * @author cyj * */ public class ShowRank extends BaseActivity { ListView lv; ArrayList<Gamer> gamerList; TextView gamerName; TextView gamerScore; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_showrank); gamerList = new ArrayList<Gamer>(); Intent intent = getIntent(); gamerList = (ArrayList<Gamer>) intent.getSerializableExtra("gamerlist"); // 初始化listview对象 lv = (ListView) findViewById(R.id.lv); // 给listview对象添加适配器 lv.setAdapter(new MyAdapter()); } class MyAdapter extends BaseAdapter { // 获得数据总数 @Override public int getCount() { return gamerList.size(); } // 根据位置获得某条数据 @Override public Object getItem(int position) { return null; } // 根据位置获得某条数据id @Override public long getItemId(int position) { // TODO Auto-generated method stub return 0; } @Override public View getView(int position, View contentView, ViewGroup parent) { View v = contentView; if (v == null) { // 通过inflate填充view v = View.inflate(ShowRank.this, R.layout.list_item, null); gamerName = (TextView) v.findViewById(R.id.gamername); gamerScore = (TextView) v.findViewById(R.id.gamerscore); } Gamer gamer = gamerList.get(position); gamerName.setText(gamer.getName()); gamerScore.setText(gamer.getScore() + ""); return v; } } } 一个游戏没有音效无可厚非,但是有了音效会更有乐趣。本游戏采用了MediaPlayer+SoundPool的形式,前者播放背景音乐,后者播放游戏的打击音效,关于MediaPlayer的使用,没有什么比官方的图来的更简单粗暴了,这里不再赘述。SoundPool的使用,第一步new SoundPool();第二步load(),通常用一个HashMap存放音乐文件id和load的映射;第三步play()。代码如下:Java代码 package cn.com.cyj.mouse.services; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.SoundPool; import cn.com.cyj.mouse.R; /** * 处理游戏的背景音乐和音效 * @author cyj * */ public class MusicService { // 用来播放背景音乐 MediaPlayer player; // 用来播放音效 SoundPool pool; Context context; // 存放音效 Map<Integer, Integer> soundMap; public MusicService(Context context) { this.context = context; initMedia(); initSound(); } private void initSound() { pool = new SoundPool(3, AudioManager.STREAM_MUSIC, 0); soundMap = new HashMap<Integer, Integer>(); // 加载音效文件并放入hashmap中 soundMap.put(R.raw.dismistake, pool.load(context, R.raw.dismistake, 1)); soundMap.put(R.raw.hathit, pool.load(context, R.raw.hathit, 1)); } /** * 通过resId播放在hashmap中对应的要播放的音效 * @param resId hashMap的key */ public void playSound(int resId){ // 获得map的value即对应音效 Integer soundId = soundMap.get(resId); if(soundId != null){ pool.play(soundId, 1, 1, 1, 0, 1); } } private void initMedia(){ // 第一次播放不用prepare player = MediaPlayer.create(context, R.raw.bg); // 循环播放 player.setLooping(true); } public void play() { // 播放背景音乐 player.start(); } /** * 停止播放 */ public void stop() { if (player.isPlaying()) { player.stop(); try { player.prepare(); // stop后再次start会继续播放,设置从0开始播放 player.seekTo(0); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * 暂停播放 */ public void pause(){ if(player.isPlaying()){ player.pause(); } } /** * 游戏退出释放资源 */ public void close() { if(player == null) return ; if(player.isPlaying()){ // 停止播放 player.stop(); } // 释放资源,无法使用。mediaPlayer引用还在 player.release(); player = null; } /** * 继续播放音乐 */ public void continuePlay() { player.start(); } /** * 判断背景音乐是否在播放 * @return */ public Boolean isNowPlay(){ if(player.isPlaying()){ return true; } return false; } } 游戏中的控制类,该类处理玩家对界面的操作和游戏响应,把界面操作和游戏逻辑分开设计,降低耦合性。代码如下:Java代码 package cn.com.cyj.mouse.controller; import java.util.ArrayList; import android.content.Context; import android.content.Intent; import android.os.Bundle; import cn.com.cyj.mouse.database.GamerDatabase; import cn.com.cyj.mouse.enity.Gamer; import cn.com.cyj.mouse.services.MusicService; import cn.com.cyj.mouse.ui.ShowRank; /** * 游戏中的控制类 * @author cyj * */ public class Controller { ArrayList<Gamer> gamerList; GamerDatabase gamerDatabase; Context context; MusicService musicService; public Controller(Context context) { this.context = context; gamerDatabase = new GamerDatabase(context); musicService = new MusicService(context); gamerList = new ArrayList<Gamer>(); } /** * 插入数据 * * @param gamer * 玩家对象 * @return true 插入玩家成功;false 插入玩家失败 */ public Boolean insert(Gamer gamer) { if (gamerDatabase.insertGamer(gamer)) return true; return false; } /** * 查询所有玩家信息 * * @return true 有玩家信息; false 没有玩家信息 */ public Boolean query() { gamerList = gamerDatabase.queryGamerAll(); if (gamerList.size() == 0) return false; Intent intent = new Intent(context, ShowRank.class); Bundle bundle = new Bundle(); // 装入被序列化的玩家信息列表,将数据传到新的Activity bundle.putSerializable("gamerlist", gamerList); intent.putExtras(bundle); intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); context.startActivity(intent); return true; } /** * 播放音乐 */ public void play() { musicService.play(); } /** * 关闭音乐 */ public void stop() { musicService.stop(); } /** * 暂停音乐 */ public void pauseMusic() { musicService.pause(); } /** * 继续播放 */ public void continuePlay() { musicService.continuePlay(); } /** * 游戏结束释放资源 */ public void close() { musicService.close(); } /** * 播放音效 * @param resId 音乐文件资源id */ public void playSound(int resId) { musicService.playSound(resId); } /** * 判断音乐是否在播放 * @return true音乐正在播放,false音乐已经停止 */ public Boolean isPlay(){ if(musicService.isNowPlay()){ return true; } return false; } } 阅读全文 »
Android游戏开发之打地鼠(四、游戏结束和数据存储)
分类标签: SQLite
游戏结束弹出保存玩家姓名和分数的窗口,玩家输入姓名后点击确定保存到数据库中。玩家可以通过主界面的排行榜可以查看到分数从高到低排行的榜单。 建立一个玩家类用来处理玩家的信息,该类实现类序列化接口,实例可以被序列化便于数据的传递。 游戏结束弹出保存玩家姓名和分数的窗口,玩家输入姓名后点击确定保存到数据库中。玩家可以通过主界面的排行榜可以查看到分数从高到低排行的榜单。 建立一个玩家类用来处理玩家的信息,该类实现类序列化接口,实例可以被序列化便于数据的传递。代码如下:Java代码 package cn.com.cyj.mouse.enity; import java.io.Serializable; /** * 玩家信息类,可以被序列化 * @author cyj * */ public class Gamer implements Serializable{ // 玩家姓名 private String name; // 玩家分数 private int score; public Gamer(String name, int score) { this.name = name; this.score = score; } public String getName() { return name; } public int getScore() { return score; } @Override public String toString() { return "Gamer [name=" + name + ", score=" + score + "]"; } } 游戏窗口是一个Activity,将主题样式设置成Dialog即在清单文件中GameOver设置属性android:theme="@android:style/Theme.Dialog"。代码如下:Java代码 package cn.com.cyj.mouse.services; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import cn.com.cyj.mouse.R; import cn.com.cyj.mouse.enity.Gamer; import cn.com.cyj.mouse.ui.BaseActivity; import cn.com.cyj.mouse.ui.MouseStart; /** * 游戏结束窗口 * @author cyj * */ public class GameOver extends BaseActivity { // 确定按钮 Button confirm; // 取消按钮 Button cancel; // 姓名输入框 EditText name; // 显示分数 TextView finalScore; Intent intent; // 玩家的分数 int nowScore; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gameover); init(); intent = getIntent(); nowScore = Integer.parseInt(intent.getStringExtra("score")); // 显示分数 finalScore.setText(nowScore+""); // 设置点击其他位置不关闭窗口 ,最低版本API11 this.setFinishOnTouchOutside(false); } /** * 初始化组件 */ private void init() { confirm = (Button) findViewById(R.id.confirm); cancel = (Button) findViewById(R.id.cancel); name = (EditText) findViewById(R.id.name); finalScore = (TextView) findViewById(R.id.finalscore); /** * 给按钮设置点击事件 */ confirm.setOnClickListener(new MyOnClick()); cancel.setOnClickListener(new MyOnClick()); } class MyOnClick implements OnClickListener{ @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.confirm: // 判断输入内容是否为空 if(!TextUtils.isEmpty(name.getText().toString())){ Gamer gamer = new Gamer(name.getText().toString(), nowScore); // 添加数据到数据库 MouseStart.controller.insert(gamer); // 关闭窗口 finish(); }else{ Toast.makeText(GameOver.this, "姓名不能为空", Toast.LENGTH_SHORT).show(); } break; case R.id.cancel: finish(); break; default: break; } } } } 数据库中存储玩家的姓名和分数,建立MouseSqlite类通过继承SQLiteOpenHelper来建立数据库和处理数据库升级等操作。Java代码 package cn.com.cyj.mouse.database; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; /** * 玩家信息数据库的api * @author cyj * */ public class MouseSqlite extends SQLiteOpenHelper { public MouseSqlite(Context context) { super(context, "gamer.db", null, 1); } /** * 创建gamer.db表 */ @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table gamer (id integer primary key autoincrement, name varchar(20), score integer);"); } @Override public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) { } } 本游戏对数据库的操作主要有添加一条玩家信息和查询所有玩家信息,查询的时候按照分数从高到低排列。对数据库操作一定要关闭数据库。代码如下:Java代码 package cn.com.cyj.mouse.database; import java.util.ArrayList; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import cn.com.cyj.mouse.enity.Gamer; /** * 对gamer表进行操作,数据库使用完毕必须关闭 * @author cyj * */ public class GamerDatabase { MouseSqlite mouseSqlite; SQLiteDatabase db; // 所有玩家信息 ArrayList<Gamer> gamerList; public GamerDatabase(Context context) { mouseSqlite = new MouseSqlite(context); } /** * 插入一条数据 * @param gamer 玩家信息 * @return */ public Boolean insertGamer(Gamer gamer){ // 获得可写数据库 db = mouseSqlite.getWritableDatabase(); // 用于保存玩家信息到数据库 ContentValues values = new ContentValues(); String name = gamer.getName(); int score = gamer.getScore(); values.put("name", name); values.put("score", score); // 插入数据到数据库 long res = db.insert("gamer", null, values); // 关闭数据库 db.close(); if(res != -1) return true; return false; } /** * 查询所有数据 * @return 所有玩家信息 */ public ArrayList<Gamer> queryGamerAll(){ // 获得可读数据库 db = mouseSqlite.getReadableDatabase(); gamerList = new ArrayList<Gamer>(); // 查询所有玩家信息,按分数从高到低排序 Cursor cursor = db.query("gamer", null, null, null, null, null, "score desc"); while(cursor.moveToNext()){ // 获得当前游标所指向数据的姓名 String name = cursor.getString(cursor.getColumnIndex("name")); // 获得当前游标所指向数据的分数 int score = cursor.getInt(cursor.getColumnIndex("score")); Gamer gamer = new Gamer(name, score); // 添加到集合里 gamerList.add(gamer); } // 关闭数据库 db.close(); return gamerList; } }阅读全文 »
Android游戏开发之打地鼠(三、打地鼠设计实现)
分类标签: Layout TextView
上篇文章中对开始打地鼠游戏的思路做了简单的介绍,现在来具体的说一说开始打地鼠游戏的实现,先说说布局,用LinearLayout或TableLyout都可以。上面一行是4个TextView下面的地洞是ImageButton。游戏中打中或没打中地鼠都更新会对应按钮背景图。打中地鼠的效果图(图1)和没打中的效果图(图2)。 上篇文章中对开始打地鼠游戏的思路做了简单的介绍,现在来具体的说一说开始打地鼠游戏的实现,先说说布局,用LinearLayout或TableLyout都可以。上面一行是4个TextView下面的地洞是ImageButton。游戏中打中或没打中地鼠都更新会对应按钮背景图。打中地鼠的效果图(图1)和没打中的效果图(图2)。 游戏中需要开启一个线程来控制游戏时间,更新显示剩余时间时,游戏0.5s更新一次,游戏时间为30s,因此更新次数是游戏时间的二倍。当游戏时间为0s时游戏停止关闭游戏界面并开启记录玩家信息的窗口。该线程同时产生一个随机数(1-12)来指定地鼠出现位置,由于子线程不能更新UI,需要通过handler发送消息来更新UI。更新界面时将每一个按钮背景都有重置为地洞,再更新地鼠出现位置的图片,这样会清除由于点击出现的锤子和上一次地鼠出现位置设置的图片。当用户点击屏幕是,如果打中地鼠,效果如图1,没打中效果如图2,并且如果开启了音效,会播放不同的打击声音。最后在游戏界面不可见是关闭线程。 代码如下:Java代码 package cn.com.cyj.mouse.services; import java.util.HashMap; import java.util.Random; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageButton; import android.widget.TextView; import cn.com.cyj.mouse.R; import cn.com.cyj.mouse.ui.BaseActivity; import cn.com.cyj.mouse.ui.MouseStart; /** * 游戏开始的界面:游戏中有12个ImageButton,每个ImageButton背景设置成地鼠洞,游戏中开启一个线程控制游戏时间 * * @author cyj * */ public class GameRun extends BaseActivity { /** * 线程睡眠时间 */ public static final int THREAD_SLEEP_TIME = 500; /** * 游戏时间 */ public static final int TIME = 30; private ImageButton one; private ImageButton two; private ImageButton three; private ImageButton four; private ImageButton five; private ImageButton six; private ImageButton seven; private ImageButton eight; private ImageButton nine; private ImageButton ten; private ImageButton eleven; private ImageButton twleve; // 显示时间 private TextView showTime; // 显示分数 private TextView score; MyClick click; private Random random; // 游戏当前时间 private int time; // 游戏总时间 private int totalTime; // 老鼠下一次出现位置 private int next; // 游戏当前分数 private int nowScore; // 游戏线程 private Thread t; // 存放按钮和next的映射 HashMap<ImageButton, Integer> battle; HashMap<Integer, ImageButton> nextMap; public Handler handler = new Handler() { public void handleMessage(android.os.Message msg) { changeUI(); }; }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gamerun); battle = new HashMap<ImageButton, Integer>(); nextMap = new HashMap<Integer, ImageButton>(); initImageButton(); initOnClick(); initbattleMap(); initNextMap(); next = -1; random = new Random(); totalTime = TIME; time = 0; nowScore = 0; showTime.setText(TIME + ""); } @Override protected void onResume() { super.onResume(); if(t == null){ // 控制游戏时间 t = new Thread(new Runnable() { @Override public void run() { try { while (totalTime != 0) { Thread.sleep(THREAD_SLEEP_TIME); next = random.nextInt(12) + 1; time++; handler.sendEmptyMessage(1); } } catch (Exception e) { e.printStackTrace(); } if (totalTime == 0) { Intent intent = new Intent(GameRun.this, GameOver.class); // 该参数跳转页面不会触发onUserLeaveHint()方法 intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); intent.putExtra("score", "" + nowScore); GameRun.this.startActivity(intent); finish(); } } }); } t.start(); } // 初始化按钮 private void initImageButton() { one = (ImageButton) findViewById(R.id.first); two = (ImageButton) findViewById(R.id.second); three = (ImageButton) findViewById(R.id.three); four = (ImageButton) findViewById(R.id.four); five = (ImageButton) findViewById(R.id.five); six = (ImageButton) findViewById(R.id.six); seven = (ImageButton) findViewById(R.id.seven); eight = (ImageButton) findViewById(R.id.eight); nine = (ImageButton) findViewById(R.id.nine); ten = (ImageButton) findViewById(R.id.ten); eleven = (ImageButton) findViewById(R.id.eleven); twleve = (ImageButton) findViewById(R.id.twelve); showTime = (TextView) findViewById(R.id.showtime); score = (TextView) findViewById(R.id.score); } // 给按钮添加点击事件 private void initOnClick() { click = new MyClick(); one.setOnClickListener(click); two.setOnClickListener(click); three.setOnClickListener(click); four.setOnClickListener(click); five.setOnClickListener(click); six.setOnClickListener(click); seven.setOnClickListener(click); eight.setOnClickListener(click); nine.setOnClickListener(click); ten.setOnClickListener(click); eleven.setOnClickListener(click); twleve.setOnClickListener(click); } // 按钮id和next映射关系 private void initbattleMap() { battle.put(one, 1); battle.put(two, 2); battle.put(three, 3); battle.put(four, 4); battle.put(five, 5); battle.put(six, 6); battle.put(seven, 7); battle.put(eight, 8); battle.put(nine, 9); battle.put(ten, 10); battle.put(eleven, 11); battle.put(twleve, 12); } // next和按钮id的映射关系 private void initNextMap() { nextMap.put(1, one); nextMap.put(2, two); nextMap.put(3, three); nextMap.put(4, four); nextMap.put(5, five); nextMap.put(6, six); nextMap.put(7, seven); nextMap.put(8, eight); nextMap.put(9, nine); nextMap.put(10, ten); nextMap.put(11, eleven); nextMap.put(12, twleve); } /** * 更新小老鼠出现位置和显示游戏剩余时间 */ private void changeUI() { // 更新显示剩余时间,游戏0.5s更新一次,因此更新次数是游戏时间的二倍 if (time % 2 == 0) { showTime.setText(--totalTime + ""); } if (next == -1) return; // 每次出地鼠时将按钮背景初始化 reImageButton(); // 获得next对应的按钮 ImageButton bt = nextMap.get(next); // 给按钮设置地鼠图片 bt.setBackgroundResource(R.drawable.end); } // 按钮背景初始化 private void reImageButton() { one.setBackgroundResource(R.drawable.start); two.setBackgroundResource(R.drawable.start); three.setBackgroundResource(R.drawable.start); four.setBackgroundResource(R.drawable.start); five.setBackgroundResource(R.drawable.start); six.setBackgroundResource(R.drawable.start); seven.setBackgroundResource(R.drawable.start); eight.setBackgroundResource(R.drawable.start); nine.setBackgroundResource(R.drawable.start); ten.setBackgroundResource(R.drawable.start); eleven.setBackgroundResource(R.drawable.start); twleve.setBackgroundResource(R.drawable.start); } /** * 点击事件,判断是否打中 * * @author cyj * */ class MyClick implements OnClickListener { @Override public void onClick(View v) { // 是否的分的标记 Boolean isScore = false; // 获取点击按钮对应next int battleId = battle.get(v); // 如果点击按钮为next得分 if (battleId == next) { // 得分为true isScore = true; } if (isScore) { // 设置打中的图片 v.setBackgroundResource(R.drawable.zhong); if (MouseStart.controller.isPlay()) { // 打中的音效 MouseStart.controller.playSound(R.raw.hathit); } // 加分 score.setText((nowScore += 10) + ""); } else { // 设置没打中的图片 v.setBackgroundResource(R.drawable.meizhong); if (MouseStart.controller.isPlay()) { // 没打中的音效 MouseStart.controller.playSound(R.raw.dismistake); } } } } @Override protected void onStop() { // 停止线程 super.onStop(); if (t != null) { t.interrupt(); t = null; } } } 小技巧:在本游戏中免不了各种判断:在产生地鼠位置时,要通过随机数来确定按钮位置,例如:如果随机数是1,则在第一个按钮设置地鼠图片,需要多次判断,应该是出现随机数1那么直接在第一个按钮设置;玩家点击按钮时,需要判断点击的是那个按钮并判断是否是有地鼠的按钮,再设置相应的图片。那么应该怎么办呢? 答案是用HashMap来代替switch,在HashMap来映射随机数和按钮,更新地鼠位置时直接用随机数来获得按钮设置图片。用另一个HashMap中映射按钮和对应的随机数,这样在点击按钮时直接获取映射的数,该数和现在地鼠位置的随机数比较即可判断是否打中地鼠;这样就完美解决了。阅读全文 »
Android游戏开发之打地鼠(二、游戏设计和主界面设计)
分类标签: Activity
游戏设计思路: 主界面点击开始游戏:进入打地鼠界面游戏中有12个地洞,游戏时间为30s(可以自己设置),每0.5s会有地鼠随机出现在一个地洞中,玩家触摸屏幕,打到地鼠加10分,否则不加分。30s后游戏结束,弹出窗口显示获得分数,需要玩家输入姓名后,点击确定保存到本地数据库中。 设计实现:每个地洞为一个ImageButton,开始设置背景为地洞图片,地鼠出现则设置为地鼠图片,给每个按钮添加点击事件 游戏设计思路: 主界面点击开始游戏:进入打地鼠界面游戏中有12个地洞,游戏时间为30s(可以自己设置),每0.5s会有地鼠随机出现在一个地洞中,玩家触摸屏幕,打到地鼠加10分,否则不加分。30s后游戏结束,弹出窗口显示获得分数,需要玩家输入姓名后,点击确定保存到本地数据库中。 设计实现:每个地洞为一个ImageButton,开始设置背景为地洞图片,地鼠出现则设置为地鼠图片,给每个按钮添加点击事件,当玩家点击按钮时,如果打到地鼠,该按钮设置打中地鼠图片,否则设置没打中地鼠的图片。游戏结束开启记录窗口,记录玩家信息。 主界面点击排行榜:如果没有记录,提示暂无排行,有记录就跳转界面,按分数从高到低显示玩家信息。 设计实现:通过对数据库的查询操作,返回一个ArrayList,如果ArrayList长度为0,则提示“暂无排行”,否则开启一个新的Activity显示玩家信息。 主界面点击关于:显示游戏的相关信息。 设计实现:Activity跳转。 主界面点击退出:游戏退出 设计实现:调用finish()函数。 主界面点击音乐图标:游戏打开默认播放音乐,点击图标背景音乐和音效会关闭,再次点击会播放背景音乐和音效。 设计实现:一个ToggleButton(开关按钮)背景设置成音乐图标,点击会触发响应事件。 按物理返回键游戏停止,在onDestroy()方法中做释放资源等操作。 注:游戏中所写的Activity继承BaseActivity,自己实现的一个继承Activity的类。那么为什么要实现这么一个类呢?在游戏的后期添加音效时,程序进入后台,背景音乐会一直播放,因为背景音乐在所有的Activity中都会播放,所以要在每个Activity的生命周期的回调函数中对音乐操作无疑是比较麻烦的,所以继承自一个我们自己实现的BaseActivity,只需要在BaseActivity中来操作即可。 BaseActivity的代码如下:Java代码 package cn.com.cyj.mouse.ui; import android.app.Activity; import android.os.Bundle; /** * * @author cyj * */ public class BaseActivity extends Activity { // 音乐播放标记 protected Boolean isLive = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onResume() { super.onResume(); // 如果进入后台前音乐是播放的,进入前台时继续播放 if (isLive) { MouseStart.controller.continuePlay(); } } // 进入后台时系统调用 @Override protected void onUserLeaveHint() { super.onUserLeaveHint(); // 如果音乐在播放就暂停 if (MouseStart.controller.isPlay()) { MouseStart.controller.pauseMusic(); isLive = true; } else { isLive = false; } } } 主界面代码如下:Java代码 package cn.com.cyj.mouse.ui; import android.content.Intent; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.widget.ImageButton; import android.widget.Toast; import android.widget.ToggleButton; import cn.com.cyj.mouse.R; import cn.com.cyj.mouse.controller.Controller; import cn.com.cyj.mouse.services.GameRun; /** * 游戏主界面 * * @author cyj * */ public class MouseStart extends BaseActivity { // 开始游戏按钮 ImageButton start; // 排行榜按钮 ImageButton rank; // 关于按钮 ImageButton about; // 退出按钮 ImageButton exit; // 音乐开关 ToggleButton music; Intent intent; // 只有能一个controller定义成静态共别的Activity调用,之前在BaseActivity创建controller的对象每个子类都会有一个controller,出现问题 public static Controller controller; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gamestart); controller = new Controller(this); /* * 初始化各个按钮 */ start = (ImageButton) findViewById(R.id.startgame); rank = (ImageButton) findViewById(R.id.rank); about = (ImageButton) findViewById(R.id.about); exit = (ImageButton) findViewById(R.id.exit); music = (ToggleButton) findViewById(R.id.musical); /* * 给每个按钮添加点击事件 */ GameStartOnClick game = new GameStartOnClick(); start.setOnClickListener(game); start.setOnTouchListener(game); rank.setOnClickListener(game); rank.setOnTouchListener(game); about.setOnClickListener(game); about.setOnTouchListener(game); exit.setOnClickListener(game); exit.setOnTouchListener(game); music.setOnClickListener(game); // 游戏开启默认播放背景音乐 controller.play(); } class GameStartOnClick implements OnClickListener, OnTouchListener { @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.startgame: // 进入开始游戏Activity intent = new Intent(MouseStart.this, GameRun.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); MouseStart.this.startActivity(intent); break; case R.id.rank: // 通过控制类对象查询全部玩家信息 if (!controller.query()) { Toast.makeText(MouseStart.this, "暂无排行", Toast.LENGTH_SHORT) .show(); } break; case R.id.about: // 打开关于Activity intent = new Intent(MouseStart.this, About.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); startActivity(intent); break; case R.id.exit: // 关闭Activity finish(); break; case R.id.musical: if (music.isChecked()) { controller.stop(); } else { controller.play(); } break; default: break; } } /** * 设置按钮按下和抬起的效果 */ @Override public boolean onTouch(View v, MotionEvent event) { int id = v.getId(); switch (id) { case R.id.startgame: if (event.getAction() == MotionEvent.ACTION_DOWN) { start.setBackgroundResource(R.drawable.startgamean); } if (event.getAction() == MotionEvent.ACTION_UP) { start.setBackgroundResource(R.drawable.startgame); } break; case R.id.rank: if (event.getAction() == MotionEvent.ACTION_DOWN) { rank.setBackgroundResource(R.drawable.rankan); } if (event.getAction() == MotionEvent.ACTION_UP) { rank.setBackgroundResource(R.drawable.rank); } break; case R.id.about: if (event.getAction() == MotionEvent.ACTION_DOWN) { about.setBackgroundResource(R.drawable.aboutan); } if (event.getAction() == MotionEvent.ACTION_UP) { about.setBackgroundResource(R.drawable.about); } break; case R.id.exit: if (event.getAction() == MotionEvent.ACTION_DOWN) { exit.setBackgroundResource(R.drawable.exitan); } if (event.getAction() == MotionEvent.ACTION_UP) { exit.setBackgroundResource(R.drawable.exit); } break; default: break; } return false; } } @Override protected void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); controller.close(); } } 顺便贴一下About中的代码:这个类比较简单直接加载对应的xml文件就可以,一些介绍的话在xml中写。Java代码 package cn.com.cyj.mouse.ui; import android.os.Bundle; import cn.com.cyj.mouse.R; /** * 关于界面 * @author cyj * */ public class About extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); } }阅读全文 »
Android游戏开发之打地鼠(一、需求分析与设计)
分类标签: Activity ListView Drawable
最近做了这个小作品,并不是规范的开发,只是完成了部分功能,和市面上的作品不能相提并论,游戏中所用到的图片和音效均来自于网络。先来看看需求分析以及效果图: 游戏的第一界面有5个按钮,开始游戏,排行榜,关于,退出和音乐图标,点击开始游戏即进入打地鼠游戏。 游戏结束后会进入玩家记录窗口。 点击排行榜查看游戏玩家记录。 最近做了这个小作品,并不是规范的开发,只是完成了部分功能,和市面上的作品不能相提并论,游戏中所用到的图片和音效均来自于网络。先来看看需求分析以及效果图: 游戏的第一界面有5个按钮,开始游戏,排行榜,关于,退出和音乐图标,点击开始游戏即进入打地鼠游戏。 游戏结束后会进入玩家记录窗口。 点击排行榜查看游戏玩家记录。 点击关于可以看到对游戏的说明,点击退出则游戏退出。 根据分析创建如下包和类以及xml文件。 Controller类作为游戏的控制中心,处理界面,玩家点击和游戏逻辑;GamerDatabase用来保存玩家记录到数据库;MouseSqlite创建数据库等;Gamer封装玩家信息;GameOver游戏结束的处理;gameRun游戏运行时的处理;MusicService处理游戏的背景音乐以及音效;About游戏的关于界面;BaseActivity作为基类所有的Activity都要继承它;showRank显示玩家记录的排行榜。xml文件名称对应相应的类的布局,list_item用于显示排行榜的listview的布局。游戏中使用到的图片和音效分别放在res下的drawable和raw下。阅读全文 »
23天从0开始完成一款Android游戏开发 – 第21~23天
分类标签: 游戏引擎
第21天:Android游戏的商业化 我做这个游戏只是一个尝试,没指望它挣很多的钱。游戏可以免费下载,如果你愿意可以购买游戏里的金币。我喜欢这种类似布丁怪兽的游戏方式,玩家不用付费就能体验游戏的全部内容。玩Drone Invaders时不用支付一毛钱,它不是付费取胜的游戏。即便如此,那些不愿意等待的玩家可以通过购买金币让武器立即就绪。 第21天:Android游戏的商业化 我做这个游戏只是一个尝试,没指望它挣很多的钱。游戏可以免费下载,如果你愿意可以购买游戏里的金币。我喜欢这种类似布丁怪兽的游戏方式,玩家不用付费就能体验游戏的全部内容。玩Drone Invaders时不用支付一毛钱,它不是付费取胜的游戏。即便如此,那些不愿意等待的玩家可以通过购买金币让武器立即就绪。 我很顺利地用25美金开了一个Google Play账号。我阅读过一些博客都建议购买一个Google Play的推广服务用来解决市场饱和的问题,这个需要5000美金的注册费用。开发者需要谨慎选择。我不太确定是否需要支付这5000美金,在我看来只有那些在平台上挣着钱的大工作室才需要吧。 不管怎样,创建一个谷歌商业账户在我的国家是不可能的事情。这时还有什么其他选择呢?你可以使用Fortumo或Centili的服务来支付,然而这会带来两个问题: 1. 这种做法违背了Google Play的使用条款。你的账户会面临被停用的风险,或者只能发布到其他安卓应用商店(我知道的市场有30个以上。统计显示他们大约占了整个安卓市场的30%,在我看来这个比例似乎太高了)。 2. 为了支持手机短信支付功能,你的游戏需要请求两个系统权限。有的用户可能会不喜欢应用“发送和接收短信”。尽管在一些国家这是让游戏赚钱的唯一办法,但如果你的目标是为美国市场这绝对是一个坏点子。 第三种选择是在美国开一间公司,然后注册一个账户。如果希望通过游戏挣取大量的财富,这无疑是最正确的做法。但拥有一家美国公司也需要一些费用。有专门的机构可以在美国特拉华州帮你注册公司。特拉华州是美国的避税天堂,你可以随时从公司账户把挣得的财富转移到自己的户头,只要付一笔转账税即可。至于转让,这是我第一个游戏,我甚至还不知道它能不能挣钱,所以目前还没准备到那一步。 幸运的是,我有个朋友住在美国。我们以他的名义开了一个账户,并用这个帐户推出游戏。至少谷歌商业帐户对美国公民还是开放的。我还是不明白为什么谷歌商业账户不开放给我的国家,但通过谷歌AdSense赚钱却没有任何问题。我想可能是谷歌各部门的政策各有不同吧。 第22天:声音效果 尽管我有一个多年积累下来的WAV SFX文件音乐素材库,但是看到别的游戏开发者都使用Bfxr创建自己的音效,我决定也尝试一下。 这是个非常棒的决定,因为你拥有这些音效100%的版权,所以无需证明从哪里获取这些音频文件。有些SFX文件过去下载的网站可能已经不存在了,又或者联系到作者不太现实。例如,我最喜欢的MOD音乐Aspirating Milk。我在modarchive上发现了它,但当我试着按照联系信息发e-mail过去联系作者,却没有得到答复。可能这是他大学时创作的或者有什么别的原因。 虽然我喜欢在Linux上开发,但Bfxr只支持Windows或Mac操作系统。我想可以通过Wine运行,但最后没有朝这个方向努力,因为我发现了这个: http://www.superflashbros.net/as3sfxr/ 这是一个虽然简单却超级好用的Flash程序,可以在浏览器上运行。强烈推荐! 这些SFX程序产生各种简单的音效。一些复杂的音效可以通过Audacity程序、混合几个简单的效果来完成。下图是我创建原子弹通电音效的SFX: 顺便说一句,游戏基本上开发完成,正在各种安卓设备上进行测试。可能明天会在Goole Play商店发布。 第23天:发布! 我的游戏大功告成,可用从谷歌Play上下载: https://play.google.com/store/apps/details?id=com.bigosaur.backyardpanic阅读全文 »
23天从0开始完成一款Android游戏开发 – 第18~20天
分类标签: 游戏引擎
第18天:外星人图形与圆形冲突、完美的子弹轨迹 今天我受够了“射击月亮”bug。有时候外星人即使在屏幕中出现,也可能射不中。我做了大量测试,在屏幕上布满外星人并且设置月亮半透明以定位这个bug的原因。我发现测试击中区域的坐标偏移了一个bit位,但即使解决了这个问题原先的bug依然存在。外星人图形不能简单用圆形覆盖,否则玩家要么射不到外星人,要么会射到隐蔽在月亮下的外 第18天:外星人图形与圆形冲突、完美的子弹轨迹 今天我受够了“射击月亮”bug。有时候外星人即使在屏幕中出现,也可能射不中。我做了大量测试,在屏幕上布满外星人并且设置月亮半透明以定位这个bug的原因。我发现测试击中区域的坐标偏移了一个bit位,但即使解决了这个问题原先的bug依然存在。外星人图形不能简单用圆形覆盖,否则玩家要么射不到外星人,要么会射到隐蔽在月亮下的外星人。 所以我决定使用圆形检查。由于月亮比外星人大很多,能够很容易地检查外星人图形边缘的四个点是否都在圆形月亮内。为了测试,我使用libGDX内置的ShapeRender类,具体的实现代码如下:Java代码 shapeRenderer.setProjectionMatrix(camera.combined); shapeRenderer.begin(ShapeType.Circle); shapeRenderer.setColor(1, 1, 1, 1); shapeRenderer.circle(sMoon.getX() + 119, sMoon.getY() + 116, 167); shapeRenderer.end(); 上面的代码加在SpriteBatch完成以后,沿着月亮表面画白色的圆圈。类似地,给外星人边界画上长方形。 测试一个点是否在圆内的高效方法不是计算平方根(速度较慢)而是比较距离的平方。libGDX的内置函数Circle.contains(x,y)恰好实现了这个功能,所以我使用了这个函数进行检查。事实证明这个方法非常有效。我为半径长度增加了一些像素值,因为所有外星人之间会有一些间隔。改动后的结果令我非常满意。 完美的子弹轨迹 在这个游戏中,子弹是从距离屏幕下方50像素值的地方发射的。我使用了函数atan2让子弹旋转着击中目标,但我的代码中有一些错误,在没有射中目标时错误会经常出现。为了理解这部分内容,请注意在这个游戏所有的射击都采用了HitScan策略。 译注:HitScan与射击目标相对,指的是射击出的子弹不针对任何目标而是摧毁子弹运行轨迹上的任何物体。 在没有射中目标时,现在的代码将子弹轨迹延伸到屏幕尽头,而以前的代码把尽头设置得太远。由于子弹的飞行使用了中间位置,结果看上去有很大的跳跃并且在子弹射出屏幕之前只能看到2、3个点。通过把结束点设置到屏幕的边缘来解决了这个问题,现在你能清楚地看到子弹在飞行。 这时又暴露出另外一个问题:子弹有时候距离玩家接触的屏幕点只有10到20个像素点。导致这个问题有三个原因。第一个问题,我使用了子弹的X坐标和Y坐标。由于这个坐标位于屏幕底部的角落。通过把子弹的中心坐标加上一半的宽和高解决了这个问题。但仍有一些子弹没有射中。第二个问题,我忘记设置原点,所以子弹围绕着左下角进行旋转。这个问题也解决了,但仍有一些朝屏幕左边射射出的子弹没有射中。 第三个问题,我意识到当子弹旋转时宽度和高度是在变化的,所以子弹的中心点需要在旋转后需要重新计算。解决了这个问题,子弹就能正确地从玩家触摸的地方射击。修改后的代码如下:Java代码 // 子弹飞行 LaserBullet lb = new LaserBullet(tUI, 65, 64, 20, 40); lb.setPosition(0, -450); lb.setOrigin(10, 20); lb.setRotation( (float)(Math.atan2(-x, 450f+y) * 180f / Math.PI) ); Rectangle r = lb.getBoundingRectangle(); x = (int)(x - r.width * 0.5f); y = (int)(y - r.height * 0.5f); lb.target.set(x, y); bullets.add(lb); Tween.to(lb, SpriteTweenAccessor.POSITION_XY, delay).target(x, y).start(tweenManager); 第19天:每日挑战和任务 每日挑战是收集5个字母,操作方式和道具一样。一旦收集了所有字母,就可以得到一些用于购买道具的游戏币。这是一个通过玩游戏获取硬币的简单方法,这个灵感是受到“地铁跑酷”(Subway Surfers)的启发。 任务由许多子任务组成,通过完成这些子任务可以赚取硬币。硬币可以用于购买升级道具和消费物质,如盔甲、炸弹等等。每天的任务由三部分组成,你必须完成所有三项子任务才能获得奖励。 我发现使用内置的文本换行来显示任务比较简单。然而行高会显得过大,而且直接修改代码没有办法减小行高。因此我选择编辑由BMFont生成的.fnt文件,进行如下调整: lineHeight=33 变成 lineHeight=23 在开始生成位图时,我在字母的四周增加了5个像素的阴影,所以现在需要把高度减少了10像素(上面减少5像素,下面减少5像素)。 在为此查找文档时,我发现了一些先前遗漏的问题:在为游戏选择字体时,可能数字看起来效果不是很好。数字1看起来很修长,而数字11看起来很奇怪。要解决这个问题,可以为图中的字体设置固定宽度。 font.setFixedWidthGlyphs("0123456789"); 这样效果看起来会非常好。但由于已经决定使用修长字体,因而没有采用固定宽度。 第20天:周挑战、用户数据持久化、Java日期灾难 周挑战是在一周内收集特定数目的星星,从而获得一些优异的奖励,如8个原子弹、5个盔甲等等。我用Gimp做了一个很棒的金色星星并在尝试了不同的闪烁和星光效果,但是这些看上去效果不是特别好。所以我想到了强化道具的粒子效果,对它进行改变直到满足星星的要求。星星有了自己的闪烁节奏,而且可以在屏幕上同时显示星星和强化道具。 我还添加了玩家数据的加载和保存。这个比我想象中要简单。我以为必须学习一些Android的数据存储API,但libGDX提供了简单键值存储类。只要调用以下代码进行初始化: Preferences prefs = Gdx.app.getPreferences("DroneInvaders"); 然后使用get(“key”, defaultValute)和set(key,value)进行值的读写。 我唯一遇到的麻烦是时间问题。为了持续跟踪天挑战和周挑战,必须存储最后玩游戏的时间。当玩家开始游戏,系统比较这个时间并重新设置一些计数器。理论上我可以阻止玩家将系统日历修改到过去的时间,但是我不想这么做。当时间回滚时,我所做的是设置新的每日挑战和周挑战并且重置星星和搜集到的字母个数。 为了实现这个功能,必须获取上一次玩游戏的时间并计算与当前的时间差。是否是同一天、一天前或几天前都会影响计算结果。我在谷歌上搜索到很多讨论这个问题的网站以及StackOverflow问题。大多数答案很好笑。许多程序员简单地用相差的秒数来计算时间差,然后除以60*60*24得到天数,完全忽略了夏令时和闰秒。有人会争辩说,对一个游戏来说这个差别影响不大。但是我不喜欢每年收到2次大量的bug报告。另一些家伙简单地通过从开始到结束日期一天天累加天数。这些循环看起来是正确的,但是计算结果还是会丢失了部分时间。比如一个对象在1月1号上午5点存储了,然后你在1月2好晚上23点计算时间差,在第一个时间点上加上1天仍然比第二个时间点少。但是按他们的计算方法,实际增加了2天。 在这种情况下,我使用的一个技巧是总是设置前一次游戏的日期为早上10点,而设置最后一次游戏的日期为下午5点。尽管夏令时总是在晚上改变,但是这个设置是安全的。因为即使如果有一天有人决定夏令时的变化发生在中午,在这之间同样也有7个小时。阅读全文 »
23天从0开始完成一款Android游戏开发 – 第15~17天
分类标签: 游戏引擎
第15天: Android“后退”按钮、主菜单、固定坐标bug 还记得第11天屏幕坐标和鼠标点击射击不到外星人的问题吗?是的,那都是我的错。幸运的是这让我及时发现了很多下载游戏的Android用户屏幕分辨率并不是800×400。在那之前我是这样直接转换触摸坐标到实际坐标:... 第15天: Android“后退”按钮、主菜单、固定坐标bug 还记得第11天屏幕坐标和鼠标点击射击不到外星人的问题吗?是的,那都是我的错。幸运的是这让我及时发现了很多下载游戏的Android用户屏幕分辨率并不是800×400。在那之前我是这样直接转换触摸坐标到实际坐标:Java代码 float x = Gdx.input.getX() - 240f; float y = 400 - Gdx.input.getY(); 这不是正确的做法。简单恰当的办法是通过GDX进行转换 :Java代码 Vector3 touchPos; touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0); camera.unproject(touchPos); 在Android上处理“返回”按钮 大多数网上的例子在处理“返回”按钮时都谈到重载KeyDown方法。不幸的是这种办法要求使用Stage,我没有这么做。我知道现在的代码里复制了很多Actor和Stage,但那不重要。在下一个项目里我才会使用Stage。 幸运的是,我找到了解决办法。只要在Game子类的create()函数里添加下面函数:Java代码 Gdx.input.setCatchBackKey(true); 然后在render()方法中检查否已经按下“返回”按钮:Java代码 if (Gdx.input.isKeyPressed(Keys.BACK)) { Gdx.app.exit(); } 由于render()每秒钟会被调用很多次,你可能需要一个boolean标记变量来检测“返回”按钮是否已释放。Java代码 if (backReleased && Gdx.input.isKeyPressed(Keys.BACK)) { backReleased = false; Gdx.app.exit(); } else { backReleased = true; } 现在可以进入游戏,进入商店菜单,然后返回主菜单。当然,菜单只显示选项,还没有真正实现功能。 使用9-patch处理动态大小的按钮和容器 注:9-patch一个对png图片做处理的工具,能够为生成一个“*.9.png”的图片实现部分拉升。 我还学会了如何使用9-patch创建漂亮的按钮。有一次,我意识到不得不像绘制10个大小不同的选项按钮,但样子基本上一模一样只有里面的内容不同。我甚至参考了Gdx按钮,但最终还是决定自己DIY一个。在我游戏里,按钮有一些特殊需求,在一个文本按钮里要结合了2张图、4个文本以及2种不同字体。 无论如何,我得画一个包括所有按钮尺寸和其他的东西的46×46 9-patch图片,然后写一些代码定制其他覆盖在图片上面的东西。我在构造函数里通过TextureRegion从大皮肤里提取9-patch。减掉了一个皮肤开关。 通过这种处理使我得以有各种不同的选择来填充主菜单,同时我还加入了滚动字幕给出玩法提示。我真的很喜欢这个概念,但很少有游戏使用它。有的游戏只显在一开始的时候有个提示。也许他们不想让玩家看主菜单时分心吧。 下面是购买强化道具的商店菜单: 强化道具 关于道具我又有了一些新点子。一种是可以暂时让外星人减速,另一种是在短时间内积分x5。我正在考虑移除之前商店里的“双倍积分”道具。有些玩家真的很能得高分,所以这可能是一个坏主意。 另一方面,在下次装弹前能增加射速的道具可能会大受欢迎,所以我正在加入。 我希望商店能保持只有7个道具,这样就能刚好在一个屏幕内显示。但现在我不肯定所有可能的升级……拭目以待吧。 第16天:从GDX游戏中录制影片 视频地址:www.youtube.com/embed/RUy177pvT8I?rel=0 我曾想过在YouTube上传游戏视频,然后用recordmydesktop程序录制,但结果一团糟。由于libGDX和RMD不同步,我在屏幕上看到的是一堆零件,诸如被切掉了一半的精灵等等。我搜索了一下发现了几篇有用的文章。基本上都是将每帧做成一个PNG文件然后组成视频。可以想见这么做会耗费大量的磁盘空间,这对我不是大问题。我发现了一个很有用的帖子: http://www.wendytech.de/2012/07/opengl-screen-capture-in-real-time/ 然而,他们的代码有一些问题。出于某种原因,当我用半透明精灵叠加在背景上时,由此产生的PNG文件在那块区域会出现半透明像素。这样生成的视频会有很多乱七八糟的东西。我尝试了不同的设置,甚至改变渲染代码,但问题依旧。现在,只要一个简单的处理步骤——使用ImageMagick(加入黑色背景)就可以解决这个问题。所以我想,如果无论如何都要做这步处理,我可能还要在ImageMagick中做垂直翻转。所以我关掉了代码中的Y轴翻转,这使得它更有效率,从而没有必要在每一帧中分配w *h*4个字节的内存。在800×480的屏幕上,每一帧大约需要1.5MB! 同时,处理帧率(跳帧)的代码没有怎么优化。处理过程跳过了几个文件号,这没什么问题。但同时还给每帧还创建了对应的ScreenShot对象,这完全没有必要。譬如你正在录制30fps的视频而游戏运行速率是60fps,你花了一半的时间在创建完全用不到的对象上。 最后,FPS处理代码似乎没有释放像素图。所以如果你运行了很长的时间,RAM会被吃光。 所以,我从ScreenShot类里提取出了全部的FPS代码,剩下的代码只负责处理连续视频。我还注意到一些变量有初始化但从未使用过。现在ScreenShot类变得更加直观并且易于理解:Java代码 public class ScreenShot implements Runnable { private static int fileCounter = 0; private Pixmap pixmap; @Override public void run() { saveScreenshot(); } public void prepare() { getScreenshot(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false); } public void saveScreenshot() { FileHandle file = new FileHandle("/tmp/shot_"+ String.format("%06d", fileCounter++) + ".png"); PixmapIO.writePNG(file, pixmap); pixmap.dispose(); } public void getScreenshot(int x, int y, int w, int h, boolean flipY) { Gdx.gl.glPixelStorei(GL10.GL_PACK_ALIGNMENT, 1); pixmap = new Pixmap(w, h, Pixmap.Format.RGBA8888); Gdx.gl.glReadPixels(x, y, w, h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, pixmap.getPixels()); } } 好了,全部就这么多。我在渲染循环中的每个渲染结尾加上了:Java代码 ScreenShot worker = new ScreenShot(); worker.prepare(); // grab screenshot executor.execute(worker); // delayed save in other thread 考虑到完整性,在Screen的子类添加了executor:Java代码 private ExecutorService executor; ... executor = Executors.newFixedThreadPool(25); 现在,在我的酷睿2已经赶不上帧率了。这是好消息,一方面因为游戏速度变慢我能够录下更好的视频,另一方面能更好地记录截图以供稍后导出视频。所以我添加了一个截图热键。在按住S键时开始录制,当你只是记录了一些有趣的片段,松开S键让PNG writer赶上进度。当CPU的负荷恢复到正常,意味着PNG都生成好了,你可以再次开始录制。 这种方式创建的视频很容易编辑。只要删除不需要的PNG文件,用剩下的压制视频即可。而且这种方法也很容易与音乐同步,因为可以随意添加或删除帧。 用截图生成YouTube视频 由于Android屏幕默认分辨率是480×800,而最接近YouTube的分辨率是1280 x720。因此需要将图像缩放到432×720 ,以保持宽高比。这样两边会多出很多未使用的面积。你可以把你的logo、广告贴上去,甚至可以并排显示两个视频。我决定用另一段视频填补空白,那是我用一台手持设备拍摄的,所以图像更小只有372×620。 现在,我创建了一个大小1280×720包含了logo的静态图像。现在我把它混合进游戏,并垂直翻转。在Linux上,我使用这样的命令: for i in shot*png; do echo $i; convert $i -flip -filter Lanczos -resize 372x620 temp1.png; composite temp1.png back.png -geometry +126+56 $i; done 一旦所有的图像都准备就绪,就可以运行MEncoder来导出视频。YouTube建议720p的视频采用H.264格式和5000以上的比特率 。他们还建议两个B帧(RGB)。这里是执行的命令: mencoder mf://shot*.png -mf w=1080:h=720:fps=25:type=png -ovc x264 -audiofile music.mp3 -oac copy -o movie.avi -x264encopts bitrate=5000:bframes=2:subq=6:frameref=3:pass=1:nr=2000 这样就生成了一个质量过硬的YouTube游戏视频。在这篇文章的开始,你可以看到我的成果。至于音频,我只是提取了一些游戏的音轨并没有捕捉实际游戏中的音频。 第17天:Android图标、完成道具 我喜欢Android允许(甚至建议)图标不是圆角矩形。这样可以赋予游戏自己的个性风格。起初,我考虑过给这游戏做一个特殊的图标,但我真的非常非常喜欢这个画着外星人像素图形的盾。我用Inkscape制作,这样就可以输出任意大小的图片(而不像在GIMP下制作的其他一些图形)。献上Drone Invaders官方图标: 丰富的道具 下面的视频显示所有收藏的强化道具: http://www.youtube.com/embed/SZ73G0n6cm4?rel=0 我准备了原子弹,但名字还没有最终确定。也许会叫核弹、钚炸弹、智能炸弹或完全不同的东西。它会摧毁屏幕上的一切。Boss能抵挡一两个,但遇到三个炸弹一样完蛋。在系统内部,每个Boss有20点血而炸弹有8点的伤害。普通攻击就是1点伤害,除非你升级激光。 其次,有3路散弹。射击三次仍然要更换弹夹。这是一个非常强大的道具,有了它,真是人挡杀人佛当杀佛,清理掉一波波的怪物和boss。 第三,自动重装填。正如名字那样,你的激光会自动加载。所以可以自由地射击,射击,再射击。 第四,减速。它只是减缓外星人的移动速度,其他一切速度正常。在前20关这玩意儿相当废柴。但越到后来,你就越觉得它有用。 第五,双倍积分。在道具作用期间,获得的点数翻一倍。我仍然在考虑是否要在达到某个分数的时候给予奖励,但达到高分仍是一件很酷的事情。阅读全文 »
23天从0开始完成一款Android游戏开发 – 第12~14天
分类标签: 游戏引擎
第12天:新游戏名 DRONE INVADER 名字终于选好了。备选名字有很多,但只有6、7个是可用的。这一个看起来最符合游戏的主题。全新的主题也做好了,同样选用了Ruslan字体。 今天弄懂了Java里Comparable和Comparator的区别。我改动了子弹部分的代码,以便同时发射多个子弹(激光碎片)。子弹不必接触到外星人才能打中,只需朝着一个方向发射,子弹便会自动攻击外星人。 第12天:新游戏名 DRONE INVADER 名字终于选好了。备选名字有很多,但只有6、7个是可用的。这一个看起来最符合游戏的主题。全新的主题也做好了,同样选用了Ruslan字体。 今天弄懂了Java里Comparable和Comparator的区别。我改动了子弹部分的代码,以便同时发射多个子弹(激光碎片)。子弹不必接触到外星人才能打中,只需朝着一个方向发射,子弹便会自动攻击外星人。这样就可以简单地根据子弹发射时刻的轨迹判断先击中了哪个外星人,从而取代全碰撞检测。这个方法对除Boss级外的所有级别都适用。 因为Boss会吐出其他外星人,他应该排在数组里的第一个,这样子弹会飞过所有外星人直接打中boss。如果子弹直接打中boss,会保留之前的处理。但如果只是向某个方向射击,会按照y坐标轴对外星人排序,然后打中最近的那个。 新特性会稍稍改变游戏体验,现在玩起来更容易也更有趣了。射击外星人不再会因为手指点击出现误操作。游戏从“精确碰撞”变为“射击测试”,弹卡是10发的,以防你瞬间摧毁一切。 我想我应该把外星人的运动变得更有挑战性一些,这样游戏不至于太容易。 我开始喜欢上这个游戏了,而且是特别喜欢。我决定在睡前再做一会儿,在16、26这样的关卡添加了一个大月亮,外星人就藏在月亮后面。月亮慢慢移动穿过屏幕,在Boss出现的时候正好从屏幕移出。这让游戏难度陡然增加,因为很难判断是否有外星人躲在月亮后面。由于月亮是圆形,这增加了操作长方形难度。我调查了精确像素检测方法,每当射中一个外星人像素会逐个显示,特别是当月亮一直在转的时候。然而这种方法恐怕会让游戏变慢。原来的矩形被我减少了12%,用矩形检测取而代之。虽然并不是精确像素,但是工作得也很好。这种方式可以让子弹穿过月亮打中有月亮做掩护的外星人。 我对现在的游戏体验非常满意。有趣,有挑战性,而且很吸引人。现在我需要更多boss,更多的外星人类型和能量升级。如果不升级能量想升到60级是非常难的,所以我试着增加足够的内容,起码在100级之前没有重复的boss出现。我觉得增加能量升级会容易点,看看增加这些会以后游戏会变成什么样。 我第一次感觉这会是一个很棒的游戏,会从一般的太空射击游戏里脱颖而出。 第13天:盾牌、新Boss 我加入了一个新boss,现在有两个boss。以下是目前外星人的名字:Worker、Eater、Hairy、Glider。Boss叫Worker Boss(看起来像是更大的Worker,而且会吐出很多小Worker)和Borg(它是立方形的,被摧毁以后变成许多大立方块)。 我还添加了盾牌。以前我曾经用Inkscape画过这种漂亮的盾牌标志,还从某个YT教程里获得了灵感。我试着照着教程做(教程使用的是Adobe Illustrator,不是Inkscape),但是失败了。我开始观察一些喜欢的盾牌,注意到盾牌只是由一些分支、曲线或梯度构成。我把Hairy放上去,看起来很不错。这个还可以用作Android标志。 不管怎样,这个盾牌可以随时引入并且持续20秒。后面的一些升级可以使盾牌持续更长时间。如果外星人碰到盾牌会加速损耗能量,这样即使盾牌消失你仍然可以干掉它们。 盾牌的图形看起来很像力场,使用Gimp再配合手动修改可以让它变得更好看。我想要一个漂亮的曲线而不是直线,因为直线好像不能添加梯度阴影效果。也许有一些技巧可以做到,但是目前我还不知道。最后,我结合了不同角度的多重线性梯度,出来的效果非常棒。 我试着在这个盾牌标志上面添加一些荧光效果,但是看上去有些太刺眼了。我会把它留在飞行过程中吃到能量升级时使用。 有了这些新图形,游戏看起来更完整了。我还在考虑在哪里放置分数倍增器比较合适,还有是不是需要显示当前攻击波。 我还在想玩家需要在玩之前买一些升级装备,但这需要一些硬币,或者类似的钱,目前,快速游玩显然还不支持。也许可以在一些外星人后面留一些水晶或者一种类似随机升级的装备会出现。再或者你经历了多少波,就得到多少硬币。 第14天:完成所有低级外星人图形 所有7种低级外星人都完成了.我刚刚做好了Catcher,Humaniod和Scorpio是昨天晚上做的。我还做了一些基本图形放在商店里,玩家可以从那儿买到升级产品。 关于外星人boss我又有了一些新想法。其中一种像蛇,身体由多段组成,需要分段消灭。 第二种外星人boss自己也可以制造boss。如果你不能及时摧毁它,它就会放出另一个boss。第二种boss吐出普通外星人并向下移动(以便为下一个留出空间)。 第三种boss是一种特别的生物,它能够自我分裂。当它被打中时,会分裂成两个相同的外星人。每一个分裂出的外星人能量是有母外星人的一半。外星人会一直分裂,直到变成一堆能量为1的外星人。打死那些外星人就可以结束游戏。 第四种boss是……好吧,让我留一些惊喜给你们,玩的时候就知道了。我敢肯定对一些玩家而言,第四种boss是非常讨厌的,除非他们发现这种boss的规律。 安排音乐时间 今天剩下的时间里,我浏览了一些免费音乐。有许多音乐网站,但成千上百的音乐逐个听过去并不好玩。多数免费的音乐网站都是垃圾。我在Reddit的gamedev上找到了一些推荐的网址,同样在gamedev.stackexhange.com上可以看见,还有一些独立的网站。除了Jamendo, 这个有点贵,大多数免网站真的很糟糕。 我通常会自己创作音乐,用MilkyTracker或者一些其它破解程序。这个习惯是从Amiga500来的,在.it或.s3m调制器的时代这让我感觉得心应手。但这次我觉得没有时间这么做。通常创作一个好听的音频要花我3、4周。然而,我可以再利用一些以前的作品,但我已经把它们都用到以前的游戏中了——说实话,没有一个适合用到这次的游戏。我还是找到了一些Kevin MacLeod的音乐。这些音乐相当不错,我决定就用他了。 是花上几个小时去听那些免费音乐,还是集中精力自己做,似乎真的取决于你对时间的估计。 阅读全文 »
23天从0开始完成一款Android游戏开发 – 第9~11天
分类标签: 游戏引擎
第9天:这是一款第一人称视角射击游戏,但它绝不老套 在与人们谈论起这款游戏的时候,为它定义一个明确的分类确实很难。虽然可以将它看作一款传统的街机游戏,但与那些到处移动自己的飞船、直线开火的街机游戏不同的是——你的位置是固定的并且可以按照指令向任意方向开枪。经过仔细回想,我从来没有见过一模一样的游戏,所以不要试图把它归到那些现有的分类中。 第9天:这是一款第一人称视角射击游戏,但它绝不老套 在与人们谈论起这款游戏的时候,为它定义一个明确的分类确实很难。虽然可以将它看作一款传统的街机游戏,但与那些到处移动自己的飞船、直线开火的街机游戏不同的是——你的位置是固定的并且可以按照指令向任意方向开枪。经过仔细回想,我从来没有见过一模一样的游戏,所以不要试图把它归到那些现有的分类中。我能给出的最为贴切的描述是:与太空入侵者类似,但是游戏中没有飞船。这样的描述把人们完全搞晕了。 今天我重写了制造外星人的代码,并且把外星人的颜色都改成了白色。这样就可以在游戏运行过程中用同一个子画面更新外星人的颜色,假设你没有发现外星人是单色的。除了boss之外,我都打算这样处理。boss会增加一些发光的动态效果之类的。谁知道呢,也许会有艺术家发现游戏的潜力并且创作出给力的效果。如果发现这种情况请联系我。 我考虑在敌人将要“升级”的时候给出提示,像是“外星人已经升级”这样带有金属感的文字会与外星人一起出现, 爆炸的代码也进行了统一。一开始是通过一个2D“像素”数组展示敌人被炸成碎片。现在这段代码进行了优化,各种外星人都使用了同样的效果,只有boss除外。boss会分阶段炸成碎片。这个设计我还需要仔细考虑。 无论如何,如果你对玩这个游戏感兴趣,这个.apk文件就是迄今为止的全部成果。虽然没有什么特色,但是你能看到我一直在努力。 第10天:让游戏玩起来更有意思 我已经开始怀疑这个概念是否真的具有较长时间的可玩性。如果外星人可以左右移动游戏会变得更好玩一点。我新增了两种攻击队形看上去感觉好多了。我还增加了击毁外星人爆炸时镜头抖动的效果,这样游戏感觉更鲜活了。重新填充弹药的功能也实现了,现在如果你把十发容量的弹夹打空就不得不重新填装。在使你中断射击重新填充弹药的同时可玩性也增加了。我把一些外星人丑陋的品红色替换成冷色调的蓝白色效果好看多了。 我想如果设定成玩家只操控屏幕的下半部分——有护盾遮挡的地方,屏幕的上半部分就可以一直看到敌人的情况而不会被玩家的手指挡到。这种情况是经常出现的。一开始护盾可能有半个屏幕大,随着敌人的攻击护盾逐渐被打掉直到枪口裸露没有任何防护。虽然这意味着可能要画一把枪却不失为一个好主意,我可能会试试。 星空之战的名字已经取好了。我尝试了其他20几个名字,但由于太空主题的游戏太多很难找到一个免费的名字。 第11天:增加外星人种类、攻击方式,第一级boss出现 第一个boss出现在第10关出现,它的外形就是个大方块。我称它为Borg,虽然它和Borg一点不像。它会不断发射外星人,你必须打很多次才能消灭它,我为干掉boss的场景设置了一个大的震动。boss是由10×10的像素积木组合而成爆炸效果很炫。 我还加入了个新蛇形移动攻击,外星人落下时会左右移动并然后下降不断重复直至到达底部。我在这个攻击模式中稍稍加快了移动速度,这样玩起来更具有挑战性。 我还画了一些好看的外星人并给它们起了名字,每种类型都由Java类的名字命名。分别是:Hairy、Glider、Worker和Eater。 这是今天做的.jar文件,只要你安装了java就可以在windows、linux、mac上运行。在linux上我执行的命令是: java -jar stardust.jar 为了保证正常的游戏效果,请注意窗口的高度不能小于800。我在 1680×1050的台式机上运行良好,但在1280×800的笔记本上由于任务栏占据了一定空间窗口被垂直压缩,因此我需要点击外星人的下方进行射击。我想如果要发布PC版的就必须解决这个问题。阅读全文 »
23天从0开始完成一款Android游戏开发 – 第6~8天
分类标签: 游戏引擎
第6天:第一批外星人和屏幕方向 好了,现在有了一些复古风格的外星人它们正在到处飞。抱歉,暂时还没有demo。这款Andriod游戏目前只有.apk格式的安装包。想必你不大可能安装一个陌生人发给你的安装包。总的来说,目前背景和星星已经可以载入而且还会慢慢移动。为了让星星滚动我在星星图案上调用了setV()和setV2()函数,并逐个增加计时器的值。 第6天:第一批外星人和屏幕方向 好了,现在有了一些复古风格的外星人它们正在到处飞。抱歉,暂时还没有demo。这款Andriod游戏目前只有.apk格式的安装包。想必你不大可能安装一个陌生人发给你的安装包。总的来说,目前背景和星星已经可以载入而且还会慢慢移动。为了让星星滚动我在星星图案上调用了setV()和setV2()函数,并逐个增加计时器的值。像这样:Java代码 scrollTimer -= delta*0.014; if (scrollTimer < 0.0f) scrollTimer = 1.0f; sStars.setV(scrollTimer); sStars.setV2(scrollTimer+1); 然后出现了一些外星人,它们会向右移动。这就是这款游戏目前所有的功能。我仍然在学 libGDX,因此会花大量时间 Google、阅读手册或是看视频。 小贴士:我注意到一些借助 libGDX 开发的游戏通常会固定屏幕方向。在翻转设备时游戏界面不会转动。我在 SO(StackOverflow)的一些缺头少尾的跟帖和评论中找到了一个简单的解决办法:只要将 AndriodManifest.xml 文件中的 landscape 用 sensorLandscape 替换就可以让屏幕随着设备一起自动翻转。 第7天:改变布局和屏幕方向 现在可以射击外星人了。由于是复古风格的图形,我决定在外星人爆炸时采用大像素(在Gimp里使用无差方式将外星人图片放大5倍)。然后写了一点碎片管理代码,在libGDX 从中央向其它方向随机发射碎片时进行中间计算。 也许你注意到屏幕上没有图形表示你自己。这是有意为之的。如果你没有意识到这一点也没关系,因为这是第一人称射击游戏。搞笑吧!嘿嘿,至少从技术上来说是这样的。你不必知道你在哪儿,只要射击敌人就好。在一些测试版本中,我也尝试了对外星人做一些尺度上的变换以增加纵深感,让你感觉它们从远处过来。但在安卓设备上用手指是很难射击那些很小的东西的,所以我放弃了这个想法。 为了让这个游戏更加有趣,射击外星人用的激光枪需要不断补充能量。每把可以射击10次,然后需要点击屏幕左下角的能量按钮。我现在还不确定它的特性——因为还需要测试。 当我在测试这个功能的时候有件事让我很头疼:当射击外星人的时候我的食指会满屏幕点,很难看清屏幕上发生了什么。然后我看了一些自己手机上各种安卓游戏,意识到如果屏幕方向是纵向的效果会更好。在纵向模式下,食指不是从屏幕下方而是从右边过来的(当然你需要习惯使用右手)。在这种模 式下,屏幕的大部分区域还是可见的。我还改变了外星人飞行的方向,外星人会从屏幕上方出现而不是之前的从左向右出现。 同样基于这个原因,我还改变了游戏的主要设计。你可以一直玩到让一个外星人越过你,而不是之前的只能在固定时间内玩。起初我的想法是你可以每次玩一分钟, 在这一分钟内你要尽量多得分(类似于水果忍者的加分规则)。现在我的想法是,虽然你可以有3条命,然而一旦你让外星人越过了你游戏就到此结束。 后来我还修改了背景星云图。之前我写过怎么用Gimp画带有星星的夜 空和星云图,但是每次不得不为了满意的效果而做很多实验。可以通过对两个图层做减法并增加一些固有噪声,然后镀上一层渐变颜色以达到效果。问题是当对两 个图层做减法的时候总会产生一些“小”星云图,而我想要的是整个屏幕都充满色彩。我用画笔随机地在画纸上点一些点,然后着上不同颜色。我把星星分成3个图 层:第一个图层,给每个点加上一个两像素的阴影(没有补偿),第二个图层进行模糊化,第三个图层让它保持原样。这个技巧可以画出很好的星星效果,奥秘在于保证相近的点有不同颜色或不同的清晰度。而这也正是真正星空所具有的特点。看起来相近的星星其实相距N光年之遥,只是从地球上看起来他们比较接近而已。 第8天:星空之战 我在试着给这个游戏起个新名字。虽然想了一个“星空之战”,但是是否合适感觉仍然有待研究。我尝试了一些字体,后来决定使用一种叫做Ruslan Display的字体。 今天我改进了外星人爆炸效果的算法。我一直在思考怎么让敌人看起来有一波一波的进攻。先有10波敌人,紧接着boss出场。通过一关以后游戏会有些加 速,还会加入一些新的敌人。在每10级里(10,20,30…)都会有一个新的boss。这个算法会一直重复执行,直到游戏者输掉为止。如果发现你想消灭所有外星人,游戏的速度会被设置为开始速度的两倍这样你需要付出很多的努力。 下面是现在对能量提升的设想。 有时限的能量提升,可以在游戏过程中收集到: 盾牌(在屏幕的底部,防止一个外星人通过) 分数倍乘提升器 快速火力(只需手指持续按下然后到处移动) 大炸弹(毁灭掉所有屏幕上的外星人) 超能力(跳过十波敌人,但是获得所有分数) 银河忍者(用绝地武士之剑腰斩敌人——水果忍者的风格) 自动补充能量(自动为激光枪补充能量) 多火力(同时有3或4个激光枪开火) 多生命(启动游戏时有4条命) 永久性的能量提升,可以在每次玩的时候不断升级: 快速火力 盾牌 银河忍者 自动补充能量 阅读全文 »
23天从0开始完成一款Android游戏开发 – 第3~5天
分类标签: 游戏引擎
第3天:一个新的游戏点子 在网上泡Android相关论坛,找点子的时候,我看到了《Revenge of the Titans》的一则广告。有着复古图像和漂亮动画的酷游戏。我也能做复古图像。所以,外星人会从太空降落到后院…… 不对!后院貌似不是个好点子。我想我得给游戏取个新名字了。 外星人在边界上攻击了远程防守站。他们摧毁了自动系统,所以你不得不手动操作并射击。 第3天:一个新的游戏点子 在网上泡Android相关论坛,找点子的时候,我看到了《Revenge of the Titans》的一则广告。有着复古图像和漂亮动画的酷游戏。我也能做复古图像。所以,外星人会从太空降落到后院…… 不对!后院貌似不是个好点子。我想我得给游戏取个新名字了。 外星人在边界上攻击了远程防守站。他们摧毁了自动系统,所以你不得不手动操作并射击。这个游戏可以叫 MANUAL OVERRIDE 或 EMERGENCY OVERRIDE。我在网上和Google Play上查了一下,貌似这两个名字,别人都还没用的。 外星人的大型飞船在一个停靠站着陆,你必须要把他们赶在。他们可能躲在油桶后面,或者是已着陆的飞船后面等等。我想在游戏中加一些可摧毁的东西,放在敌人旁边。游戏可能叫 DOCKING BAY 或 DECK 42 之类的名字。 名字听起来不是那么令人兴奋?嗯,是的。 第4天:一个转变 我一直在尝试画停靠站的图形,但他们看起来很垃圾,所以我正放弃了这个点子。战斗肯定是在太空中进行,这点不用改变。外星人会从屏幕的一端出来,你可以在他们到达屏幕另一端之前射击他们。在底部有一些能被摧毁的东西。外星人可能躲在这些东西后面,你可以将这些东西打掉或者在上面射出几个洞。下图是一个例子,里面有塔,卫星接收器和其它一些东西: 我的想法是让玩家在有限的时间内(比如说一分钟),尽可能击毙更多的外星人。 由于这个游戏有一个太空主题,我浏览了自己的字体集,并且找出了一些很酷的银河字体。目前,我将使用AdourGM字体,用于显示游戏中的文本,比如:points和屏幕选项等。 受到Gimp的星云教程的启发,我使用Gimp完成了上图。读了Gimp的星云教程,自己做了一些调整,我在几个小时内完成上述图。我讨厌在一件事情上花费太多时间,不过,还好这个背景在这个游戏的大部分地方中都能用到。 第5天: libGDX和图形的想法 我选择了将要使用的库。它基于java语言,看似相当的完整。我尝试搭建并调试了一个测试程序,可以很快上手。但我仍然困惑是使用场景(Scene),角色(Actor)以及其它的特性,还是像以往那样自己搭建这些东西。这个库的文档缺少示例,也没介绍如何结合,这种情况下,我猜想自己将会不得不依赖于Google。例如,没有任何解释,你应该如何连接角色(Actor)和怪物(Sprite)。如果你有近20个图形(或者其他类似的东西)一样的怪物(Sprite),应该如何处理。 对于第一个项目,我想我刚学会如何使用怪物(Sprite),然后使用以前的经验来构建余下的。对于将来的一些项目,我可能会更聪明,会清楚哪些我需要使用的东西已经有了。我只是讨厌先学了很多了,然后发现框架或者库不够灵活多变,不足以满足我将来可能遇到的一些非常规的想法。 下面是我对复古图形的外星人的一些想法。大部分的时间,我先画在纸上。这也许只是一种习惯,但远离键盘和显示器会让我集中精力:阅读全文 »
23天从0开始完成一款Android游戏开发 – 第1~2天
分类标签: Unity
开篇 我想开发一款Android游戏有一段时间了,但从来没有一个好的idea。最近,我一直在玩地铁跑酷,发现它实在太有意思了。通常来讲,我不喜欢没有终点的游戏,因为你不可能通关,所以每次我玩这些游戏的时候,我总会随意设置一些目标然后再去玩。这次我的目标是得到30倍分数复乘技能。当我实现这个目标的时候,我就获得一些技能然后就会迷上这款游戏。 本系列是译文,由 ImportNew 及其用户翻译。 英文原文:Day 23: Release! 开篇 我想开发一款Android游戏有一段时间了,但从来没有一个好的idea。最近,我一直在玩地铁跑酷,发现它实在太有意思了。通常来讲,我不喜欢没有终点的游戏,因为你不可能通关,所以每次我玩这些游戏的时候,我总会随意设置一些目标然后再去玩。这次我的目标是得到30倍分数复乘技能。当我实现这个目标的时候,我就获得一些技能然后就会迷上这款游戏。这款游戏也让我意识到,一款Android游戏没有必要非要有什么大的来头和复杂的游戏设置,简单随意就好。 不管怎样,我觉得一个游戏如果能做到仅需你将手指对准一个东西然后向它射击就很好了。如果再能有一个系统性的任务,每天每周需完成的挑战和一些武器升级的话,那么这个游戏就会很有趣了。我的第一个想法是创立一款游戏,让一些可爱的小怪兽在一个美丽的场景中到处跑,可以藏在树后面,或者躲在老房子里,甚至可以是在云朵里。你需要做的就是尽可能多地将它们打下。我说的可爱的小怪兽,指的是一些像Gremlins(小精灵)之类的玩意。它们会搬一些体形巨大的机器,或者是做一些其他的愚蠢的事情来让你哈哈大笑。当然,作为一个拥有零预算的独立游戏开发者,就按我可以想象到的动画复杂程度来看,我知道这些将需要投入大量的艺术设计和开发时间。所以我决定采用一种不同的设计,这样我可以在合理的时间内自己完成。 我现在的目标是在一个月内开发一款有趣而可玩的Android游戏。 第1天:后院大恐慌 在逛动画素材网站的时候,我发现了一些给力又免费的僵尸动画。有僵尸走动、僵尸被枪击中之类的。所以,这可能是个类似于僵尸射手的游戏。 游戏角色会在他的走廊上,射杀从外面的麦田涌来的僵尸。游戏的名字就叫后院大恐慌。你可以转换不同的游戏角色,老奶奶、拿着猎枪的抠脚大汉或者是拿着远射程来复枪的大兵。下图是非常非常草的草图: 我刚才玩了会僵尸在美国。游戏倒是挺有意思,但是像疯子一样一直按射击多少有点无聊。我打算把后院大恐慌设计成一碰就死的模式,至少僵尸里不要有boss。我仔细的想了想,僵尸主题对我来说有点太阴郁了,而且只要市面上有,这个类型就不稀罕了。所以,我不确定游戏会不会做成这个类型。另外,这个游戏看起来会涉及到3D图形,不过我会坚持2D,至少我的第一个安卓游戏会坚持。 第2天:安装Eclipse,选择游戏库 我下载了安卓包,然后在Linux系统上安装了Eclipse。 能在Galaxy S2手机上运行“Hello World”。 你好 Java。我真的得记住所有这些Java指令,因为从2002起,我就没用过这些玩意儿了。 看着这些2D/3D OpenGL类的东西,似乎用工具包/库来搭建游戏会是一个好办法。我从NordicGame2013上弄到一些免费的Unity3D序列号,但是还没有在Google Play上发行的权限。所以我想我得暂时跳过Unity了。现在我正着手于CoronaSDK和一些其他的库。阅读全文 »
Android游戏开发教程:手把手教你写跳跃类游戏(三)
分类标签: 游戏引擎
在本次教程中,我们将完成Food这个类的设计。这是游戏相当关键的一部分,直接决定了游戏的可扩展性、可玩性。。。 进入正题: 先给大家看下Food类的类图: 从图中我们可以看到有多种食物,并且它们具有很多相同的属性和方法。所以我们很容易想到使用继承Food类。 新的类图就变成这个样子了: 这样当我们为游戏添加新的事物类型时只需要继承Food类并重写draw方法就可以了。 在本次教程中,我们将完成Food这个类的设计。这是游戏相当关键的一部分,直接决定了游戏的可扩展性、可玩性。。。 进入正题: 先给大家看下Food类的类图: 从图中我们可以看到有多种食物,并且它们具有很多相同的属性和方法。所以我们很容易想到使用继承Food类。 新的类图就变成这个样子了: 这样当我们为游戏添加新的事物类型时只需要继承Food类并重写draw方法就可以了。(不同的食物,所表现的外观不同,所以需要具体的食物类,自己实现这个方法) 但是还有一点值得注意:每一个食物都有多种运动方式。如:水平运动,垂直运动,静止。。。如果将这些方式写在具体的事物类里面,首先这个move方法会变得很复杂,里面会有各种分支,判断。。。 那么如果后期我们对游戏进行升级,比如增加一种新的运动方式,或者删除一种新的运动方式将变得非常繁琐,而且也不利于代码的复用。 这个时候我们需要采用这样一种设计模式:策略模式。 我们把move这个方法抽象为一个类MoveBehaviour。 那么新的类图就是这个样子了: 当我们需要添加新的运动方式时,只需要写一个类继承MoveBehavior,并实现具体的move方法就可以了。 这样就完成了食物类的设计了。我们这样设计之后,就可以非常灵活的创建新的食物,并为其添加新的运动方式了。。。了不起!!! 给大家看下项目的结构图: 通过将源码放在对应的package里面更利于游戏的管理。阅读全文 »
Android游戏开发教程:手把手教你写跳跃类游戏(二)
分类标签: View
在本次教程中,我们将完成小球这个类的编写 首先给大家看下小球这个类的类图: 主要确定小球的坐标:x,y。小球的半径:r。以及小球的运动函数和如何讲小球画在屏幕上的draw()函数。 难点是:move()函数。我们将简单的模拟一个物理环境,使小球看上去显得很有弹性。具体关于这部分,我会在接下去的文章中进行解答。 在本次教程中,我们将完成小球这个类的编写 首先给大家看下小球这个类的类图: 主要确定小球的坐标:x,y。小球的半径:r。以及小球的运动函数和如何讲小球画在屏幕上的draw()函数。 难点是:move()函数。我们将简单的模拟一个物理环境,使小球看上去显得很有弹性。具体关于这部分,我会在接下去的文章中进行解答。 下面给出Ball.java的代码Java代码 package jumpball.game; import Android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; public class Ball { float r, x, y; private Paint mPaint; // 最大垂直速度 方向向上 -16. float MAXVERTICALSPEED = -16, MAXVERTICALA = 1; // 默认最大高度 float defaultJumpHight; // 小球在垂直方向上需要移动的距离! float verticalMove; float ha = 0, va = 1f, v0v, vtv, v0h, vth; GameView gameView; public Ball(GameView gameView) { this.gameView = gameView; mPaint = new Paint(); mPaint.setColor(Color.BLACK); defaultJumpHight = gameView.height / 3; r = gameView.width / 36; y = gameView.height - r * 2; x = gameView.width / 2 - r; //根据加速度公式计算得出Vt^2-V0^2=2AX MAXVERTICALSPEED = -(int) ((float) Math.sqrt(2 * gameView.height / 3) - 1); verticalMove = defaultJumpHight; v0v = MAXVERTICALSPEED; } public void draw(Canvas canvas) { move(); canvas.drawCircle(x, y, r, mPaint); } public void move() { // Vt=V0+aT 当前速度=初始速度+加速度*时间 vtv = v0v + va; // 当下降速度达到一定程度时,设置加速度为0.4f。 if (vtv > -5 * MAXVERTICALSPEED / 8) { va = 0.4f; } else { va = MAXVERTICALA; } // 当当前还需上升的高度 大于默认高度时,速度继续保持最大速度,vtv<0表示方向向上 if (verticalMove > defaultJumpHight && vtv < 0) { vtv = MAXVERTICALSPEED; } float vMove = (v0v + vtv) / 2; // 这一次垂直高度移动距离。 verticalMove = verticalMove + vMove;// 减小时,表示网上移动了 y = y + vMove; v0v = vtv; if (y > gameView.height) {// 触地了 y = gameView.height - this.r; v0v = MAXVERTICALSPEED; verticalMove = defaultJumpHight; } } } 在GameVIew类里面定义一个小球对象。Java代码 Ball ball; 在GameView的构造方法里面进行实例化Java代码 ball=new Ball(this); 同时在GameView的mDraw函数里面添加Java代码 ball.draw(mCanvas); 这样我们就完成了ball对象...娃哈哈~(游戏 已经有点样子了!) 运行程序。我们就能看到一个有弹性的小球了,在那边跳啊跳~~~~ 最后我们完成小球的左右移动。这部分比较简单~~~ 在GameView定义两个boolean变量表示左右键是否按下 同时重写两个方法Java代码 public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { gameActivity.finish(); return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { leftKeyDown = true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { rightKeyDown = true; } return true; } public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { leftKeyDown = false; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { rightKeyDown = false; } return true; } 然后在小球的move方法里面添加:Java代码 //水平移动设置 if(gameView.leftKeyDown==true){ xx=x-10; } if(gameView.rightKeyDown==true){ xx=x+10; } if (x < r) { x = r; } if (x > gameView.width - r) { x = gameView.width - r; } 本来写到这里就已经完成了小球的左右移动,但是实际运行却发现根本移动不了~~~ 我查了下资料发现问题在这: 我们自己定义的View,必须获得焦点之后才会调用onKeyDown方法~而Activty的onKeyDown方法是当所有空控件均没处理的时候才会被调用。 所以我们在GameView的构成方法里面添加一句:Java代码 setFocusable(true); 好了。。。大功告成~~~~这样我们就可以控制小球的移动了!!! 关于小球move函数,我会在视频中详细的讲解~~~ 下面贴出程序的运行效果图: 以及项目的一个结构: 阅读全文 »
Android游戏开发教程:手把手教你写跳跃类游戏(一)
分类标签: SurfaceView
前言: 看了不少Andriod游戏开发方面的博客,发现大多都是讲解某一方面的知识,没有一个完整的开发教程。 所以我就写了这样一个系列的博客,完整的描述整个游戏的开发过程。 希望能给大家一点帮助,同时也希望大家能给出好的建议。我们共同学习~ 好了进入正题: 这是第一篇,里面涉及到的东西都比较基础~~~ 1.游戏介绍:这次编写的游戏是一款跳跃类游戏,类似与涂鸦跳跃。 前言: 看了不少Andriod游戏开发方面的博客,发现大多都是讲解某一方面的知识,没有一个完整的开发教程。 所以我就写了这样一个系列的博客,完整的描述整个游戏的开发过程。 希望能给大家一点帮助,同时也希望大家能给出好的建议。我们共同学习~ 好了进入正题: 这是第一篇,里面涉及到的东西都比较基础~~~ 1.游戏介绍:这次编写的游戏是一款跳跃类游戏,类似与涂鸦跳跃。我们通过小球触碰方块,完成小球的跳跃。我们将在这一列的博客中来实现这款游戏的开发。 2.最终成品展示: 这个是程序的最终效果。每个小方块都有许多运动方式,水平的,垂直的,静止的,圆周运动的。。。同时每个小方块都有不同的属性。有的碰到会消失,有的弹性更好等等。。。具体的设计方法我会后面几篇文章详细介绍。童鞋们也可以先自己想想看,怎么设计这个游戏。 3.这节课主要完成基本框架的搭建:我们采用SurfaceView实现游戏,(View一般用来做那些不需要一直刷新的游戏:如五子棋,连连看之类的)。 SurfaceView默认实现双缓冲。所以在效率上会高一点。 所谓双缓冲简单的理解就是把要画在屏幕上的图案,先画到一张画布(如Bitmap)上,然后再把这张画布直接画到屏幕上,这样就可以避免闪烁现象。 我们先建一个Android project。 然后建一个GameView。也就是SurfaceView的子类。项目的机构图: 附上代码:Java代码 package jumpball.game; import android.app.Activity; import android.os.Bundle; import android.util.DisplayMetrics; import android.view.Window; import android.view.WindowManager; public class GameActivity extends Activity { GameView mView; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE);// 设置无标题 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);// 设置全屏模式 //获取系统的屏幕属性 DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); mView = new GameView(this, dm.widthPixels, dm.heightPixels); //应用这个布局 setContentView(mView); } } GameView.javaJava代码 package jumpball.game; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.SurfaceHolder.Callback; //本身就是一个Runnable接口 public class GameView extends SurfaceView implements Callback, Runnable { public int width, height; private Canvas mCanvas; //刷新界面线程 private Thread mThread; //处理者 private SurfaceHolder mSurfaceHolder; private boolean mIsRunning = false; private int TIME_IN_FRAME = 50; GameActivity gameActivity; public GameView(Context context, int width, int height) { super(context); //setFocusable(true); //activity是 context的一个子类。 gameActivity = (GameActivity) context; this.width = width; this.height = height; mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); } public void mDraw() { //设置画布的颜色 mCanvas.drawColor(Color.WHITE); drawBG(mCanvas); } public void drawBG(Canvas mCanvas) { Paint mPaint = new Paint(); mPaint.setColor(Color.BLACK); //设置透明度 mPaint.setAlpha(50); //设置抗锯齿 mPaint.setAntiAlias(true); float h = height * 0.01666667f; for (int i = 0; i < height; i += h) { mCanvas.drawLine(0, i, width, i, mPaint); } } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } public void surfaceCreated(SurfaceHolder holder) { mIsRunning = true; //新建一个刷屏线程 mThread = new Thread(this); mThread.start(); } public void surfaceDestroyed(SurfaceHolder holder) { //销毁这个, mIsRunning=false; } public void run() { //决定线程是否继续执行 while (mIsRunning) { long startTime = System.currentTimeMillis(); //调用mDraw进行绘制 synchronized (mSurfaceHolder) { mCanvas = mSurfaceHolder.lockCanvas(); mDraw(); mSurfaceHolder.unlockCanvasAndPost(mCanvas); } long endTime = System.currentTimeMillis(); int diffTime = (int) (endTime - startTime); while (diffTime < TIME_IN_FRAME) { diffTime = (int) (System.currentTimeMillis() - startTime); Thread.yield(); } } } } 上一个最终的运行效果图: 源码下载地址 http://www.linuxidc.com/Linux/2012-02/54645p4.htm阅读全文 »
Android游戏开发教程之十九:Tween动画的实现
分类标签: 动画
今天和大伙讨论一下Android开发中的Tween动画的实现。首先它和上一章我们讨论的Frame动画同属于系统提供的绘制动画的方法。Tween动画主要的功能是在绘制动画前设置动画绘制的轨迹,包括时间, 位置 ,等等。但是Tween动画的缺点是它只能设置起始点与结束点的两帧,中间过程全部由系统帮我们完成。所以在帧数比较多的游戏开发中是不太会用到它的。 Tween一共提供了4中动画的效果 今天和大伙讨论一下Android开发中的Tween动画的实现。首先它和上一章我们讨论的Frame动画同属于系统提供的绘制动画的方法。Tween动画主要的功能是在绘制动画前设置动画绘制的轨迹,包括时间, 位置 ,等等。但是Tween动画的缺点是它只能设置起始点与结束点的两帧,中间过程全部由系统帮我们完成。所以在帧数比较多的游戏开发中是不太会用到它的。 Tween一共提供了4中动画的效果 Scale:缩放动画 Rotate:旋转动画 Translate:移动动画 Alpha::透明渐变动画 Tween与Frame动画类似都需要在res\anim路径下创建动画的 布局文件 补充:最近有盆友提问可不可以不用XML配置动画,希望可以在代码中配置。那我当然要向大家补充了噢~~~ 1.Scale缩放动画 <scale>标签为缩放节点 android:fromXscale=”1.0″ 表示开始时X轴缩放比例为 1.0 (原图大小 * 1.0 为原图大小) android:toXscale=”0.0″表示结束时X轴缩放比例为0.0(原图大小 *0.0 为缩小到看不见) android:fromYscale=”1.0″ 表示开始时Y轴缩放比例为 1.0 (原图大小 * 1.0 为原图大小) android:toYscale=”0.0″表示结束时Y轴缩放比例为0.0(原图大小 *0.0 为缩小的看不到了) android:pivotX=”50%” X轴缩放的位置为中心点 android:pivotY=”50%” Y轴缩放的位置为中心点 android:duration=”2000″ 动画播放时间 这里是2000毫秒也就是2秒 这个动画布局设置动画从大到小进行缩小。XML/HTML代码 <?xml version="1.0" encoding="utf-8"?> <scale xmlns:android="http://schemas.android.com/apk/res/android" android:fromXScale="1.0" android:toXScale="0.0" android:fromYScale="1.0" android:toYScale="0.0" android:pivotX="50%" android:pivotY="50%" android:duration="2000"> </scale> 在代码中加载动画Java代码 mLitteAnimation = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mLitteAnimation.setDuration(2000); 代码如下Java代码 import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.Button; import android.widget.ImageView; public class ScaleActivity extends Activity { /**缩小动画按钮**/ Button mButton0 = null; /**放大动画按钮**/ Button mButton1 = null; /**显示动画的ImageView**/ ImageView mImageView = null; /**缩小动画**/ Animation mLitteAnimation = null; /**放大动画**/ Animation mBigAnimation = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.scale); /**拿到ImageView对象**/ mImageView = (ImageView)findViewById(R.id.imageView); /**加载缩小与放大动画**/ mLitteAnimation = AnimationUtils.loadAnimation(this, R.anim.scalelitte); mBigAnimation = AnimationUtils.loadAnimation(this, R.anim.scalebig); mButton0 = (Button)findViewById(R.id.button0); mButton0.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { /**播放缩小动画**/ mImageView.startAnimation(mLitteAnimation); } }); mButton1 = (Button)findViewById(R.id.button1); mButton1.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { /**播放放大动画**/ mImageView.startAnimation(mBigAnimation); } }); } } 2.Rotate旋转动画 <rotate>标签为旋转节点 Tween一共为我们提供了3种动画渲染模式。 android:interpolator=”@android:anim/accelerate_interpolator” 设置动画渲染器为加速动画(动画播放中越来越快) android:interpolator=”@android:anim/decelerate_interpolator” 设置动画渲染器为减速动画(动画播放中越来越慢) android:interpolator=”@android:anim/accelerate_decelerate_interpolator” 设置动画渲染器为先加速在减速(开始速度最快 逐渐减慢) 如果不写的话 默认为匀速运动 android:fromDegrees=”+360″设置动画开始的角度 android:toDegrees=”0″设置动画结束的角度 这个动画布局设置动画将向左做360度旋转加速运动。XML/HTML代码 <?xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator" android:fromDegrees="+360" android:toDegrees="0" android:pivotX="50%" android:pivotY="50%" android:duration="2000" /> 在代码中加载动画Java代码 mLeftAnimation = new RotateAnimation(360.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mLeftAnimation.setDuration(2000); 代码实现Java代码 import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.Button; import android.widget.ImageView; public class RotateActivity extends Activity { /**向左旋转动画按钮**/ Button mButton0 = null; /**向右旋转动画按钮**/ Button mButton1 = null; /**显示动画的ImageView**/ ImageView mImageView = null; /**向左旋转动画**/ Animation mLeftAnimation = null; /**向右旋转动画**/ Animation mRightAnimation = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.retate); /**拿到ImageView对象**/ mImageView = (ImageView)findViewById(R.id.imageView); /**加载向左与向右旋转动画**/ mLeftAnimation = AnimationUtils.loadAnimation(this, R.anim.retateleft); mRightAnimation = AnimationUtils.loadAnimation(this, R.anim.retateright); mButton0 = (Button)findViewById(R.id.button0); mButton0.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { /**播放向左旋转动画**/ mImageView.startAnimation(mLeftAnimation); } }); mButton1 = (Button)findViewById(R.id.button1); mButton1.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { /**播放向右旋转动画**/ mImageView.startAnimation(mRightAnimation); } }); } } 3.Translate移动动画 <translate>标签为移动节点 android:repeatCount=”infinite” 设置动画为循环播放,这里可以写具体的int数值,设置动画播放几次,但是它记录次数是从0开始数的,比如这里设置为2 那么动画从0开始数数0 、1、 2 、实际上是播放了3次。 剩下的几个标签上面已经介绍过了。 这个动画布局设置动画从左到右(0.0),从上到下(320,480)做匀速移动。XML/HTML代码 <?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:fromXDelta="0" android:toXDelta="320" android:fromYDelta="0" android:toYDelta="480" android:duration="2000" android:repeatCount="infinite" /> 在代码中加载动画Java代码 mAnimation = new TranslateAnimation(0, 320, 0, 480); mAnimation.setDuration(2000); 代码实现Java代码 import android.app.Activity; import android.os.Bundle; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; public class TranslateActivity extends Activity { /**显示动画的ImageView**/ ImageView mImageView = null; /**移动动画**/ Animation mAnimation = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.translate); /**拿到ImageView对象**/ mImageView = (ImageView)findViewById(R.id.imageView); /**加载移动动画**/ mAnimation = AnimationUtils.loadAnimation(this, R.anim.translate); /**播放移动动画**/ mImageView.startAnimation(mAnimation); } } 4 .Alpha:透明渐变动画 <alpha>标签为alpha透明度节点 android:fromAlpha=”1.0″ 设置动画起始透明度为1.0 表示完全不透明 android:toAlpha=”0.0″设置动画结束透明度为0.0 表示完全透明 也就是说alpha的取值范围为0.0 – 1.0 之间 这个动画布局设置动画从完全不透明渐变到完全透明。XML/HTML代码 <?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1.0" android:toAlpha="0.0" android:repeatCount="infinite" android:duration="2000"> </alpha> 在代码中加载动画Java代码 mAnimation = new AlphaAnimation(1.0f, 0.0f); mAnimation.setDuration(2000); 代码实现Java代码 import android.app.Activity; import android.os.Bundle; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; public class AlphaActivity extends Activity { /**显示动画的ImageView**/ ImageView mImageView = null; /**透明动画**/ Animation mAnimation = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.translate); /**拿到ImageView对象**/ mImageView = (ImageView)findViewById(R.id.imageView); /**加载透明动画**/ mAnimation = AnimationUtils.loadAnimation(this, R.anim.alpha); /**播放透明动画**/ mImageView.startAnimation(mAnimation); } } 5.综合动画 可以将上面介绍的4种动画设置在一起同时进行播放,那么就须要使用<set>标签将所有须要播放的动画放在一起。 这个动画布局设置动画同时播放移动、渐变、旋转。XML/HTML代码 <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <rotate android:interpolator="@android:anim/accelerate_interpolator" android:fromDegrees="+360" android:toDegrees="0" android:pivotX="50%" android:pivotY="50%" android:duration="2000" android:repeatCount="infinite" /> <alpha android:fromAlpha="1.0" android:toAlpha="0.0" android:repeatCount="infinite" android:duration="2000"> </alpha> <translate android:fromXDelta="0" android:toXDelta="320" android:fromYDelta="0" android:toYDelta="480" android:duration="2000" android:repeatCount="infinite" /> </set> 代码实现Java代码 import android.app.Activity; import android.os.Bundle; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; public class AllActivity extends Activity { /**显示动画的ImageView**/ ImageView mImageView = null; /**综合动画**/ Animation mAnimation = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.translate); /**拿到ImageView对象**/ mImageView = (ImageView)findViewById(R.id.imageView); /**加载综合动画**/ mAnimation = AnimationUtils.loadAnimation(this, R.anim.all); /**播放综合动画**/ mImageView.startAnimation(mAnimation); } } 源码下载地址:http://vdisk.weibo.com/s/aamcS阅读全文 »
Android 应用开发推荐书单
分类标签: Android好书推荐
开发入门》 Beginning Android Games (作者:Mario Zechner) 对那些喜欢Android游戏的开发者,本书将是最好的入门。Mario Zechner在书中描述了Android游戏开发的前景。本书从游戏开发的最基本概念开始,比如接收用户输入、获取设备资源、播放音乐以及把游戏屏幕上展示。还涉及了通过OpenGL渲染游戏中的图像数据相关知识。同样,你还可以从书中获得每一个例子可运行的完整阅读全文 »
Android游戏开发教程之十八:AnimationDrawble动画
分类标签: 动画
Android开发中在制作2D帧动画中提供了使用XML配置动画文件的方式绘制,也就是说Android底层提供了动画播放的接口,那么我们分析一下如何调用它的接口来绘制动画。首先在工程res资源文件夹下创建anim动画文件夹,在这个文件夹中建立一个animation.xml文件, 这样它的路径就为re/anim/animation.xml。 Android开发中在制作2D帧动画中提供了使用XML配置动画文件的方式绘制,也就是说Android底层提供了动画播放的接口,那么我们分析一下如何调用它的接口来绘制动画。首先在工程res资源文件夹下创建anim动画文件夹,在这个文件夹中建立一个animation.xml文件, 这样它的路径就为re/anim/animation.xml。 看看内容应该是很好理解的,<animation-list>为动画的总标签,这里面放着帧动画 <item>标签,也就是说若干<item>标签的帧 组合在一起就是帧动画了。<animation-list > 标签中android:oneshot=”false” 这是一个非常重要的属性,默认为false 表示 动画循环播放, 如果这里写true 则表示动画只播发一次。 <item>标签中记录着每一帧的信息android:drawable=”@drawable/a”表示这一帧用的图片为”a”,下面以此类推。 android:duration=”100″ 表示这一帧持续100毫秒,可以根据这个值来调节动画播放的速度。XML/HTML代码 <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@drawable/a" android:duration="100" /> <item android:drawable="@drawable/b" android:duration="100" /> <item android:drawable="@drawable/c" android:duration="100" /> <item android:drawable="@drawable/d" android:duration="100" /> <item android:drawable="@drawable/e" android:duration="100" /> <item android:drawable="@drawable/f" android:duration="100" /> <item android:drawable="@drawable/g" android:duration="100" /> <item android:drawable="@drawable/h" android:duration="100" /> <item android:drawable="@drawable/i" android:duration="100" /> <item android:drawable="@drawable/j" android:duration="100" /> </animation-list> 下面这个例子的内容为 播放动画 与关闭动画 、设置播放类型 单次还是循环、拖动进度条修改动画的透明度,废话不多说直接进正题~~XML/HTML代码 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" > <Button android:id="@+id/button0" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="播放动画" /> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="停止动画" /> </LinearLayout> <RadioGroup android:id="@+id/radiogroup" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <RadioButton android:id="@+id/checkbox0" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="true" android:text="单次播放" /> <RadioButton android:id="@+id/checkbox1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="循环播放" /> </RadioGroup> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="拖动进度条修改透明度(0 - 255)之间" /> <SeekBar android:id="@+id/seekBar" android:layout_width="fill_parent" android:layout_height="wrap_content" android:max="256" android:progress="256"/> <ImageView android:id="@+id/imageView" android:background="@anim/animation" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> 这是一个比较简单的布局文件,应该都能看懂吧。 我主要说一下 最后的这个 ImageView, 它就是用来显示我们的动画。 这里使用android:background=”@anim/animation”设置这个ImageView现实的背景为一个动画,动画资源的路径为res/anim/animation.xml ,当然 设置background同样也可以在代码中设置。Java代码 imageView.setBackgroundResource(R.anim.animation); 通过getBackground方法就可以拿到这个animationDrawable对象。Java代码 /**拿到ImageView对象**/ imageView = (ImageView)findViewById(R.id.imageView); /**通过ImageView对象拿到背景显示的AnimationDrawable**/ animationDrawable = (AnimationDrawable) imageView.getBackground(); AnimationDrawable 就是用来控制这个帧动画,这个类中提供了很多方法。 animationDrawable.start(); 开始这个动画 animationDrawable.stop(); 结束这个动画 animationDrawable.setAlpha(100);设置动画的透明度, 取值范围(0 – 255) animationDrawable.setOneShot(true); 设置单次播放 animationDrawable.setOneShot(false); 设置循环播放 animationDrawable.isRunning(); 判断动画是否正在播放 animationDrawable.getNumberOfFrames(); 得到动画的帧数。 将这个例子的完整代码贴上Java代码 import android.app.Activity; import android.graphics.drawable.AnimationDrawable; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; public class SimpleActivity extends Activity { /**播放动画按钮**/ Button button0 = null; /**停止动画按钮**/ Button button1 = null; /**设置动画循环选择框**/ RadioButton radioButton0= null; RadioButton radioButton1= null; RadioGroup radioGroup = null; /**拖动图片修改Alpha值**/ SeekBar seekbar = null; /**绘制动画View**/ ImageView imageView = null; /**绘制动画对象**/ AnimationDrawable animationDrawable = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple); /**拿到ImageView对象**/ imageView = (ImageView)findViewById(R.id.imageView); /**通过ImageView对象拿到背景显示的AnimationDrawable**/ animationDrawable = (AnimationDrawable) imageView.getBackground(); /**开始播放动画**/ button0 = (Button)findViewById(R.id.button0); button0.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { /**播放动画**/ if(!animationDrawable.isRunning()) { animationDrawable.start(); } } }); /**停止播放动画**/ button1 = (Button)findViewById(R.id.button1); button1.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { /**停止动画**/ if(animationDrawable.isRunning()) { animationDrawable.stop(); } } }); /**单次播放**/ radioButton0 = (RadioButton)findViewById(R.id.checkbox0); /**循环播放**/ radioButton1 = (RadioButton)findViewById(R.id.checkbox1); /**单选列表组**/ radioGroup = (RadioGroup)findViewById(R.id.radiogroup); radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup radioGroup, int checkID) { if(checkID == radioButton0.getId()) { //设置单次播放 animationDrawable.setOneShot(true); }else if (checkID == radioButton1.getId()) { //设置循环播放 animationDrawable.setOneShot(false); } //发生改变后让动画重新播放 animationDrawable.stop(); animationDrawable.start(); } }); /**监听的进度条修改透明度**/ seekbar = (SeekBar)findViewById(R.id.seekBar); seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean frameTouch) { /**设置动画Alpha值**/ animationDrawable.setAlpha(progress); /**通知imageView 刷新屏幕**/ imageView.postInvalidate(); } }); } } 拖动进度条设置Alpha值的时候 一定要使用 imageView.postInvalidate(); 方法来通知UI线程重绘屏幕中的imageView 否则会看不到透明的效果 。这里切记切记~~ 下载地址:http://vdisk.weibo.com/s/aalv0阅读全文 »
Android游戏开发教程之十七:横竖屏的切换
分类标签: 屏幕
玩过Android手机的同学们应该都很清楚手机横竖屏的机制吧,大部分游戏都是无法横竖屏切换的, 有的游戏只能竖屏的玩,有的游戏只能横屏玩,为什么开发中要强制游戏为单一的屏幕呢?原因很简单 就是因为切换屏幕后带来的问题过多开发起来过于麻烦所以大多数游戏都会强制横屏或者竖屏。的今天我用一个小例子带同学们盘点一下Android开发中横竖屏切换的一些开发技巧。 玩过Android手机的同学们应该都很清楚手机横竖屏的机制吧,大部分游戏都是无法横竖屏切换的, 有的游戏只能竖屏的玩,有的游戏只能横屏玩,为什么开发中要强制游戏为单一的屏幕呢?原因很简单 就是因为切换屏幕后带来的问题过多开发起来过于麻烦所以大多数游戏都会强制横屏或者竖屏。的今天我用一个小例子带同学们盘点一下Android开发中横竖屏切换的一些开发技巧。 1. 强制横竖屏 这种方式是最为简单并且可以避免因为切换屏幕导致的一些开发问题,强制屏幕为横屏或者竖屏可以用两种方式来实现, 第一种为代码实现、第二种为配置文件实现,请同学们阅读下面这段简单的例子。Java代码 import android.app.Activity; import android.content.pm.ActivityInfo; import android.os.Bundle; public class HandlerActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.handler); //强制为横屏 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); super.onCreate(savedInstanceState); } } AndroidManifest.xml 中设置强制横屏XML/HTML代码 <activity android:name=".HandlerActivity" android:screenOrientation="landscape"/> 通过代码的方式强制为竖屏Java代码 public class HandlerActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.handler); //强制为竖屏 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); super.onCreate(savedInstanceState); } } AndroidManifest.xml 中设置强制竖屏XML/HTML代码 <activity android:name=".HandlerActivity" android:screenOrientation="portrait"/> 2. 横竖屏切换的响应 如果没有设置强制横屏或者竖屏那么每次横竖屏切换的时候Activity都会被重新创建一次,这样就会存在一个问题 横屏后会把图片拉伸可能在竖屏时感觉很协调的界面切换到横屏以后会被拉伸的很难看,比较好的解决办法是为横竖屏做两套图做两个布局文件,这样子程序中只需要监听屏幕切换的结果 设置不同的布局 绘制不同的图片即可。XML/HTML代码 <activity android:name=".HandlerActivity" android:configChanges="orientation¦keyboardHidden"/> 设置后屏幕切换后就不会再次调用OnCreate()方法重新创建这个Activity, 切换屏幕之前程序中的一些数据或者内容就不会因为重新创建Activity导致重置清空。 每当切换横竖屏的时候系统会自己调用onConfigurationChanged()方法这样子就可以拿到当前屏幕的横竖屏状态,根据状态就可以做我们想做的事。Java代码 import android.app.Activity; import android.content.res.Configuration; import android.os.Bundle; public class HandlerActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.handler); super.onCreate(savedInstanceState); } @Override public void onConfigurationChanged(Configuration newConfig) { int type = this.getResources().getConfiguration().orientation; if (type == Configuration.ORIENTATION_LANDSCAPE) { //切换到了横屏 } else if (type == Configuration.ORIENTATION_PORTRAIT) { //切换到了竖屏 } super.onConfigurationChanged(newConfig); } } 3. 横竖屏切换数据的保存 如果说项目必需要实现横竖屏的切换那么我们就须要对数据进行保存与读取,这也是为什么大部分游戏都不支持横竖屏切换的原因,因为游戏中存在大量的数据 ,比如玩家属性、坐标点、游戏状态、人物属性等等等等实在是太多了。如果要实现正常切换的话那么这些数据都须要进行保存与记录,其实相对软件开发来说做横竖屏切换的到挺多的,毕竟软件当前Activity中保存的数据不会有游戏那么多呵呵,废话就不多说了我分享一个横竖屏切换保存数据与读取数据的方法。 比如横屏切换竖屏实际上是先把当前的横屏的Activity杀掉 然后重新创建一个竖屏的Activity,我们可以使用onSaveInstanceState()方法保存数据,它是在横屏Activity将杀死前调用,可以将须要保存的数据放入Bundle封装在系统中,切换竖屏后这个Activity又重新被创建 这样可以在onCreate(Bundle)或者onRestoreInstanceState(Bundle)方法中来回复之前保存在Bundle中的数据,这样就可以实现横竖屏界面切换数据的保存与读取,当然前提是只能保存Bundle类型的数据,也就是说大量的对象数据的话就要想其它办法来恢复。 AndroidManifest.xml 中设置属性禁止重新创建Activity,并且添加屏幕切换监听。Java代码 import android.app.Activity; import android.os.Bundle; import android.util.Log; public class HandlerActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.handler); super.onCreate(savedInstanceState); } /**切换屏幕之前将数据保存在Bundle中**/ @Override protected void onSaveInstanceState(Bundle outState) { long outTime = System.currentTimeMillis(); //屏幕切换将当前的时候保存在Bundle中 outState.putLong("time", outTime); Log.v("InstanceState", "outTime is "+ outTime); super.onSaveInstanceState(outState); } /**切换屏幕之后在Bundle中把数据取出来**/ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { //取出屏幕切换前保存的时间 Long saveTime = savedInstanceState.getLong("time"); Log.v("InstanceState", "saveTime is "+ saveTime); super.onRestoreInstanceState(savedInstanceState); } } 看一下效果图红框内的Log信息,outTime 为很横竖切换屏幕之前保存的当前时间,SaveTime为横竖切换屏幕之后读取到之前保存的时间。通过这种方法可以正常的保存与读取数据。阅读全文 »