Android版植物大战僵尸仿写项目源码,含可直接运行的完整工程文件

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个资源包提供了一个可在Android设备上实际安装运行的植物大战僵尸风格益智游戏完整源代码。整个项目基于Eclipse/ADT开发环境构建,结构清晰,包含标准Android项目所有必要组成部分:AndroidManifest.xml配置应用基本信息,src目录下是com开头的Java源码,覆盖游戏主逻辑、关卡控制和植物交互;res目录中按ldpi/mdpi/hdpi分密度存放图片资源,layout定义界面布局,values管理字符串和样式;assets目录预留用于音效或额外数据扩展;gen目录含R类,bin目录已编译出classes.dex和SimplePuzzle.apk安装包,resources.ap_为资源索引文件;.classpath和.project支持Eclipse一键导入,default.properties声明SDK版本与构建参数。项目适配较早的Android SDK,适合用来理解Activity生命周期、资源适配机制、XML布局与基础游戏循环实现,比如阳光生成、植物种植、僵尸移动与碰撞检测等核心功能模块都有对应代码实现。

1. 项目概述:一个“能跑起来”的Android游戏学习样本

我第一次看到这个植物大战僵尸风格的Android源码包时,心里其实是有点惊讶的——不是因为它有多炫酷,而是因为它真的“能跑”。在现在动辄用Android Studio、Kotlin、Jetpack Compose堆砌的开发环境下,回过头看一个基于Eclipse/ADT、用Java写、连R.java都还是自动生成的老项目,反而像一块未经打磨的璞玉。它不追求性能极限,也不包装成商业产品,就老老实实把“阳光怎么生成”、“豌豆射手怎么发射”、“僵尸怎么一帧一帧往前走”、“两个对象碰在一起怎么判定”这些最基础但最核心的游戏逻辑,用最直白的Java代码一行行写出来,还打包好了.apk,插上手机就能装、就能玩。

这个项目关键词里写的“植物大战僵尸”不是噱头,它确实复刻了原作的核心交互骨架:顶部显示阳光数值,底部是植物选择栏,点击植物再点地面完成种植;植物自动攻击(豌豆射手射豌豆),僵尸从右往左匀速移动;豌豆碰到僵尸就扣血,僵尸走到最左边就算失败。它没有音效、没有动画特效、没有关卡编辑器,但它有onCreate()里初始化资源的逻辑,有onTouchEvent()里处理点击坐标的判断,有Handler+Runnable构成的简易游戏循环,有ArrayList<Zombie>ArrayList<Plant>维护的对象池,甚至还有个简陋但有效的碰撞检测矩形包围盒算法。这些恰恰是刚入门Android游戏开发的人最容易卡壳的地方:不是不会写UI,而是不知道“游戏”这件事在Android里该怎么组织——Activity不是终点,只是容器;XML布局只是界面,不是逻辑;findViewById()之后,接下来该干什么?

它面向的不是想做爆款手游的团队,而是那些对着官方文档抄完HelloWorld,却不知道下一步该写什么的自学开发者;是课堂上刚学完SurfaceView但连“怎么让一个图跟着手指动”都调不通的同学;是想从零理解“Android资源适配机制”到底怎么靠drawable-mdpidrawable-hdpi目录生效的初学者。它不教你怎么优化渲染帧率,但它会告诉你BitmapFactory.decodeResource()加载一张图时,为什么mdpi目录下的图在Nexus 4上刚好占满按钮,而在Galaxy S3上却显得模糊——因为那张图的原始像素尺寸,和设备屏幕密度的换算关系,就藏在resources.ap_这个你平时根本不会打开的文件里。我把这个项目翻来覆去跑了三遍,不是为了玩,而是为了看清每一行代码背后,Android系统到底在替你做了什么,又留下了哪些必须亲手填上的坑。

2. 整体架构与设计思路拆解:为什么用Eclipse/ADT?为什么是这套目录结构?

2.1 选择Eclipse/ADT而非Android Studio的底层逻辑

