news 2026/9/29 11:06:09

用C++复刻植物大战僵尸:核心战斗模型与代码实现详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用C++复刻植物大战僵尸:核心战斗模型与代码实现详解

简介:一份基于C++实现的植物大战僵尸模型与完整工程代码,适合C++初学者和游戏开发爱好者学习。压缩包共109个文件,大小约15.82MB,包含Visual Studio工程文件(sln/vcxproj)、C++源码、编译生成的exe可执行程序,以及调试所需的pdb、obj等中间文件;同时附有游戏运行所需的mp3音频、金币阳光等素材,完整覆盖从源码到可运行程序的链路。目前已有5879人学习下载,热度可见一斑。这份资源能帮助读者直观理解面向对象设计、游戏主循环、碰撞检测、资源管理等核心知识点;通过阅读源码并运行exe对比实际效果,可快速掌握塔防游戏的基础实现思路。对于需要课程设计或入门游戏开发练手的读者,是相当实用的参考工程。

1. 用C++复刻植物大战僵尸:模型和代码的核心边界

我刚开始练C++的时候,也想过做一个植物大战僵尸的完整游戏。结果贴图、音效、动画一上来,代码就乱了,最后连一个豌豆射手都打不死僵尸。后来我才想明白,最值钱的部分不是画面,而是背后的模型:植物什么时候射击,子弹往哪飞,僵尸怎么扣血,冷却怎么计时。这套东西如果立不住,换再好看的素材也是白搭。

这篇笔记不讲怎么用SDL渲染一棵向日葵,而是专注于“模型和代码”本身——用C++把植物大战僵尸的核心战斗逻辑拆开,让你在不依赖图形库的情况下,先把可运行的逻辑骨架跑起来。你会看到一个能直接编译的最小战斗模拟,也会看到每个关键参数该怎么调,以及我踩过的五个坑。适合已经学过C++语法、想做个像样项目的新手,也适合拿来当课程设计的第一版原型。

2. 先搭对象模型:植物、僵尸、子弹的类设计与数据流

2.1 基类与继承:让植物和僵尸共享“战斗单位”的接口

写任何游戏逻辑之前,先把战斗单位抽象出来。植物和僵尸虽然行为完全不一样,但它们都有行、列、生命值、攻击伤害这四样东西。所以我设计了一个Entity基类,再用Peashooter和Zombie去继承它。这样做的好处是,你可以把植物和僵尸放在同一个容器里,统一用基类指针调用update,后面做范围判定、死亡清理都会简单很多。

