浏览器里的3D世界:用HTML5+Three.js打造你的第一款小游戏

文章目录

浏览器里的3D世界:用HTML5+Three.js打造你的第一款小游戏

一、阶段1:零基础入门(环境搭建与核心概念)

目标:搭建开发环境,理解Three.js基本原理,创建第一个3D场景。

核心知识点

  1. 开发环境准备

    • HTML5基础:canvas元素(3D渲染容器)、基本DOM操作
    • Three.js引入:通过CDN(<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>)或npm安装
    • 开发工具:VS Code(推荐插件:Live Server实时预览)
  2. Three.js核心组件

    • 场景(Scene):3D世界的容器,所有物体都需要添加到场景中
    • 相机(Camera):模拟人眼视角,常用PerspectiveCamera(透视相机,有近大远小效果)
    • 渲染器(Renderer):将3D场景渲染到canvas
    • 物体(Mesh):由几何体(Geometry)和材质(Material)组成

实战代码:第一个3D场景(旋转立方体)

<!DOCTYPE html>
<html>
<head>
    <title>我的第一个Three.js游戏</title>
    <!-- 引入Three.js -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
    <style>
        body { margin: 0; } /* 移除默认边距 */
        canvas { display: block; } /* 全屏显示canvas */
    </style>
</head>
<body>
    <script>
        // 1. 创建场景
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0xeeeeee); // 浅灰色背景

        // 2. 创建相机(视野角度75°,宽高比=窗口宽高,近平面0.1,远平面1000)
        const camera = new THREE.PerspectiveCamera(
            75,
            window.innerWidth / window.innerHeight,
            0.1,
            1000
        );
        camera.position.z = 5; // 相机位置(沿z轴远离原点)

        // 3. 创建渲染器并添加到页面
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染尺寸
        document.body.appendChild(renderer.domElement); // 将canvas添加到页面

        // 4. 创建物体:立方体(几何体+材质)
        const geometry = new THREE.BoxGeometry(1, 1, 1); // 1x1x1的立方体
        const material = new THREE.MeshBasicMaterial({ 
            color: 0x00ff00, // 绿色
            wireframe: true // 线框模式(便于观察3D结构)
        });
        const cube = new THREE.Mesh(geometry, material); // 组合几何体和材质
        scene.add(cube); // 将立方体添加到场景

        // 5. 窗口大小变化时调整渲染器和相机
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix(); // 更新相机投影矩阵
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        // 6. 动画循环(每帧执行)
        function animate() {
            requestAnimationFrame(animate); // 浏览器原生动画API,确保流畅渲染
            
            // 让立方体旋转
            cube.rotation.x += 0.01;
            cube.rotation.y += 0.01;
            
            renderer.render(scene, camera); // 渲染场景
        }
        animate(); // 启动动画
    </script>
</body>
</html>

最佳实践与注意事项

  • 相机位置:初始位置需确保物体在相机视野内(如z=5可看到原点附近的物体)
  • 渲染循环:必须使用requestAnimationFrame而非setInterval,前者会根据浏览器刷新率自动调整(通常60帧/秒)
  • 窗口适配:监听resize事件并更新相机和渲染器,避免窗口缩放后画面变形

二、阶段2:3D物体与场景构建(游戏世界基础)

目标:掌握3D物体创建、材质纹理应用、灯光与阴影,构建游戏场景。