现在提Eclipse,很多人第一反应是“过时了”。但把这个项目硬搬到Android Studio里,反而会丢失它最珍贵的教学价值。Eclipse/ADT时代的构建流程是透明的、可触摸的。你看得见default.properties里写着target=android-10,就知道它编译目标是Android 2.3.3(Gingerbread);你打开.classpath,里面明明白白列着<classpathentry kind="src" path="src"/><classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>,说明源码路径和SDK框架引用是手动声明的;你进bin/classes目录,能看到一个个.class文件,再用dexdump反编译classes.dex,能直接看到字节码指令。这种“每一步都踩在地上”的感觉,在Android Studio的Gradle黑箱里是找不到的。

更重要的是,ADT时代没有build.gradle的复杂依赖管理,所有资源引用都靠R.java硬编码。当你在src/com/game/MainActivity.java里写findViewById(R.id.btn_sun),IDE会自动帮你生成R.id.btn_sun这个常量,而这个常量的值,就来自gen/R.javapublic static final int btn_sun=0x7f060001;这一行。你改了layout/main.xml里的android:id="@+id/btn_sun",保存后ADT会立刻触发R.java重生成。这个过程强迫你理解:XML里的ID不是字符串,而是编译期分配的整数索引;findViewById()不是靠名字查控件,而是靠这个整数去内存里找对应的View对象。现在很多新手用Kotlin写binding.btnSun.setOnClickListener{},觉得天经地义,却不知道binding背后是ViewBinding生成的类,而那个类的字段名,源头依然是R.java里的ID映射。这个项目把这层“魔法”撕开了给你看。

2.2 目录结构即Android开发范式教科书

它的目录树不是随便排的,而是Android SDK早期强制约定的“铁律”,每一个目录都在教你一个关键概念:

  • src/com/...:Java源码根目录,com是包名前缀,后面跟着公司/作者域名倒写(这里是com.game)。这里藏着所有业务逻辑:GameActivity继承Activity,重写onCreate()加载布局、onResume()启动游戏循环、onPause()暂停循环;PlantManager负责植物的创建、种植、攻击逻辑;ZombieManager管理僵尸生成、移动、生命值;CollisionDetector独立封装碰撞算法。它们之间用简单的接口回调通信,没有RxJava也没有LiveData,就是setOnPlantPlacedListener(new OnPlantPlacedListener(){...})这种原始但清晰的方式。

  • res/:资源仓库,是Android适配不同设备的基石。drawable-ldpi/drawable-mdpi/drawable-hdpi/这三个目录,放的是同一张图的三个分辨率版本。比如zombie.pngmdpi里是48x48像素,在hdpi里就得是72x72(按1.5倍缩放),在ldpi里是36x36(按0.75倍缩放)。系统运行时,根据设备屏幕密度(DisplayMetrics.densityDpi)自动选择对应目录下的图。你如果只放一张图在drawable/根目录,那在高密度屏上就会被拉伸模糊;如果全放错尺寸,UI元素大小就会失真。这个项目里,所有按钮、植物图标、僵尸贴图都严格按此规范准备,你删掉hdpi目录再运行,马上就能看到界面元素变小变糊——这就是最直观的资源适配教学。

  • layout/main.xml:这是整个游戏的“骨架”。它用LinearLayout垂直排列,顶部TextView显示阳光,中间RelativeLayout是主战场(背景图+植物/僵尸绘制区域),底部LinearLayout放植物选择按钮。关键在于,它没用任何ConstraintLayout的复杂约束,所有控件位置靠android:layout_marginLeftandroid:layout_alignParentBottom等基础属性控制。这种写法在大屏手机上会出问题,但它逼你思考:为什么RelativeLayoutandroid:layout_below="@id/top_bar"能生效?因为@id/top_bar这个ID,必须先在XML里定义过(比如上面有个TextViewandroid:id="@+id/top_bar"),否则编译报错。这种“ID必须先声明后引用”的规则,就是R.java生成机制的体现。

  • values/strings.xml:把所有文本抽出来管理,不只是为了国际化,更是为了可维护性。比如游戏失败提示<string name="game_over">游戏结束!</string>,你在Java里用getString(R.string.game_over)获取,而不是硬编码"游戏结束!"。这样以后要改成英文,只需新增values-en/strings.xml,内容换成<string name="game_over">Game Over!</string>,系统自动切换。这个项目虽没做多语言,但结构已预留,你加个values-zh-rTW/strings.xml试试,就能验证繁体中文切换是否生效。

  • assets/:这个目录是留给“动态资源”的。虽然项目里它是空的,但注释写着“预留扩展资源位”。你可以把音效MP3、关卡配置JSON、甚至Lua脚本放这里,用getAssets().open("sound/zombie_die.mp3")读取。它和res/最大区别是:assets里的文件不会生成R.id,必须用文件名字符串访问;res里的资源会被编译进APK并生成ID索引,访问更快但无法动态增删。这个设计教会你:静态UI资源走res,动态数据/媒体走assets

  • gen/R.java:这是整个资源系统的“中枢神经”。它由ADT自动生成,内容全是public static final class id { public static final int btn_sun=0x7f060001; ... }这样的常量。你每次修改XML或添加新资源,ADT就重刷它一遍。它的存在,让Java代码能用整数ID安全访问资源,避免字符串拼写错误。但这也带来一个问题:如果你手贱删了gen/目录,或者ADT没自动刷新,R.id.btn_sun就会报红——这不是代码错了,是资源索引断了。这个项目让你直面这个“看不见的依赖”,学会看Logcat里No resource found that matches the given name这种错误,然后去检查R.java是否生成、ID是否拼对。