class Entity { public: Entity(int row, int col, int hp, int damage) : row(row), col(col), hp(hp), maxHp(hp), attackDamage(damage) {} virtual ~Entity() = default; // 每帧更新,具体行为由子类决定 virtual void update(double dt) = 0; bool alive() const { return hp > 0; } int row; int col; int hp; int maxHp; int attackDamage; }; class Peashooter : public Entity { public: Peashooter(int row, int col) : Entity(row, col, 100, 20) {} void update(double dt) override { // 射击计时逻辑写在这里 } }; class Zombie : public Entity { public: Zombie(int row, int col) : Entity(row, col, 100, 10) {} void update(double dt) override { // 向左移动,走到防线就攻击植物 col -= 1; } };

这个例子里,坐标先用int是因为网格是离散的。但现实中的僵尸移动需要一个连续位置,不然动画会一顿一顿。所以实际项目里,我会把row保持为整数表示“在第几行”,另外加一个double x表示“当前水平位置”。这两个字段不要合并成一个,否则后面子弹碰撞判定会非常别扭。

为什么用纯虚函数?因为植物和僵尸的update确实不同。你不需要担心虚函数性能,这个规模下开销可以忽略,先把结构做对,再去想优化。这里还有一个容易忽略的点:析构函数要声明成virtual,这样通过基类指针delete派生对象时,才不会只析构一半。

2.2 网格地图的选型:二维数组还是vector?

地图是5行9列,新手第一反应是写一个二维数组。但对于一个会持续增删对象的游戏,二维数组删除元素很麻烦,你只能把某个格子标记成空位。所以我更推荐用vector嵌套,每个格子放一个unique_ptr,空位就是nullptr:

// 5行9列,每个格子放一个unique_ptr,空位是nullptr std::vector<std::vector<std::unique_ptr<Entity>>> grid( 5, std::vector<std::unique_ptr<Entity>>(9));

这里有一个很重要的设计决定:僵尸是移动单位,不适合被绑死在网格里。如果你把僵尸也塞进grid,那么每一帧移动后都要重新计算它属于哪个格子,还要担心两个单位挤在同一格,纯属给自己找麻烦。

我一般把场景拆成两层:静态单位(植物)放进grid,动态单位(僵尸)按行管理。僵尸身上保留row字段,表示它在哪条横线上运动,x字段表示它在横线上的具体位置。子弹、僵尸移动、攻击判定都发生在这套数据结构上。

std::vector<std::vector<std::unique_ptr<Entity>>> plantGrid; // 只放植物 std::vector<std::vector<Zombie>> zombiesByRow; // 每行一个僵尸列表

这种选型的最大好处在战斗判定:子弹只检查自己所在行的僵尸列表,不用全图遍历。当一张地图上有几十个僵尸时,这个差异非常明显。

2.3 游戏主循环:用固定时间步长把逻辑和渲染分开

新手写游戏循环通常是while (true) { update(); render(); },然后Sleep(16)。这个写法在低帧率电脑上逻辑变慢,在高帧率电脑上逻辑变快,同一个代码在不同机器上难度完全不同。植物大战僵尸这种塔防游戏,玩家对时间流逝极敏感,僵尸什么时候走到防线必须稳定。

所以我在项目里用的是固定时间步长累加器:

#include <chrono> double fixedTimeStep = 1.0 / 60.0; // 每秒60次逻辑更新 double accumulator = 0.0; auto lastTime = std::chrono::steady_clock::now(); while (running) { auto now = std::chrono::steady_clock::now(); accumulator += std::chrono::duration<double>(now - lastTime).count(); lastTime = now; while (accumulator >= fixedTimeStep) { update(fixedTimeStep); accumulator -= fixedTimeStep; } render(); }

这里update收到的dt永远是1/60秒,跟屏幕刷新率是60Hz还是144Hz无关。如果某帧卡了一下,accumulator会累积多个fixedTimeStep,下一帧会补上之前欠的逻辑更新。子弹速度、植物冷却、僵尸移动都基于这个dt,所以只要dt固定,逻辑就是一致且可复现的。

如果你直接把真实帧间隔当dt,一旦渲染掉帧,子弹单帧位移变大,就可能从僵尸身上穿过去。第5章会重点讲这个翻车现场,主循环这里就是第一道防线。

3. 关键机制实现:阳光生产、攻击判定与子弹飞行

3.1 阳光的生成与收集:定时器与碰撞盒

阳光是植物大战僵尸的资源循环核心。向日葵每隔一段时间生成一个阳光,阳光掉落一段时间后可以被收集。这个机制本身不复杂,但有两个点容易写乱:生成计时和收集判定。

我习惯把阳光定义成一个独立结构体,因为它既不是植物也不是僵尸,只是一个飘落的“资源物件”:

struct Sun { double x, y; // 当前坐标 double fallSpeed; // 每秒下落多少格 double lifeTime; // 剩余存活时间 bool collected; // 是否已被收集 }; std::vector<Sun> suns;

生成逻辑直接写在向日葵的update里,用local timer累加dt。到时间了就push一个Sun,然后重置计时器:

sunTimer += dt; if (sunTimer >= sunInterval) { sunTimer -= sunInterval; suns.push_back({col, row, 1.2, 8.0, false}); }

这里的参数含义是:sunInterval是生成间隔,通常8秒一个;1.2是下落速度,代表每秒移动1.2格;8.0是存活时间,超过8秒还没被收集的阳光会消失。这三个值直接影响玩家前期能否顺利种下第一株植物,建议先用保守值测试。

收集判定最简单的做法是:给每个阳光一个圆形碰撞盒,半径0.4格。玩家点击时,计算出点击坐标和当前阳光圆心的距离,小于半径就算收集成功。如果你要做自动收集,可以把碰撞盒从圆形改成矩形,但要注意地图坐标和鼠标屏幕坐标之间的转换,这一步漏掉的话,你会发现点哪里都收集不到阳光。

3.2 攻击判定:用“行”做简化,避免像素级碰撞

豌豆子弹是直线飞行的,它只会命中同一行里的僵尸。所以我做碰撞检测时,不会一上来就计算子弹矩形与僵尸矩形的相交,而是先判断行号是否一致,再判断x坐标是否落在僵尸的横向范围内。

for (auto it = bullets.begin(); it != bullets.end();) { it->x += it->speed * dt; bool bulletHit = false; for (auto& zombie : zombiesByRow[it->row]) { if (it->x >= zombie.x - 0.3 && it->x <= zombie.x + 0.3) { zombie.hp -= it->damage; bulletHit = true; break; } } if (bulletHit || it->x >= 9.0) { it = bullets.erase(it); } else { ++it; } }

这里最关键的参数是0.3,这是僵尸的“有效碰撞半宽”。僵尸实际图片宽度可能超过1格,但战斗判定不需要那么精确,给一个范围就够了。注意我用的是区间判断,而不是等号判断。浮点数经过几十帧累加后,坐标会变成0.30000000000000004这种诡异值,用等号判断永远都不会触发。

这套判定方式明显做了简化:它假设子弹是一个点,僵尸是一段宽度。如果你要支持多个僵尸,需要遍历整行;如果每行僵尸数量很多,可以用双端队列或者按x排序来优化。但一个原型阶段,能跑通比跑得快重要。

3.3 植物冷却:用帧计数还是时间戳?

种植冷却这个功能,新手最容易写成int counter,每帧加一,加到100就允许种。这个做法在固定时间步长下没问题,但一旦你改了主循环,或者想实现暂停、倍速,计数器就会出问题。我的做法是直接用游戏时间戳:

double lastPlantTime = 0.0; const double plantCooldown = 5.0; // 在玩家点击格子时: if (gameTime - lastPlantTime >= plantCooldown) { // 允许种植 lastPlantTime = gameTime; } else { // 提示“还在冷却中” }

这里的gameTime不是std::chrono::system_clock::now(),而是update里持续累加的全局逻辑时间。为什么不用真实时钟?因为游戏要支持暂停、加速、减速,真实时钟不会听你的。统一用逻辑时间,所有冷却和持续效果都基于它,才能保证可复现。

时间戳方案还有一个额外好处:它天生支持“剩余冷却时间”的UI展示,你只需要算一格gameTime - lastPlantTime就行。计数器方案要再乘dt才能换算成秒,多一道弯。

4. 把模型跑起来:最小可用的C++战斗模拟与控制台运行

4.1 一个能直接编译的最小战斗模拟

有了前面的对象设计,现在写一个极简但完整的战斗模拟:地图只有一行,第0行放一个豌豆射手,一个僵尸从右侧走过来。豌豆射手每隔1.5秒发射一颗子弹,子弹以每秒3格的速度向右飞。僵尸以每秒0.5格的速度向左走。谁先死谁输。

#include <iostream> #include <vector> #include <chrono> #include <thread> struct Bullet { int row; double x; double speed; int damage; }; struct Zombie { int row; double x; double hp; double speed; }; struct Plant { int row; int col; double shootTimer; double shootInterval; int damage; double bulletSpeed; }; int main() { Plant plant{0, 2, 0.0, 1.5, 20, 3.0}; Zombie zombie{0, 8.0, 100.0, 0.5}; std::vector<Bullet> bullets; double dt = 1.0 / 60.0; double totalTime = 0.0; while (totalTime < 30.0) { // 植物射击:累积时间,到间隔就发射 plant.shootTimer += dt; if (plant.shootTimer >= plant.shootInterval) { plant.shootTimer -= plant.shootInterval; bullets.push_back({plant.row, static_cast<double>(plant.col), plant.bulletSpeed, plant.damage}); } // 子弹移动与碰撞 for (auto it = bullets.begin(); it != bullets.end();) { it->x += it->speed * dt; bool hit = false; if (it->row == zombie.row && it->x >= zombie.x - 0.3 && it->x <= zombie.x + 0.3) { zombie.hp -= it->damage; hit = true; } if (hit || it->x >= 9.0) { it = bullets.erase(it); } else { ++it; } } // 僵尸移动 zombie.x -= zombie.speed * dt; // 打印每帧状态 std::cout << "t=" << totalTime << " zombie_x=" << zombie.x << " hp=" << zombie.hp << " bullets=" << bullets.size() << "\n"; if (zombie.hp <= 0) { std::cout << "Win at " << totalTime << "s\n"; break; } if (zombie.x <= 0) { std::cout << "Lose at " << totalTime << "s\n"; break; } totalTime += dt; std::this_thread::sleep_for(std::chrono::milliseconds(16)); } return 0; }

这段代码可以直接复制编译运行。你会看到僵尸的血量随着子弹命中而降低,最终输出Win。关键参数说明:

  • shootInterval = 1.5秒:本次射击间隔。1.5秒一发,初始节奏偏慢但很稳。
  • bulletSpeed = 3.0格/秒:子弹飞行速度。每帧位移 = 3 * 1/60 = 0.05格,碰撞宽度0.3格,所以不会跳过。
  • zombie.speed = 0.5格/秒:僵尸移动速度。从8格走到0格需要16秒,足够射手输出。
  • zombie.hp = 100,damage = 20:5发子弹打死僵尸。射手7.5秒内打死,僵尸16秒才到,所以必然胜利。

注意代码里用的是erase移动迭代器的方式,这在逻辑上没问题。实际项目里,如果你担心频繁erase造成内存移动,可以换成“标记待删除”再统一清理。但对于模型讲解,这种写法最直观。

4.2 从控制台到图形界面:改哪些地方

这个控制台模拟已经跑通了模型层,接下来要换成SDL或raylib时,你只需要替换main函数里的输出和sleep部分。把std::cout换成SDL_RenderDrawRect,把sleep换成SDL_Delay,update函数和碰撞检测逻辑可以完整保留。

最常见的错误是有人一上来就开SDL窗口,然后把植物和僵尸的位置全部用SDL_Rect表示,导致逻辑层和渲染层完全耦合。正确做法是:逻辑层永远用行列和浮点坐标,渲染时才根据格子宽高计算出屏幕像素坐标。

我一般会把代码拆成model.h存放Entity、Plant、Zombie、Bullet,game.cpp存放update,render.cpp存放图形绘制。后面做图形界面,只需要换render.cpp。如果你一开始就按这个结构写,后面加音效、动画都会轻松很多。

4.3 参数表:先定这几个数值再谈平衡性

在动关卡编辑器之前,先把核心数值集中定义,方便调。这里是我常用的初始值表格:

单位属性初始值备注
豌豆射手生命值100被普通僵尸啃10口
豌豆射手射击间隔1.5秒每次攻击生成一枚子弹
豌豆射手子弹伤害205发击杀普通僵尸
子弹飞行速度3格/秒到达地图边界后消失
普通僵尸生命值1003发豌豆后可被坚果辅助击杀
普通僵尸移动速度0.5格/秒16秒从右走到左
向日葵阳光间隔8秒每次生成25阳光
阳光下落速度1.2格/秒存活8秒后消失

这套参数不代表原版数值,只是我比较顺手的起点。调整时遵循一个原则:一次只改一个参数,跑一次自动战斗测试,看胜率变化,再改下一个。一次改三个数字,最后你根本不知道是谁影响了结果。

5. 避坑/常见问题:C++实现植物大战僵尸时最容易翻车的5个点

5.1 内存管理:new出来的植物和僵尸迟迟不释放

现象:游戏运行时间变长,内存占用持续上升,最后卡死。

原因:很多人习惯用new Zombie()创建僵尸,然后放在std::vector<Zombie*>里。当僵尸死亡,只调用了erase,没有delete。更危险的是erase之后,那个指针失去了索引,想delete都找不到。

解决:不要用裸指针。用std::unique_ptr或直接存对象值。第4章的例子就把子弹定义成普通结构体,放进std::vector<Bullet>,析构自动完成。植物网格里用std::unique_ptr<Entity>,即使你忘了delete,对象离开作用域也会被自动清理。血泪教训:用裸指针做游戏原型,最终都会在内存泄漏排查上耗掉大量时间。

5.2 更新与渲染耦合:帧率变高后子弹直接穿透

现象:把笔记本插上电源,帧率从60变成144,子弹反而打不中僵尸,直接穿过去。

原因:主循环用的是真实帧间隔当dt。显示器刷新率越高,单帧时间越短,但逻辑更新次数变多,子弹每次移动距离不变?不对,如果你直接拿真实间隔当dt,当帧率从60变成144,dt从1/60变成了1/144,子弹移动距离变小,按理说更容易命中。那为什么会穿透?问题反了,常见的是low帧率,比如渲染一帧要50ms,dt变成0.05,子弹移动0.15格,如果碰撞宽度0.3还勉强,但如果你把子弹速度调到30格/秒,单帧移动1.5格,子弹就跨过僵尸了。

解决:用固定时间步长累加器,dt恒定为1/60秒。这样子弹单帧位移永远是0.05格,无论屏幕多卡,逻辑都不会跳变。这个坑在第2.3节已经埋了伏笔,但很多人在做完控制台版之后,觉得固定步长啰嗦就删掉了,等到换电脑跑就被打脸。

5.3 硬编码数字:植物属性改起来像玄学

现象:你把豌豆射手伤害从20改成30,结果发现关卡变难了,僵尸走位还更飘了。

原因:代码里到处是魔法数字,比如if (zombie.hp -= 20),你忘了这个20是伤害还是阳光扣费。调参时改了一处,另一处没同步,数值之间互相矛盾。

解决:在程序开头定义常量,或者干脆用JSON配置。比如:

constexpr int PEASHOOTER_DAMAGE = 20; constexpr int PEASHOOTER_INTERVAL = 1.5; constexpr int ZOMBIE_HP = 100;

这样每个数字都有名字,调整时只要打开一个头文件或JSON文件。第6章会写怎么用JSON驱动属性,到时候连重新编译都省了。

5.4 浮点数比较:子弹到达检测在关键时刻失灵

现象:子弹明明飞到了僵尸脸上,画面都重叠了,僵尸就是不掉血。

原因:使用了if (bullet.x == zombie.x)。浮点数是二进制近似,0.1 + 0.2算出来不是0.3,而是0.30000000000000004。多个帧累加后,坐标很难刚好相等。

解决:用区间判断,比如fabs(bullet.x - zombie.x) < 0.3,或者用第4章代码里的>= zombie.x - 0.3 && <= zombie.x + 0.3。这个区间同时充当了碰撞宽度,一石二鸟。记住:游戏碰撞检测里永远不要写等号。

5.5 控制台中文乱码:植物名字输出锟斤拷

现象:代码里写了“豌豆射手”,控制台却显示“銆婄帺銆”或“锟斤拷”。

原因:Windows控制台默认代码页是GBK,而源文件用VSCode保存成了UTF-8,两者不一致。这个问题在Linux终端上不明显,在Windows下特别常见。

解决:最简单的是输出英文名,比如Peashooter和Zombie;如果想显示中文,在main开头调用system("chcp 65001"),并把源文件保存为UTF-8 with BOM;或者用宽字符wcout和L"豌豆射手"。我个人的习惯是逻辑层用英文标识,中文留给UI显示,这样最小化编码风险。

6. 进阶:用JSON配置驱动植物属性,并用自动战斗验证平衡性

6.1 用JSON配置文件替代魔法常量

当植物和僵尸种类多起来,代码里的常量会变成一团乱麻。此时把属性挪到外部文件是必然选择。我通常用nlohmann/json这个单头文件库,加载方式非常干净:

#include "json.hpp" #include <fstream> using json = nlohmann::json; json config = json::parse(std::ifstream("plants.json")); struct PlantConfig { int hp; double shootInterval; int damage; }; PlantConfig peashooter { config["peashooter"]["hp"], config["peashooter"]["shoot_interval"], config["peashooter"]["damage"] };

对应的JSON文件是这样的:

{ "peashooter": { "hp": 100, "shoot_interval": 1.5, "damage": 20 } }

这样调整属性就变成了改文档,不用重写C++逻辑。尤其是做关卡设计时,你可以快速试出“僵尸血量150”到底好不好玩,而不需要反复编译。

6.2 验证平衡性:写一个自动战斗测试

怎么判断一套参数是“数值合理”还是“数值失控”?我通常会让两个单位在无干预下模拟1000局,统计胜率。把第4章的while循环抽成一个函数:

bool runOneBattle(const PlantConfig& p, const ZombieConfig& z) { // 这里是第4章的完整战斗逻辑,返回植物是否胜利 } void validateBalance() { int win = 0; const int rounds = 1000; for (int i = 0; i < rounds; ++i) { if (runOneBattle(peashooter, normalZombie)) ++win; } double winRate = static_cast<double>(win) / rounds; std::cout << "win rate : " << winRate << "\n"; }

正常来说,一个豌豆射手对普通僵尸的胜率应该是100%,因为僵尸还没走到就死了。如果你把僵尸血量调到500,胜率会变成0%。这个测试就是你要的“后悔药”:在投入美术资源之前,先用数据决定数值大方向,而不是靠手感拍脑袋。

6.3 我的习惯:先写日志再调参数

最后分享一个血泪经验:调参前一定要打日志。之前我写植物大战僵尸模型时,没有日志,全靠猜,结果把豌豆射手射击间隔误设成0.01秒,每帧发射几十颗子弹,程序卡死还不知道原因。后来养成了习惯:每次update只打印关键事件,比如“bullet hit zombie for 20 damage”,而不是每帧打印坐标。这样跑一轮下来,你能清楚看到伤害发生在哪个时间点,也能判断是子弹太快没检测到,还是坐标错位。

我现在改任何一个属性,都会先跑一遍自动战斗测试,再看日志,最后才动数值。这套流程花不了几分钟,但能让你少熬夜。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 11:00:02

UniApp全栈直播源码:跨端架构与多端上架避坑指南

每次接到直播类项目的需求&#xff0c;我第一个反应不是看直播功能怎么做&#xff0c;而是先问一句&#xff1a;这套东西到底要覆盖几个端&#xff1f;如果是初创团队或外包交付&#xff0c;十有八九会遇到同一道坎——业务方嘴上说着“先做App”&#xff0c;心里其实藏着“微信…

作者头像 李华
网站建设 2026/9/29 10:59:47

Linux inode与硬链接/软链接本质解析

1. 为什么一个文件能有“两个名字”&#xff1f;从 inode 理解链接的本质刚接触 Linux 的人常被“软连接”和“硬链接”绕晕&#xff1a;明明是同一个文件&#xff0c;为什么有的删了原文件还能用&#xff0c;有的却直接失效&#xff1f;这背后不是玄学&#xff0c;而是 Linux …

作者头像 李华
网站建设 2026/9/29 10:59:45

智慧监管平台建设方案:从AI行为分析到联动处置的工程化指南

简介&#xff1a;这份方案以大数据云平台、人工智能与虚拟现实技术为核心&#xff0c;面向看守所管理者、政法信息化规划人员及相关集成商&#xff0c;系统梳理了从现状诊断到综合安防管理平台落地的完整建设路径&#xff0c;重点解决设备老旧、警力不足与恶性事件干预滞后等问…

作者头像 李华
网站建设 2026/9/29 10:57:33

MaskCLIP

1. 这篇论文到底想解决什么问题&#xff1f;MaskCLIP 最值得掌握的不是某个具体分割结构&#xff0c;而是它提出并验证了一个非常重要的问题&#xff1a;一个主要为 image-level recognition 训练的 VLM&#xff0c;内部到底已经包含多少 local / spatial semantics&#xff1f…

作者头像 李华
网站建设 2026/9/29 10:57:08

从“屎山”到工业级:pytest conftest.py 重构实战

1. 重构的起点&#xff1a;先看现状再动手前阵子接手了一个维护了两年的测试项目&#xff0c;第一次打开根目录的conftest.py时&#xff0c;我盯着屏幕沉默了半分钟。文件一共 1800 多行&#xff0c;里面混着 fixture、钩子函数、环境变量读取、数据库初始化、登录逻辑、数据清…

作者头像 李华
网站建设 2026/9/29 10:48:23

小鸡计数检测数据集 |小鸡计数 密集目标检测 智慧畜牧 雏鸡管理9124期

小鸡计数检测数据集 |小鸡计数 密集目标检测 智慧畜牧 雏鸡管理9124期 数据集概述 本数据集专注于养殖场景下小鸡的视觉检测与计数&#xff0c;服务于智慧畜牧、雏鸡管理及养殖密度评估。数据涵盖密集鸡群场景&#xff0c;适配小鸡自动计数、存活率统计及养殖管理决策等应用。…

作者头像 李华