核心知识点

  1. 几何体与物体

    • 内置几何体:BoxGeometry(立方体)、SphereGeometry(球体)、CylinderGeometry(圆柱体)等
    • 自定义几何体:通过顶点坐标创建复杂形状(进阶)
    • 物体操作:位置(position)、旋转(rotation)、缩放(scale
  2. 材质与纹理

    • 基础材质:MeshBasicMaterial(不受灯光影响)、MeshLambertMaterial(漫反射,受灯光影响)
    • 纹理映射:加载图片作为材质(TextureLoader),实现物体表面细节
    • 透明与混合:transparent: true + opacity控制透明度
  3. 灯光与阴影

    • 常用灯光:AmbientLight(环境光,无方向)、DirectionalLight(平行光,如太阳光)
    • 阴影开启:需同时设置渲染器、灯光、物体的阴影属性

实战代码:带纹理和阴影的3D场景

<!DOCTYPE html>
<html>
<head>
    <title>3D场景与纹理</title>
    <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
    <style> body { margin: 0; } </style>
</head>
<body>
    <script>
        // 1. 场景、相机、渲染器
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 10;

        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.shadowMap.enabled = true; // 启用阴影渲染
        document.body.appendChild(renderer.domElement);

        // 2. 灯光
        // 环境光(基础照明,无阴影)
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
        scene.add(ambientLight);

        // 平行光(产生阴影)
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
        directionalLight.position.set(5, 5, 5); // 灯光位置(斜上方)
        directionalLight.castShadow = true; // 灯光产生阴影
        scene.add(directionalLight);

        // 3. 地面(接收阴影)
        const groundGeometry = new THREE.PlaneGeometry(20, 20);
        const groundMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc });
        const ground = new THREE.Mesh(groundGeometry, groundMaterial);
        ground.rotation.x = -Math.PI / 2; // 旋转90度使其水平
        ground.position.y = -3;
        ground.receiveShadow = true; // 地面接收阴影
        scene.add(ground);

        // 4. 带纹理的球体
        const textureLoader = new THREE.TextureLoader();
        // 加载地球纹理(使用picsum的图片模拟)
        textureLoader.load('https://picsum.photos/id/237/512/512', (texture) => {
            const sphereGeometry = new THREE.SphereGeometry(2, 32, 32); // 球体,32段细分
            const sphereMaterial = new THREE.MeshLambertMaterial({ map: texture });
            const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
            sphere.castShadow = true; // 球体产生阴影
            scene.add(sphere);

            // 动画:球体旋转
            function animate() {
                requestAnimationFrame(animate);
                sphere.rotation.y += 0.005;
                renderer.render(scene, camera);
            }
            animate();
        });

        // 窗口适配
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    </script>
</body>
</html>

最佳实践与注意事项

  • 纹理优化:纹理图片尺寸建议为2的幂次方(如256x256、512x512),加载更快且渲染更稳定
  • 阴影性能:阴影计算消耗性能,复杂场景可降低阴影分辨率(directionalLight.shadow.mapSize.set(1024, 1024)
  • 几何体细分:细分段数(如球体的32,32)越高,物体越光滑,但性能消耗越大,需平衡画质与性能

三、阶段3:交互与物理(游戏核心机制)

目标:实现玩家交互(鼠标/键盘控制)、碰撞检测、物理效果,让游戏“可玩”。

核心知识点

  1. 用户交互

    • 射线检测(Raycaster):检测鼠标点击的3D物体(如拾取物品、点击按钮)
    • 键盘控制:监听keydown/keyup事件,控制角色移动
    • 触摸适配:移动端触摸事件(touchstart/touchmove)处理
  2. 物理引擎集成

    • 常用引擎:Cannon.js(轻量)、Ammo.js(基于Bullet,功能强)
    • 核心概念:刚体(RigidBody)、碰撞体(Shape)、世界(World)、重力(Gravity)

实战代码:鼠标交互与物理碰撞

<!DOCTYPE html>
<html>
<head>
    <title>交互与物理效果</title>
    <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
    <!-- 引入Cannon.js物理引擎 -->
    <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
    <style> body { margin: 0; } </style>
</head>
<body>
    <script>
        // 1. 基础组件
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0xf0f0f0);
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 5, 15);
        camera.lookAt(0, 0, 0);

        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // 2. 灯光
        scene.add(new THREE.AmbientLight(0xffffff, 0.5));
        const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
        dirLight.position.set(5, 10, 7);
        scene.add(dirLight);

        // 3. 物理世界(Cannon.js)
        const world = new CANNON.World();
        world.gravity.set(0, -9.82, 0); // 重力(y轴向下)
        world.broadphase = new CANNON.NaiveBroadphase(); // 碰撞检测算法

        // 4. 地面(物理+渲染)
        // 渲染用地面
        const groundGeo = new THREE.PlaneGeometry(20, 20);
        const groundMat = new THREE.MeshLambertMaterial({ color: 0x999999 });
        const groundMesh = new THREE.Mesh(groundGeo, groundMat);
        groundMesh.rotation.x = -Math.PI / 2;
        scene.add(groundMesh);
        // 物理用地面
        const groundShape = new CANNON.Plane();
        const groundBody = new CANNON.Body({ mass: 0 }); // 质量0=静止物体
        groundBody.addShape(groundShape);
        groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
        world.addBody(groundBody);

        // 5. 可点击的立方体(物理+渲染)
        const boxes = []; // 存储所有立方体(渲染+物理)
        function createBox(x, y, z) {
            // 渲染用立方体
            const geo = new THREE.BoxGeometry(1, 1, 1);
            const mat = new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff });
            const mesh = new THREE.Mesh(geo, mat);
            mesh.position.set(x, y, z);
            mesh.castShadow = true;
            scene.add(mesh);

            // 物理用立方体
            const shape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)); // 半边长
            const body = new CANNON.Body({ mass: 1 }); // 质量1=动态物体
            body.addShape(shape);
            body.position.set(x, y, z);
            world.addBody(body);

            boxes.push({ mesh, body });
        }

        // 初始创建3个立方体
        createBox(-3, 5, 0);
        createBox(0, 5, 0);
        createBox(3, 5, 0);

        // 6. 鼠标点击交互(射线检测)
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();

        window.addEventListener('click', (event) => {
            // 将鼠标坐标转换为Three.js标准坐标(-1到1)
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

            // 更新射线(从相机到鼠标点)
            raycaster.setFromCamera(mouse, camera);

            // 检测与立方体的碰撞
            const intersects = raycaster.intersectObjects(boxes.map(b => b.mesh));
            if (intersects.length > 0) {
                // 点击到立方体:施加向上的力
                const clickedBox = boxes.find(b => b.mesh === intersects[0].object);
                clickedBox.body.applyImpulse(
                    new CANNON.Vec3(0, 5, 0), // 力的方向和大小
                    clickedBox.body.position // 力的作用点
                );
            }
        });

        // 7. 动画循环(同步物理与渲染)
        const timeStep = 1 / 60; // 物理模拟帧率
        function animate() {
            requestAnimationFrame(animate);
            
            // 更新物理世界
            world.step(timeStep);
            
            // 同步物理位置到渲染物体
            boxes.forEach(box => {
                box.mesh.position.copy(box.body.position);
                box.mesh.quaternion.copy(box.body.quaternion);
            });
            
            renderer.render(scene, camera);
        }
        animate();

        // 窗口适配
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    </script>
</body>
</html>