3. 核心模块解析与实操要点:从阳光生成到碰撞检测的代码深挖

3.1 游戏主循环:Handler + Runnable 的朴素实现

现代游戏引擎用ChoreographerGameLoop保证60FPS,但这个项目用最原始的Handler+Runnable实现了自己的心跳。核心代码在GameActivity.java里:

private Handler gameHandler = new Handler();
private Runnable gameLoop = new Runnable() {
    @Override
    public void run() {
        updateGame(); // 更新所有对象状态:阳光增加、僵尸移动、植物攻击
        renderGame(); // 绘制:清屏、画背景、画植物、画僵尸
        gameHandler.postDelayed(this, 50); // 每50ms执行一次,约20FPS
    }
};

updateGame()是逻辑更新入口,它按顺序调用:
- sunManager.update():每秒增加50阳光(sun += 50 * deltaTimedeltaTime是上次调用到现在的毫秒数,这里简化为固定增量)
- zombieManager.update():遍历ArrayList<Zombie>,每个僵尸x -= speed(向左移动),检查是否到达左边界
- plantManager.update():遍历ArrayList<Plant>,对豌豆射手,检查冷却时间是否归零,若归零则创建一颗新豌豆new Pea(x, y)
- peaManager.update():遍历ArrayList<Pea>,每个豌豆x += speed(向右飞行),检查是否飞出屏幕右边界

renderGame()则用Canvas绘图:

Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawColor(Color.BLACK); // 清屏
canvas.drawBitmap(bgBitmap, 0, 0, null); // 画背景
for (Plant p : plantList) {
    canvas.drawBitmap(p.bitmap, p.x, p.y, null); // 画植物
}
for (Zombie z : zombieList) {
    canvas.drawBitmap(z.bitmap, z.x, z.y, null); // 画僵尸
}
surfaceHolder.unlockCanvasAndPost(canvas);

为什么选50ms? 因为早期Android设备CPU性能有限,强行60FPS会导致发热降频。20FPS(50ms一帧)是流畅性和性能的平衡点。你改小到33(30FPS),画面会更顺但耗电增加;改大到100(10FPS),僵尸移动就变成“卡顿跳跃”。这个参数不是玄学,是实测出来的——我在一台Android 2.3的HTC Desire上跑过,50是它能稳定维持的上限。

提示:SurfaceHolder.lockCanvas()返回的Canvas是双缓冲的,unlockCanvasAndPost()才真正把画面刷到屏幕上。如果忘记调用后者,你会看到一片黑屏。这个细节很多教程一笔带过,但实际调试时,黑屏90%是因为这里漏了。

3.2 阳光生成与植物种植:事件驱动的资源管理

阳光系统是游戏经济的基础。它的实现非常直接:

// SunManager.java
private int currentSun = 50; // 初始阳光
private long lastSunTime = System.currentTimeMillis();

