简介:这份资源是Android平台3D游戏《摩天大楼》的完整源代码,面向具备Java与Android基础、希望深入理解3D游戏开发的中级开发者,可用于学习OpenGL ES渲染、游戏架构与场景管理等实战技能。压缩包共108个文件,约2.99MB,以24个java源文件与33个class字节码为核心,辅以29个png、8个jpg纹理贴图,3个mp3与2个mid音频,以及apk、dex、xml配置等运行与构建文件,结构紧凑便于逐模块研读。已有473人学习下载。源码覆盖OpenGL ES绘制建筑模型与光照、MVC式逻辑视图分离、3D对象与场景组织、碰撞检测、动画与物理模拟、触屏交互与自定义View、资源加载释放、游戏状态管理及性能优化等关键环节,读者可据此掌握Android 3D游戏从渲染到交互的完整实现思路,并借鉴实际项目中的问题解决策略。
1. 摩天大楼源码:一份能直接跑起来的建筑可视化工程
第一次拿到「摩天大楼源码」这个资源时,我以为是某个塔防小游戏,解压后才发现是一套完整的建筑可视化演示工程——用代码把一栋摩天大楼从地基到塔冠逐层“搭”出来,带楼层堆叠、立面幕墙、灯光夜景和相机漫游。它适合两类人:一类是想学三维场景搭建但不想从零写渲染循环的前端或图形方向开发者,另一类是需要一个可改参数、可换风格的建筑展示 Demo 做二次开发的人。源码结构不复杂,但把“参数化建模 + 场景组织 + 交互控制”这条链路走通了,拿来做课程作业、作品集或者内部演示都够用。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆。
2. 工程结构与技术选型:从入口文件到场景装配
2.1 目录骨架与各文件职责
这套源码的组织方式很典型:一个入口页负责挂载画布,一个主脚本负责场景初始化,若干模块分别管建筑生成、材质灯光和相机控制。解压后大致是这样一层结构(不同版本文件名可能略有出入,以实际为准):
skyscraper-src/ ├── index.html # 入口页,挂载 canvas 容器 ├── src/ │ ├── main.js # 启动入口:初始化渲染器、场景、循环 │ ├── building.js # 摩天大楼几何生成:楼层、核心筒、塔冠 │ ├── facade.js # 立面幕墙:窗格、玻璃材质、发光贴图 │ ├── environment.js # 天空、地面、环境光与雾效 │ ├── controls.js # 相机漫游与鼠标交互 │ └── config.js # 全局参数:楼层数、层高、楼宽、配色 ├── assets/ │ ├── textures/ # 玻璃、混凝土、夜景贴图 │ └── models/ # 可选的外部模型(如有) └── package.json # 依赖与启动脚本config.js是整个工程的中枢,楼层数、层高、楼体宽度、幕墙颜色都从这里读,改一个数字整栋楼就变。building.js只负责“按参数堆几何”,facade.js只负责“给几何贴皮”,职责切得比较干净,这也是它好改的原因。常见做法是把配置和生成逻辑分开,这里就是这么做的,二次开发时不用去几何代码里翻常量。
2.2 技术栈判断与依赖说明
从入口页和主脚本的写法看,这套工程走的是浏览器端三维渲染路线,核心依赖是 Three.js 这一类 WebGL 封装库,构建工具大概率是 Vite 或原生 ES Module 直接跑。判断依据有三点:入口页只有一个 canvas 容器、主脚本里是Scene / Camera / Renderer三段式初始化、几何体用BoxGeometry和InstancedMesh这类 API 拼出来。如果你本地没有node_modules,先看package.json里的dependencies和scripts,通常长这样:
{ "name": "skyscraper-demo", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "three": "^0.160.0" }, "devDependencies": { "vite": "^5.0.0" } }type: module说明源码用的是 ES Module 语法,dev脚本起本地开发服务器,build出静态产物。版本号以你手上文件为准,这里只是说明字段含义:three是渲染核心,vite只负责开发和打包,不参与运行时。如果package.json里没有这些字段,说明是更早的原生脚本版本,那就用一个静态服务器打开index.html即可,不需要构建。
2.3 启动链路:一个场景是怎么被装配出来的
主脚本的职责是把渲染器、场景、相机、灯光、建筑、控制器依次装好,再启动渲染循环。核心代码大致如下,逐段说明:
// src/main.js import * as THREE from 'three'; import { buildSkyscraper } from './building.js'; import { setupEnvironment } from './environment.js'; import { setupControls } from './controls.js'; import { CONFIG } from './config.js'; // 1. 渲染器:绑定 canvas,开启抗锯齿和阴影 const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 限制像素比,防止高分屏掉帧 renderer.shadowMap.enabled = true; document.getElementById('app').appendChild(renderer.domElement); // 2. 场景与相机 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 45, // 视场角,建筑展示常用 40~50 window.innerWidth / window.innerHeight, // 宽高比 0.1, // 近裁剪面 2000 // 远裁剪面,要能装下整栋楼 ); // 3. 环境:天空、地面、雾 setupEnvironment(scene); // 4. 建筑主体:按 CONFIG 生成楼层与幕墙 const building = buildSkyscraper(CONFIG); scene.add(building); // 5. 相机控制:轨道或漫游 const controls = setupControls(camera, renderer.domElement); camera.position.set(80, 60, 120); controls.target.set(0, CONFIG.floorHeight * CONFIG.floorCount * 0.5, 0); // 6. 渲染循环 function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); // 7. 窗口自适应 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });这段代码的关键点有三个。第一,setPixelRatio做了上限限制,这是血泪经验——不限制的话 4K 屏上像素比到 3,几何一多帧率直接腰斩。第二,相机的远裁剪面设到 2000,因为摩天大楼高度可能上百米,裁剪面太小会把塔冠切掉,画面里楼顶“消失”多半是这个参数的问题。第三,controls.target设在楼体高度的一半,保证旋转时围绕楼身而不是地面原点,观感差别很大。参数都在CONFIG里,改楼层数不用动这段。
3. 核心模块拆解:楼层生成、幕墙与相机控制
3.1 楼层堆叠:用循环和实例化把楼“长”出来
建筑生成模块是整个工程最值得看的部分。它没有用外部模型,而是用参数化方式一层层堆盒子,核心思路是“标准层重复 + 顶部收分 + 底部裙楼”。简化后的实现如下:
// src/building.js import * as THREE from 'three'; export function buildSkyscraper(config) { const group = new THREE.Group(); const { floorCount, floorHeight, width, depth } = config; // 标准层:用 InstancedMesh 批量绘制,避免几百个独立 Mesh 拖慢渲染 const floorGeo = new THREE.BoxGeometry(width, floorHeight * 0.92, depth); const floorMat = new THREE.MeshStandardMaterial({ color: 0x8899aa, roughness: 0.6 }); const floors = new THREE.InstancedMesh(floorGeo, floorMat, floorCount); const matrix = new THREE.Matrix4(); for (let i = 0; i < floorCount; i++) { // 每层之间留 8% 缝隙,形成楼层分隔线 const y = i * floorHeight + floorHeight * 0.5; matrix.makeTranslation(0, y, 0); floors.setMatrixAt(i, matrix); } floors.instanceMatrix.needsUpdate = true; group.add(floors); // 核心筒:一根贯穿全楼的细长柱体,增加结构感 const coreGeo = new THREE.BoxGeometry(width * 0.3, floorCount * floorHeight, depth * 0.3); const core = new THREE.Mesh(coreGeo, new THREE.MeshStandardMaterial({ color: 0x556677 })); core.position.y = (floorCount * floorHeight) / 2; group.add(core); // 塔冠:顶部收分,用逐层缩小的盒子模拟 const crownLayers = 5; for (let i = 0; i < crownLayers; i++) { const scale = 1 - i * 0.15; const crownGeo = new THREE.BoxGeometry(width * scale, floorHeight, depth * scale); const crown = new THREE.Mesh(crownGeo, floorMat); crown.position.y = floorCount * floorHeight + i * floorHeight + floorHeight * 0.5; group.add(crown); } return group; }逻辑说明:标准层用InstancedMesh一次提交所有楼层,这是性能关键——如果每层都new THREE.Mesh,一百层就是一百次 draw call,加上幕墙和灯光很容易卡。floorHeight * 0.92留出的缝隙就是楼层分隔线,比贴图省事。核心筒用一根柱体贯穿,视觉上让楼“有骨头”。塔冠用循环缩小盒子,scale每次减 0.15,五层收完,改crownLayers就能换塔冠高度。参数方面,floorCount控制总层数,floorHeight控制层高,width / depth控制平面尺寸,这三个在config.js里改,整栋楼的比例立刻变。
3.2 幕墙与夜景:材质和发光贴图怎么配
立面模块负责给楼体贴玻璃质感。常见做法是用MeshPhysicalMaterial开透明和反射,再叠一张窗格贴图控制哪些格子亮灯。核心片段:
// src/facade.js import * as THREE from 'three'; export function applyFacade(mesh, textureLoader, config) { const glassMap = textureLoader.load('assets/textures/glass_grid.png'); const emissiveMap = textureLoader.load('assets/textures/window_lights.png'); const material = new THREE.MeshPhysicalMaterial({ map: glassMap, // 基础窗格纹理 emissiveMap: emissiveMap, // 夜景亮灯贴图 emissive: new THREE.Color(0xffcc88), emissiveIntensity: config.nightMode ? 1.2 : 0.0, // 夜景模式才发光 metalness: 0.4, roughness: 0.15, transparent: true, opacity: 0.85, envMapIntensity: 1.0 }); mesh.material = material; return material; }emissiveMap是夜景的灵魂:贴图里亮色像素对应亮灯的窗户,暗色像素不发光,emissiveIntensity控制整体亮度。nightMode开关在config.js里,切到true整栋楼就亮起来。注意transparent: true加opacity会让渲染顺序变复杂,如果出现窗户前后穿插的“鬼影”,把depthWrite设为false或者干脆关掉透明、只靠贴图表现玻璃,这是踩过的坑。metalness和roughness决定玻璃反光强弱,白天场景调高金属度更通透,夜景则要压低粗糙度让灯光有反射。
3.3 相机漫游与交互:轨道控制和自动巡航
控制模块决定用户怎么看这栋楼。源码里通常是轨道控制加一个可选的自动巡航,核心是限制俯仰角和距离,防止相机钻到地下或飞出场景:
// src/controls.js import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; export function setupControls(camera, domElement) { const controls = new OrbitControls(camera, domElement); controls.enableDamping = true; // 惯性阻尼,手感更顺 controls.dampingFactor = 0.08; controls.minDistance = 30; // 最近距离,防止穿模进楼 controls.maxDistance = 400; // 最远距离,防止楼变成一个小点 controls.maxPolarAngle = Math.PI * 0.49; // 限制俯仰,防止相机钻到地面以下 controls.autoRotate = false; // 自动巡航开关 controls.autoRotateSpeed = 0.6; return controls; }maxPolarAngle设成略小于 90 度,相机就永远在地面以上,这个参数不设的话用户一拖就能看到楼底“穿帮”。enableDamping让松手后有缓停,观感提升明显,但要在渲染循环里每帧调controls.update(),否则阻尼不生效——这是新手最容易漏的一步。autoRotate打开就是自动巡航,适合做展示大屏。
4. 跑起来与避坑:本地启动、参数调试和常见故障
4.1 本地启动的完整步骤
拿到源码后按这个顺序走,基本不会卡住:
# 1. 进入项目目录 cd skyscraper-src # 2. 安装依赖(有 package.json 的情况) npm install # 3. 启动开发服务器 npm run dev # 终端会输出类似 http://localhost:5173 的地址,浏览器打开即可 # 4. 如果要出静态包 npm run build # 产物在 dist/ 目录,用任意静态服务器托管如果没有package.json,说明是纯静态版本,用npx serve .或任意静态服务器打开index.html就行,但注意不能直接双击文件用file://打开——ES Module 和贴图加载都会被浏览器的同源策略拦掉,控制台会报 CORS 错误。这是第一个高频翻车点。
4.2 参数调试:改哪几个数字最见效
想快速看出效果,优先动config.js里这几个值:
| 参数 | 作用 | 建议范围 | 改动效果 |
|---|---|---|---|
floorCount | 总楼层数 | 20~120 | 楼体高矮,超过 100 注意性能 |
floorHeight | 单层层高 | 3~5 | 楼层疏密,影响整体比例 |
width / depth | 平面尺寸 | 10~40 | 楼体胖瘦,太大会显得笨重 |
nightMode | 夜景开关 | true / false | 切换白天与亮灯模式 |
autoRotate | 自动巡航 | true / false | 展示大屏用 |
改完保存,开发服务器会热更新,不用重启。如果改了没反应,先确认改的是src/config.js而不是dist里的旧产物。
4.3 常见问题排查:五个真实踩坑记录
现象一:页面全黑,控制台报Failed to resolve module specifier "three"。原因是没有安装依赖或用了file://直接打开,浏览器不认识裸模块名。解决:走npm install+npm run dev,或者用 importmap 把three映射到 CDN 地址。
现象二:楼体只有一半,塔冠不见了。原因是相机远裁剪面太小,塔冠超出可视范围被裁掉。解决:把PerspectiveCamera第四个参数从默认的 1000 调到 2000 以上,或者按楼高动态计算。
现象三:帧率很低,风扇狂转。原因多半是楼层用了独立 Mesh 而不是InstancedMesh,draw call 爆炸;或者setPixelRatio没设上限。解决:确认楼层走实例化,像素比限制在 2 以内,阴影贴图分辨率别开太高。
现象四:玻璃窗户前后闪烁、穿插。原因是透明材质渲染顺序问题。解决:给幕墙材质设depthWrite: false,或者放弃透明、只用贴图表现玻璃,夜景靠emissiveMap就够。
现象五:鼠标拖动后相机钻到地面以下,看到楼底穿帮。原因是没限制俯仰角。解决:controls.maxPolarAngle = Math.PI * 0.49,同时设minDistance防止穿模进楼。
提示:改任何参数前先备份
config.js,这个文件是整栋楼的“总开关”,改乱了很难回退。
5. 二次开发:换风格、加数据、做展示页的改造思路
这套源码最大的价值不是它现在长什么样,而是它的参数化结构让你能快速换一栋“别的楼”。我一般从三个方向改。
第一个方向是换建筑风格。把building.js里的标准层从方盒子换成带切角的八边形,或者给塔冠加旋转收分,改的是几何生成那几十行,config.js加一个style字段做分支即可。幕墙那边换一张窗格贴图、调metalness和emissiveIntensity,白天玻璃幕墙和夜景灯光楼就是两套观感。验证方法很简单:改完跑npm run dev,用自动巡航转一圈看有没有穿模和闪烁。
第二个方向是接真实数据。很多展示需求是“按楼层显示不同信息”,比如每层亮灯率、入驻企业色块。做法是在config.js里加一个floorData数组,building.js生成楼层时按索引读数据,用InstancedMesh的setColorAt给每层上不同颜色:
// 按楼层数据着色,替代统一材质色 const color = new THREE.Color(); for (let i = 0; i < floorCount; i++) { const rate = config.floorData?.[i]?.occupancy ?? 0.5; color.setHSL(0.55, 0.6, 0.3 + rate * 0.4); // 入驻率越高越亮 floors.setColorAt(i, color); } floors.instanceColor.needsUpdate = true;这样一栋楼就变成了数据可视化载体,比纯展示有说服力。注意setColorAt要求材质开vertexColors或者用支持实例颜色的材质,否则颜色不生效。
第三个方向是做展示页。把autoRotate打开、隐藏控制面板、加一个标题层,就是一个能挂在大屏上循环播放的建筑展示页。如果要嵌到已有页面里,把renderer.setSize改成读容器尺寸而不是window.innerWidth,再在resize里同步更新,避免画布溢出。
改造时我有个习惯:每改一个模块就先单独跑一遍,确认几何、材质、控制三块各自正常再合起来,不然出了问题分不清是哪一层引入的。从那以后我每次动building.js的循环边界,都强制先把floorCount调到 5 跑一遍小样,确认塔冠和核心筒位置对得上再放大到一百层——这个习惯帮我省了不少排查时间。希望帮到你。
本文还有配套的精品资源,点击获取