最佳实践与注意事项

  • 射线检测性能intersectObjects方法传入需要检测的物体列表(而非整个场景),减少计算量
  • 物理引擎优化
    • 质量为0的物体(如地面)设为静态,减少计算
    • 复杂场景使用Cannon.BVHBroadphase替代NaiveBroadphase,碰撞检测更快
  • 交互反馈:点击物体时添加视觉反馈(如颜色变化、粒子效果),提升用户体验

四、阶段4:游戏功能开发(核心玩法与逻辑)

目标:整合前面知识,实现游戏核心玩法(如收集、闯关、计分)、UI界面、状态管理。

核心知识点

  1. 游戏场景管理

    • 多场景切换:菜单场景、游戏场景、结束场景
    • 资源预加载:使用LoadingManager加载纹理、模型等资源,显示加载进度
  2. 核心玩法实现

    • 碰撞事件:检测玩家与目标/障碍物的碰撞(如收集物品加分)
    • 角色控制:第一/第三人称视角移动(结合键盘WASD或方向键)
    • 计分与状态:使用变量记录分数、生命值,实现游戏胜利/失败逻辑
  3. 2D UI集成

    • HTML叠加层:用普通HTML/CSS创建计分板、按钮(简单高效)
    • 纹理UI:用Three.js的CanvasTexture创建3D空间中的UI元素

实战代码:简易收集类游戏(核心玩法)