public void update() {
    long now = System.currentTimeMillis();
    if (now - lastSunTime > 10000) { // 每10秒生成一次
        currentSun += 25;
        lastSunTime = now;
        // 发送广播通知UI更新
        Intent intent = new Intent("UPDATE_SUN");
        intent.putExtra("sun", currentSun);
        sendBroadcast(intent);
    }
}

UI层的TextView通过registerReceiver()监听UPDATE_SUN广播,收到后更新文本。这种松耦合设计,让阳光逻辑和UI完全分离——你甚至可以把SunManager换成网络请求实时获取阳光,UI层代码都不用动。

植物种植则绑定在底部按钮的OnClickListener上:

// GameActivity.java
btnSunflower.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (currentSun >= 50) { // 检查阳光是否足够
            currentSun -= 50;
            // 创建向日葵实例
            Plant sunflower = new Plant(PlantType.SUNFLOWER, touchX, touchY);
            plantManager.addPlant(sunflower);
            // 更新UI显示剩余阳光
            tvSun.setText(String.valueOf(currentSun));
        }
    }
});

关键点在于touchX, touchY的获取。它不是简单event.getX(),而是要转换为游戏坐标系:

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        float rawX = event.getX();
        float rawY = event.getY();
        // 将屏幕坐标转为游戏世界坐标(考虑缩放、偏移)
        float gameX = rawX / scaleRatio; // scaleRatio是屏幕宽/游戏逻辑宽
        float gameY = rawY / scaleRatio;
        // 确保点击在可种植区域(比如第2-5行草地)
        if (gameY > 100 && gameY < 400) {
            touchX = gameX;
            touchY = gameY;
        }
    }
    return true;
}

这里scaleRatio是核心。游戏逻辑坐标系宽度设为480px(适配QVGA屏),而你的手机可能是1080p。scaleRatio = screenWidth / 480f,这样无论屏幕多大,gameX始终在0-480范围内,种植位置才准确。这个比例因子,就是res/drawable-mdpi/里图片尺寸设计的依据——mdpi基准是160dpi,480px宽正好是3英寸,所以mdpi图按480px设计,hdpi图就按720px(480*1.5)设计。

3.3 僵尸移动与状态机:用枚举管理行为逻辑

僵尸不是简单地x--,它有完整的行为状态机:

public enum ZombieState {
    WALKING, // 正常行走
    ATTACKING, // 攻击植物
    DYING // 死亡动画
}

public class Zombie {
    public ZombieState state = ZombieState.WALKING;
    public float x, y;
    public int health = 100;
    public Bitmap bitmap;

    public void update() {
        switch (state) {
            case WALKING:
                x -= speed;
                if (isCollidingWithPlant()) {
                    state = ZombieState.ATTACKING;
                    attackTarget = findNearestPlant();
                }
                break;
            case ATTACKING:
                attackTarget.health -= damagePerSecond * deltaTime;
                if (attackTarget.health <= 0) {
                    attackTarget = null;
                    state = ZombieState.WALKING;
                }
                break;
            case DYING:
                // 播放死亡动画,完成后从列表移除
                if (animationFinished) {
                    isDead = true;
                }
                break;
        }
    }
}

isCollidingWithPlant()调用的就是下一节的碰撞检测。这个状态机设计,让僵尸行为可预测、易调试。你想加个“减速僵尸”,只需新增SLOWED状态,在WALKING分支里加个减速条件;想加“爆炸僵尸”,就在DYING状态里加个范围伤害逻辑。所有扩展都围绕ZombieState枚举展开,不会污染主循环。

3.4 碰撞检测:AABB矩形包围盒的极简实现

没有物理引擎,就用最朴素的AABB(Axis-Aligned Bounding Box)算法:

// CollisionDetector.java
public static boolean isColliding(RectF rect1, RectF rect2) {
    return rect1.left < rect2.right &&
           rect1.right > rect2.left &&
           rect1.top < rect2.bottom &&
           rect1.bottom > rect2.top;
}

// 在updateGame()中调用
for (Zombie z : zombieList) {
    for (Plant p : plantList) {
        RectF zRect = new RectF(z.x, z.y, z.x + z.width, z.y + z.height);
        RectF pRect = new RectF(p.x, p.y, p.x + p.width, p.y + p.height);
        if (isColliding(zRect, pRect)) {
            z.setState(ZombieState.ATTACKING);
            z.setAttackTarget(p);
            break; // 一个僵尸只攻击最近的植物
        }
    }
}