<!DOCTYPE html>
<html>
<head>
    <title>收集星星游戏</title>
    <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
    <style>
        body { margin: 0; }
        #scoreboard { 
            position: fixed; 
            top: 20px; 
            left: 20px; 
            font-family: Arial; 
            font-size: 24px; 
            color: white; 
            background: rgba(0,0,0,0.5); 
            padding: 10px 20px; 
            border-radius: 5px;
        }
        #startScreen {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.8);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: white;
            font-family: Arial;
        }
        #startButton {
            padding: 15px 30px;
            font-size: 20px;
            margin-top: 20px;
            cursor: pointer;
            background: #4CAF50;
            border: none;
            color: white;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <!-- 游戏UI -->
    <div id="scoreboard">分数: 0</div>
    <div id="startScreen">
        <h1>收集星星</h1>
        <p>使用方向键移动,收集黄色星星得分!</p>
        <button id="startButton">开始游戏</button>
    </div>

    <script>
        // 游戏状态
        let score = 0;
        let isPlaying = false;

        // 1. 基础组件
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x111133); // 深色背景
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // 2. 灯光
        scene.add(new THREE.AmbientLight(0xffffff, 0.3));
        const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
        dirLight.position.set(10, 10, 5);
        scene.add(dirLight);

        // 3. 物理世界
        const world = new CANNON.World();
        world.gravity.set(0, -20, 0);
        world.broadphase = new CANNON.SAPBroadphase(world);

        // 4. 地面
        const ground = new THREE.Mesh(
            new THREE.PlaneGeometry(50, 50),
            new THREE.MeshLambertMaterial({ color: 0x333333 })
        );
        ground.rotation.x = -Math.PI / 2;
        scene.add(ground);
        const groundBody = new CANNON.Body({ mass: 0 });
        groundBody.addShape(new CANNON.Plane());
        groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1,0,0), -Math.PI/2);
        world.addBody(groundBody);

        // 5. 玩家(立方体)
        const playerGeo = new THREE.BoxGeometry(1, 2, 1);
        const playerMat = new THREE.MeshLambertMaterial({ color: 0x4287f5 });
        const playerMesh = new THREE.Mesh(playerGeo, playerMat);
        playerMesh.position.y = 1; // 底部与地面对齐
        scene.add(playerMesh);
        // 物理玩家
        const playerShape = new CANNON.Box(new CANNON.Vec3(0.5, 1, 0.5));
        const playerBody = new CANNON.Body({ mass: 5 });
        playerBody.addShape(playerShape);
        playerBody.position.set(0, 1, 0);
        playerBody.fixedRotation = true; // 防止旋转
        world.addBody(playerBody);

        // 6. 星星(收集目标)
        const stars = [];
        function createStar() {
            // 随机位置(x: -20~20, z: -20~20)
            const x = (Math.random() - 0.5) * 40;
            const z = (Math.random() - 0.5) * 40;
            
            // 星星几何体(用八面体模拟)
            const starGeo = new THREE.OctahedronGeometry(0.5);
            const starMat = new THREE.MeshLambertMaterial({ color: 0xffff00 });
            const starMesh = new THREE.Mesh(starGeo, starMat);
            starMesh.position.set(x, 0.5, z);
            scene.add(starMesh);
            
            // 物理星星(传感器,不产生碰撞力)
            const starShape = new CANNON.Sphere(0.5);
            const starBody = new CANNON.Body({ mass: 0, type: CANNON.Body.KINEMATIC });
            starBody.addShape(starShape);
            starBody.position.set(x, 0.5, z);
            world.addBody(starBody);
            
            stars.push({ mesh: starMesh, body: starBody });
        }

        // 初始创建10个星星
        for (let i = 0; i < 10; i++) createStar();

        // 7. 碰撞检测(玩家与星星)
        world.addEventListener('beginContact', (event) => {
            if (!isPlaying) return;
            
            const bodyA = event.bodyA;
            const bodyB = event.bodyB;
            
            // 判断是否是玩家与星星碰撞
            const isPlayerStar = (bodyA === playerBody && stars.some(s => s.body === bodyB)) ||
                                (bodyB === playerBody && stars.some(s => s.body === bodyA));
            
            if (isPlayerStar) {
                // 找到被碰撞的星星
                const star = stars.find(s => s.body === bodyA || s.body === bodyB);
                if (star) {
                    // 移除星星
                    scene.remove(star.mesh);
                    world.removeBody(star.body);
                    stars.splice(stars.indexOf(star), 1);
                    
                    // 加分
                    score += 10;
                    document.getElementById('scoreboard').textContent = `分数: ${score}`;
                    
                    // 每收集3个星星新增1个
                    if (score % 30 === 0) createStar();
                }
            }
        });

        // 8. 玩家控制(键盘)
        const keys = { left: false, right: false, forward: false, backward: false };
        window.addEventListener('keydown', (e) => {
            if (!isPlaying) return;
            switch(e.key) {
                case 'ArrowLeft': keys.left = true; break;
                case 'ArrowRight': keys.right = true; break;
                case 'ArrowUp': keys.forward = true; break;
                case 'ArrowDown': keys.backward = true; break;
            }
        });
        window.addEventListener('keyup', (e) => {
            switch(e.key) {
                case 'ArrowLeft': keys.left = false; break;
                case 'ArrowRight': keys.right = false; break;
                case 'ArrowUp': keys.forward = false; break;
                case 'ArrowDown': keys.backward = false; break;
            }
        });

        // 9. 游戏开始
        document.getElementById('startButton').addEventListener('click', () => {
            document.getElementById('startScreen').style.display = 'none';
            isPlaying = true;
        });

        // 10. 动画循环
        const timeStep = 1 / 60;
        function animate() {
            requestAnimationFrame(animate);
            
            if (isPlaying) {
                // 玩家移动
                const speed = 10;
                const direction = new CANNON.Vec3(0, 0, 0);
                if (keys.forward) direction.z -= speed;
                if (keys.backward) direction.z += speed;
                if (keys.left) direction.x -= speed;
                if (keys.right) direction.x += speed;
                playerBody.velocity.x = direction.x;
                playerBody.velocity.z = direction.z;
                
                // 相机跟随玩家
                camera.position.x = playerBody.position.x;
                camera.position.z = playerBody.position.z + 10;
                camera.position.y = playerBody.position.y + 5;
                camera.lookAt(playerBody.position.x, playerBody.position.y + 1, playerBody.position.z);
            }
            
            // 更新物理和渲染
            world.step(timeStep);
            playerMesh.position.copy(playerBody.position);
            renderer.render(scene, camera);
        }
        animate();

        // 窗口适配
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    </script>
</body>
</html>

最佳实践与注意事项

  • 游戏状态管理:用变量(如isPlaying)清晰区分游戏状态(未开始/进行中/结束),避免逻辑混乱
  • 性能控制:限制同时存在的物体数量(如星星数量),复杂场景使用对象池(Object Pool)复用物体
  • 移动适配:为移动端添加虚拟摇杆(用HTML/CSS实现),映射到方向键控制逻辑

五、阶段5:优化、测试与发布(上线准备)

目标:优化游戏性能,适配多设备,最终打包发布。

核心知识点

  1. 性能优化

    • 几何体合并:BufferGeometryUtils.mergeBufferGeometries合并静态物体,减少绘制调用
    • 层级细节(LOD):远处物体使用低多边形模型
    • 纹理压缩:使用basisu等工具压缩纹理,减少内存占用
  2. 兼容性与适配

    • 浏览器兼容:检测WebGL支持(renderer.capabilities.isWebGL2),提供降级提示
    • 响应式设计:根据设备性能自动调整画质(如移动端降低阴影质量)
  3. 打包与发布

    • 资源压缩:用terser压缩JS,gulp/webpack打包项目
    • 发布平台:部署到静态服务器(如Netlify、Vercel)或游戏平台(如itch.io)

最佳实践与注意事项

  • 性能测试:用Chrome DevTools的Performance面板分析帧率,找出瓶颈(如频繁GC、复杂碰撞)
  • 错误监控:添加全局错误捕获(window.addEventListener('error', ...)),记录玩家遇到的问题
  • 加载策略:大资源(如3D模型)分阶段加载,优先加载核心游戏资源,提升首屏加载速度

总结:从零基础到发布的全路径

  1. 基础入门:环境搭建→Three.js核心组件→第一个3D场景
  2. 场景构建:几何体与材质→纹理与灯光→3D世界搭建
  3. 交互与物理:鼠标/键盘控制→射线检测→物理引擎集成
  4. 游戏开发:核心玩法实现→UI界面→状态管理与逻辑
  5. 优化发布:性能优化→多设备适配→打包上线

Three.js降低了3D开发的门槛,即使零基础也能通过循序渐进的学习,从创建简单立方体到开发完整小游戏。关键是多实践,理解“场景-相机-渲染器”的核心逻辑,再逐步叠加交互和物理效果,最终实现自己的3D游戏创意。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值