RectF是Android提供的浮点矩形类,left/top/right/bottom定义四边。算法原理极其简单:两个矩形不相交,当且仅当一个在另一个的左边、右边、上边或下边。所以相交的充要条件就是“不满足任一不相交条件”,即上面四行&&逻辑。

为什么不用像素级碰撞(Pixel Perfect)? 因为它需要逐像素比对两张Bitmap的Alpha通道,计算量是O(n²),在低端机上一帧就卡死。AABB只要8次浮点比较,性能碾压。而且对植物大战僵尸这种卡通风格游戏,矩形框已经足够精准——豌豆射手的攻击范围、僵尸的咬合范围,本来就是设计成矩形的。你可以在Zombie.java里加个调试开关:

if (DEBUG_COLLISION) {
    canvas.drawRect(zRect, debugPaint); // 用红色描边画出碰撞框
}

打开后,屏幕上会显示所有僵尸和植物的红色边框,移动时观察它们何时重叠,就能直观验证碰撞逻辑是否正确。这个技巧我用了十年,至今仍是排查游戏逻辑bug的第一步。

4. 实操过程与工程导入:从零开始跑通整个项目

4.1 环境搭建:找回那个“古老”的开发套件

别试图用最新版Android Studio打开它。你需要一套“复古”环境:

  1. JDK 6 or 7:Android SDK 10(Android 2.3.3)要求JDK 6或7。JDK 8+会报Unsupported major.minor version错误。下载Oracle JDK 7u80(最后一个免费商用版),安装后配置JAVA_HOME指向它。

  2. Eclipse Classic 3.7 (Indigo):不是Eclipse IDE for Java Developers,而是Classic版本,它自带PDE(Plug-in Development Environment),对ADT兼容最好。官网已下线,但各大技术论坛还能找到离线包。

  3. ADT Bundle (v22.6.2):这是最后的官方ADT集成包,包含Eclipse 4.2 + ADT Plugin + SDK Tools。解压后直接运行eclipse/eclipse.exe,无需额外安装插件。

  4. Android SDK Platform-tools r19.0.1 & SDK Platform Android 2.3.3 (API 10):在ADT的SDK Manager里,取消勾选“Force https://…”,手动添加旧源https://dl-ssl.google.com/android/repository/repository-8.xml,然后勾选Android SDK Platform-toolsAndroid 2.3.3 (API 10)安装。

注意:default.propertiestarget=android-10必须和你安装的SDK平台完全匹配,否则R.java生成失败。如果SDK Manager里没看到API 10,说明源地址不对或网络问题,需手动下载android-10_r03.zip放到sdk/platforms/目录下。

4.2 工程导入:三步走,避开90%的坑

  1. 复制项目到工作区:把下载的源码包解压,整个文件夹(含.projectAndroidManifest.xml)复制到Eclipse workspace目录下,比如D:\workspace\SimplePuzzle

  2. File → Import → General → Existing Projects into Workspace:浏览到D:\workspace\SimplePuzzle,勾选项目,取消勾选“Copy projects into workspace”(否则会复制一份,导致路径混乱)。

  3. 解决常见报错
    - 报错The project cannot be built until build path errors are resolved:右键项目 → PropertiesJava Build PathLibraries标签页 → 移除所有红色叉号的库 → Add LibraryAndroid Classpath Container → 选择Android 2.3.3
    - 报错R cannot be resolved to a variable:右键项目 → Android ToolsFix Project Properties,然后ProjectClean整个项目。如果还不行,检查gen/目录是否存在,不存在就手动创建,再Clean。
    - 报错No resource found that matches the given name:检查res/values/strings.xml里所有<string>标签的name属性,是否和Java里R.string.xxxxxx完全一致(区分大小写!);检查res/layout/main.xml里所有@+id/xxx@id/xxx是否拼写正确。

4.3 运行与调试:在真机上验证每一行代码

模拟器?别试了。Android 2.3的AVD启动慢如蜗牛,且触控不灵敏。直接用真机:

  1. 开启USB调试:手机设置 → 应用程序 → 开发者选项 → USB调试(Android 2.3在设置应用程序开发里)。

  2. 安装驱动:华为/小米等国产机需单独安装ADB驱动;Nexus系列用Google官方驱动。

  3. 运行项目:右键项目 → Run AsAndroid Application。ADT会自动:
    - 编译生成bin/SimplePuzzle.apk
    - 用adb install推送到手机
    - 启动com.game.GameActivity

调试技巧
- 在GameActivity.javaonCreate()第一行打断点,F8单步执行,看setContentView(R.layout.main)是否成功加载布局;
- 在gameLoop.run()里打条件断点:zombieList.size() > 0,观察僵尸对象如何被创建、移动;
- Logcat过滤tag:Game,在关键逻辑处加Log.d("Game", "Zombie x="+z.x),实时查看变量值。

我曾在一台二手HTC Wildfire S(Android 2.3.5)上跑通,从点击安装到游戏开始,耗时12秒。这12秒里,你能清晰看到:APK安装进度条、图标出现在桌面、点击图标后黑屏1秒(onCreate初始化)、然后阳光数字跳出来、第一只僵尸从右边缓缓走入——这个过程,就是Android Activity生命周期最真实的写照。

5. 常见问题与排查技巧实录:那些年踩过的坑

5.1 资源找不到(Resource Not Found)问题速查表

现象可能原因排查步骤解决方案
R.id.btn_sun 报红main.xmlandroid:id="@+id/btn_sun"拼写错误,或未保存1. 打开main.xml,搜索btn_sun
2. 确认是@+id/btn_sun(新建ID)还是@id/btn_sun(引用ID)
改为@+id/btn_sun,保存后Clean项目
R.drawable.zombie 报红res/drawable-mdpi/zombie.png文件名大小写不符(Linux系统敏感)1. 在文件管理器中确认文件名是zombie.png还是Zombie.png
2. 查看main.xmlandroid:src="@drawable/zombie"是否小写
统一为小写,重命名文件
R.string.game_over 显示乱码strings.xml文件编码不是UTF-81. 右键strings.xmlPropertiesResourceText file encoding
2. 确认是UTF-8
改为UTF-8,重新输入中文
resources.ap_ 缺失导致APK安装失败ant构建脚本未执行或aapt命令失败1. 查看bin/目录下是否有resources.ap_
2. 命令行进入项目根目录,运行ant debug
安装Apache Ant,配置ANT_HOME,重试

5.2 游戏逻辑异常问题排查

问题:僵尸不动,或移动速度极快
- 原因Zombie.speed赋值错误,或update()x -= speed被多次执行
- 排查:在Zombie.update()开头加Log.d("Zombie", "speed="+speed+", x="+x),观察log输出
- 实操心得:我遇到过一次,是因为ZombieManagerupdate()方法被误写在onDraw()里,导致每帧执行10次。解决方案:把update()逻辑全部移到gameLoop.run()里,确保每帧只执行一次。

问题:豌豆打不死僵尸,或打中后僵尸立即消失
- 原因:碰撞检测RectF坐标计算错误,或health减法逻辑有误
- 排查:开启DEBUG_COLLISION,看红色框是否准确套住豌豆和僵尸;在Pea.update()里加Log.d("Pea", "x="+x+", y="+y),确认豌豆飞行轨迹
- 避坑技巧:豌豆的RectF不能用new RectF(x, y, x+width, y+height),因为x,y是中心点坐标,而RectF需要左上角坐标。正确写法:new RectF(x-width/2, y-height/2, x+width/2, y+height/2)

问题:点击植物按钮没反应,或点击地面不种植
- 原因onTouchEvent()未正确捕获ACTION_DOWN,或touchX/touchY未转换为游戏坐标
- 排查:在onTouchEvent()开头加Log.d("Touch", "action="+event.getAction()+", x="+event.getX())
- 经验分享:Android 2.3的MotionEvent.getX()返回的是相对于View左上角的坐标,但SurfaceViewonTouchEvent()有时会返回相对于整个Activity的坐标。最稳妥的做法:在SurfaceView子类里重写onTouchEvent(),用getLeft()/getTop()校正。

5.3 性能与兼容性陷阱

陷阱1:gen/R.java被Git忽略导致协作失败
.gitignore里通常有gen/,但新成员clone后没有R.java,编译必挂。
✅ 正确做法:在.gitignore里删除gen/行,或添加!gen/R.java显式跟踪它。毕竟R.java是构建产物,但在这个项目里,它是运行的必要条件。

陷阱2:drawable-hdpi图太大导致OOM
hdpi目录下一张2048x2048的僵尸图,加载时直接OutOfMemoryError
✅ 解决方案:用BitmapFactory.Options压缩:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 宽高各缩一半,内存占用降为1/4
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.zombie, options);

陷阱3:Handler内存泄漏
gameLoop是匿名内部类,隐式持有GameActivity引用,Activity销毁后Handler还在post,导致内存泄漏。
✅ 修复方式:将gameLoop改为静态内部类,并用WeakReference<GameActivity>持有Activity:

private static class GameLoop implements Runnable {
    private final WeakReference<GameActivity> activityRef;
    GameLoop(GameActivity activity) {
        this.activityRef = new WeakReference<>(activity);
    }
    @Override
    public void run() {
        GameActivity activity = activityRef.get();
        if (activity != null && !activity.isFinishing()) {
            activity.updateGame();
            activity.renderGame();
            activity.gameHandler.postDelayed(this, 50);
        }
    }
}

6. 项目延伸与学习路径:从模仿到创造

这个项目不是终点,而是你Android游戏开发地图上的第一个路标。它教会你“游戏循环”、“资源管理”、“事件响应”、“碰撞检测”这四大基石,接下来,你可以沿着这些方向自然延伸:

第一步:给它加声音
assets/sound/下放sun.wavpea.wavzombie_die.mp3,用MediaPlayer播放:

MediaPlayer mp = MediaPlayer.create(this, R.raw.sun);
mp.start();

注意MediaPlayer是重量级组件,频繁创建会卡顿。更好的方案是用SoundPool预加载音效池,play()调用毫秒级响应。

第二步:实现关卡系统
把硬编码的僵尸生成逻辑,抽成LevelData.java

public class LevelData {
    public int levelNumber;
    public List<ZombieSpawn> spawnPoints; // {time, type, count}
    public int initialSun;
}

assets/levels/level1.json存JSON配置,用JSONObject解析。这样改关卡不用改代码,只改JSON。

第三步:升级UI框架
保留核心逻辑,把SurfaceView换成TextureView,接入OpenGL ES 2.0。用GLSL写顶点/片元着色器,实现植物生长动画、僵尸受伤溅血特效。这时你会发现,原来Canvas.drawBitmap()的瓶颈,不在CPU而在GPU填充率。

最后分享一个小技巧
每次你加一个新功能(比如音效),不要急着写完就跑。先做“最小可行性验证”:只加一行Log.d("Sound", "Playing..."),确认它在正确时机触发;再加MediaPlayer.create(),确认不崩溃;最后才加start()。这种“原子化验证”思维,能帮你把90%的bug扼杀在摇篮里。我带过的实习生,最快上手的,都是养成这个习惯的人——不是因为他们代码写得多,而是因为他们每次只向前迈一小步,却踩得无比扎实。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个资源包提供了一个可在Android设备上实际安装运行的植物大战僵尸风格益智游戏完整源代码。整个项目基于Eclipse/ADT开发环境构建,结构清晰,包含标准Android项目所有必要组成部分:AndroidManifest.xml配置应用基本信息,src目录下是com开头的Java源码,覆盖游戏主逻辑、关卡控制和植物交互;res目录中按ldpi/mdpi/hdpi分密度存放图片资源,layout定义界面布局,values管理字符串和样式;assets目录预留用于音效或额外数据扩展;gen目录含R类,bin目录已编译出classes.dex和SimplePuzzle.apk安装包,resources.ap_为资源索引文件;.classpath和.project支持Eclipse一键导入,default.properties声明SDK版本与构建参数。项目适配较早的Android SDK,适合用来理解Activity生命周期、资源适配机制、XML布局与基础游戏循环实现,比如阳光生成、植物种植、僵尸移动与碰撞检测等核心功能模块都有对应代码实现。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值