Unity教学 项目3 3D坦克大战

视频教程:

https://www.bilibili.com/video/BV11D5QzgEpw?spm_id_from=333.788.videopod.sections&vd_source=25b783f5f945c4507229e9dec657b5bb

1. 场景搭建

  1. 创建工程文件
  2. 素材导入
  3. 将游戏场景预制体实例化
  4. 设置场景光颜色为(29, 26, 00)
  1. 设置天空颜色为(128, 110, 36)
  1. 设置 camera 位置为(-31, 25, -20)
  2. 设置 camera 旋转角度为(37, 53, 6)
  3. 设置 camera 为正交视野,Size 为 8

正交投影:常用于 2D 游戏开发、UI 设计、建筑图纸绘制等,这些场景更关注物体实际尺寸和相对位置,不需要模拟真实 3D 空间深度感。比如 2D 横版过关游戏,正交投影能保证角色和场景元素大小一致,便于玩家把握距离和位置 。

透视投影:广泛用于 3D 游戏、虚拟现实(VR)和增强现实(AR)等场景,能营造逼真空间感和深度感,让玩家有身临其境的体验。如第一人称射击游戏,通过透视投影呈现真实远近效果,增强沉浸感。

2. 移动旋转

  1. 创建坦克实例

  2. 将烟拖动到坦克上,设置位置为(0.6, 0, -0.94)和(-0.5, 0, -0.94)

  3. 坦克添加刚体组件

  4. 坦克添加碰撞盒子,设置位置为(0, 0.95, 0),大小为(1.51, 1.71, 1.62)

    1. 注意:碰撞盒子不能紧挨地面,容易检测坦克与地面发生碰撞导致坦克无法移动。
  5. 将坦克做成预制体

  6. 创建脚本文件 Tank.cs

  7. 添加脚本,实现坦克的前后移动功能

  8. 约束刚体部分轴,位置冻结 Y 轴,旋转冻结 X 和 Z 轴

  1. 添加脚本,实现坦克旋转功能

2.1. Tank.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tank : MonoBehaviour
{
    public float moveSpeed;
    public float angularSpeed;
    private Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {

    }

    private void FixedUpdate()
    {
        Move();
    }

    /// <summary>
    /// 移动
    /// </summary>
    void Move()
    {
        float v = Input.GetAxis("Vertical");
        float h = Input.GetAxis("Horizontal");
        rb.velocity = transform.forward * v * moveSpeed;
        rb.angularVelocity = transform.up * h * angularSpeed;
    } 
}

3. 两个玩家控制

进入输入设置

  1. 复制 Horizontal 轴
  2. 修改 Horizontal 控制按键
  3. 复制 Vertical 轴
  4. 修改 Vertical 控制按键
  1. 修改代码,实现通过编号区分不同的控制

3.1. Tank.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tank : MonoBehaviour
{
    public float moveSpeed;
    public float angularSpeed;
    public int playerNum;
    private Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {

    }

    private void FixedUpdate()
    {
        Move();
    }

    /// <summary>
    /// 移动
    /// </summary>
    void Move()
    {
        float v = Input.GetAxis("VerticalP" + playerNum);
        float h = Input.GetAxis("HorizontalP" + playerNum);
        rb.velocity = transform.forward * v * moveSpeed;
        rb.angularVelocity = transform.up * h * angularSpeed;
    } 
}

4. 发射炮弹

子弹、爆炸预设体,发射位置,脚本

子弹预设体

爆炸预设体

4.1. PreShell.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PreShell : MonoBehaviour
{
    public float speed = 2f;
    public GameObject preShellExplosionGo;
    private Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.velocity = transform.forward * speed;
    }

    // Update is called once per frame
    void Update()
    {

    }

    private void OnCollisionEnter(Collision collision)
    {
        GameObject shellExplosionGo = Instantiate(preShellExplosionGo, transform.position, transform.rotation);
        Destroy(gameObject);
        Destroy(shellExplosionGo.gameObject, 1.5f);
    }
}

4.2. Tank.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tank : MonoBehaviour
{
    public float moveSpeed;
    public float angularSpeed;
    public int playerNum;
    public KeyCode attackKey;
    public Transform shootPointTr;
    public GameObject preShellGo;
    private Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        attackKey = KeyCode.Space;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(attackKey))
        {
            Shoot();
        }
    }

    private void FixedUpdate()
    {
        Move();
    }

    /// <summary>
    /// 移动
    /// </summary>
    void Move()
    {
        float v = Input.GetAxis("VerticalP" + playerNum);
        float h = Input.GetAxis("HorizontalP" + playerNum);
        rb.velocity = transform.forward * v * moveSpeed;
        rb.angularVelocity = transform.up * h * angularSpeed;
    } 

    /// <summary>
    /// 攻击
    /// </summary>
    void Shoot()
    {
        GameObject shellGo = Instantiate(preShellGo, shootPointTr.position, shootPointTr.rotation);
        //shellGo.GetComponent<Rigidbody>().velocity = shellGo.transform.forward * 5;
    }
}

5. 产生伤害

5.1. PreShell.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PreShell : MonoBehaviour
{
    public float speed = 2f;
    public GameObject preShellExplosionGo;
    private Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.velocity = transform.forward * speed;
    }

    // Update is called once per frame
    void Update()
    {

    }

    private void OnCollisionEnter(Collision collision)
    {
        GameObject shellExplosionGo = Instantiate(preShellExplosionGo, transform.position, transform.rotation);
        Destroy(gameObject);
        Destroy(shellExplosionGo.gameObject, 1.5f);

        //造成伤害
        Tank tank = collision.gameObject.GetComponent<Tank>();
        if (tank == null) return;
        tank.currentBlood -= 10;
        //死亡
        if(tank.currentBlood < 0)
        {
            tank.currentBlood = 0;
            Destroy(collision.gameObject);
        }
    }
}

5.2. Tank.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tank : MonoBehaviour
{
    public float moveSpeed;
    public float angularSpeed;
    public int playerNum;
    public int currentBlood;
    public int maxBlood = 100;
    public KeyCode attackKey;
    public Transform shootPointTr;
    public GameObject preShellGo;
    private Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        attackKey = KeyCode.Space;
        currentBlood = maxBlood;
    }

    // Update is called once per frame
    void Update()
    {
        if (playerNum == 1 && Input.GetKeyDown(attackKey))
        {
            Shoot();
        }
        else if (playerNum == 2 && Input.GetKeyDown(KeyCode.Keypad9))
        {
            Shoot();
        }
    }

    private void FixedUpdate()
    {
        Move();
    }

    /// <summary>
    /// 移动
    /// </summary>
    void Move()
    {
        float v = Input.GetAxis("VerticalP" + playerNum);
        float h = Input.GetAxis("HorizontalP" + playerNum);
        rb.velocity = transform.forward * v * moveSpeed;
        rb.angularVelocity = transform.up * h * angularSpeed;
    } 

    /// <summary>
    /// 攻击
    /// </summary>
    void Shoot()
    {
        GameObject shellGo = Instantiate(preShellGo, shootPointTr.position, shootPointTr.rotation);
        //shellGo.GetComponent<Rigidbody>().velocity = shellGo.transform.forward * 5;
    }
}

6. 摄像机跟随坦克

6.1. MainCamera.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MainCamera : MonoBehaviour
{
    public Transform tank1; // 第一辆坦克的Transform
    public Transform tank2; // 第二辆坦克的Transform
    private Vector3 offset; // 摄像机相对于两辆坦克中心的偏移量
    private Camera mainCamera; // 主摄像机组件
    private float distance; // 两辆坦克之间的距离
    private float cameraSize; // 正交摄像机的尺寸

    // Start is called before the first frame update
    void Start()
    {
        // 检查是否正确赋值了两个坦克的Transform
        if (tank1 == null || tank2 == null) return;

        // 计算摄像机的初始偏移量
        Vector3 tanksCenter = (tank1.position + tank2.position) / 2;
        offset = transform.position - tanksCenter;

        // 获取主摄像机组件
        mainCamera = GetComponent<Camera>();
    }

    // Update is called once per frame
    void Update()
    {
        // 如果任意一个坦克不存在,则直接返回
        if (tank1 == null || tank2 == null) return;

        // 更新摄像机的位置
        Vector3 tanksCenter = (tank1.position + tank2.position) / 2;
        transform.position = tanksCenter + offset;

        // 计算两辆坦克之间的距离
        distance = Vector3.Distance(tank1.position, tank2.position);

        // 根据距离调整摄像机的正交尺寸
        cameraSize = distance * 0.875f;
        mainCamera.orthographicSize = cameraSize;
    }
}

7. 添加血条

7.1. 设置血条物体

新增血条物体

设置 Canvas

设置 Slider

设置 Background

设置 Fill

7.2. 编写代码

7.2.1. PreShell.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PreShell : MonoBehaviour
{
    public float speed = 5f;//速度
    public int damage = 10;//伤害值
    public GameObject preShellExplosionGo;//爆炸预设体对象
    private Rigidbody rb;//刚体

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.velocity = transform.forward * speed;
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    /// <summary>
    /// 碰撞则产生爆炸并自我销毁
    /// </summary>
    /// <param name="collision"></param>
    private void OnCollisionEnter(Collision collision)
    {
        GameObject shellExplosionGo = Instantiate(preShellExplosionGo, transform.position, transform.rotation);//生成爆炸效果
        Destroy(gameObject);//销毁炮弹
        Destroy(shellExplosionGo, 1.5f);//销毁爆炸效果

        Tank tank = collision.gameObject.GetComponent<Tank>();
        if (tank != null) {
            tank.currentBlood -= damage;//造成了伤害
            tank.hpSlider.value = (float)tank.currentBlood / tank.maxBlood;//更新血量滑动条
            if (tank.currentBlood <= 0) Destroy(collision.gameObject);//销毁坦克
        }
    }
}

7.2.2. Tank.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Tank : MonoBehaviour
{
    public float moveSpeed = 5f;//移动速度
    public float angularSpeed = 2f;//旋转角度
    public int playerNum = 1;//玩家编号
    public int currentBlood;//当前血量
    public int maxBlood = 100;//最大血量
    public GameObject preShellGo;//炮弹预设游戏对象
    public Slider hpSlider;//血量滑动条
    private Transform shootPoint;//发射位置
    private Rigidbody rb;//刚体

    // Start is called before the first frame update
    void Start()
    {
        currentBlood = maxBlood;
        rb = GetComponent<Rigidbody>();
        shootPoint = transform.Find("ShootPoint");
    }

    // Update is called once per frame
    void Update()
    {
        //如果按下空格键则发射炮弹
        if (playerNum == 1 && Input.GetKeyDown(KeyCode.Space))
        {
            Shoot();
        }
        else if (playerNum == 2 && Input.GetKeyDown(KeyCode.P))
        {
            Shoot();
        }
    }

    private void FixedUpdate()
    {
        Move();
    }

    /// <summary>
    /// 移动
    /// </summary>
    private void Move()
    {
        float v = Input.GetAxis("VerticalP" + playerNum);
        float h = Input.GetAxis("HorizontalP" + playerNum);
        rb.velocity = transform.forward * v * moveSpeed;//前后移动
        rb.angularVelocity = transform.up * h * angularSpeed;//左右旋转
    }

    /// <summary>
    /// 发射炮弹
    /// </summary>
    private void Shoot()
    {
        Instantiate(preShellGo, shootPoint.position, shootPoint.rotation);
    }
}

7.2.3. MainCamera.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MainCamera : MonoBehaviour
{
    public Transform tank1; // 第一辆坦克的Transform
    public Transform tank2; // 第二辆坦克的Transform
    private Vector3 offset; // 摄像机相对于两辆坦克中心的偏移量
    private Camera mainCamera; // 主摄像机组件
    private float distance; // 两辆坦克之间的距离
    private float cameraSize; // 正交摄像机的尺寸

    // Start is called before the first frame update
    void Start()
    {
        // 检查是否正确赋值了两个坦克的Transform
        if (tank1 == null || tank2 == null) return;

        // 计算摄像机的初始偏移量
        Vector3 tanksCenter = (tank1.position + tank2.position) / 2;
        offset = transform.position - tanksCenter;

        // 获取主摄像机组件
        mainCamera = GetComponent<Camera>();
    }

    // Update is called once per frame
    void Update()
    {
        // 如果任意一个坦克不存在,则直接返回
        if (tank1 == null || tank2 == null) return;

        // 更新摄像机的位置
        Vector3 tanksCenter = (tank1.position + tank2.position) / 2;
        transform.position = tanksCenter + offset;

        // 计算两辆坦克之间的距离
        distance = Vector3.Distance(tank1.position, tank2.position);

        // 根据距离调整摄像机的正交尺寸
        cameraSize = distance * 0.875f;
        mainCamera.orthographicSize = cameraSize;
    }
}

7.3. 少量细节

8. 音效

8.1. 坦克静止、行驶音效

调正摄像机位置靠近坦克,使摄像机录制到坦克音效

8.2. 坦克销毁音效

8.3. 炮弹发射音效

8.4. 炮弹爆炸音效

8.5. 背景音乐

相关推荐
寂寞的船1 天前
【Unity知识速递】第8期 IL2CPP 托管堆内存碎片与原生堆交互机制深度解析
unity·游戏引擎·交互
时间的情敌1 天前
Cesium 加载 3D Tiles 倾斜摄影、压平、样式设置(Vue3 实战)
vue.js·3d·cesium
寂寞的船2 天前
【Unity知识速递】第5期 Unity 6.2 深度解析:AI Assistant 登场,World Space UI 与 Sentis 全面升级
人工智能·ui·unity
寂寞的船2 天前
【Unity知识速递】第4期 MCP for Unity:用 AI 自然语言控制编辑器
人工智能·unity·编辑器
淡海水2 天前
09-04-内存-Unity原生对象生命周期
unity·c#·游戏引擎·gc
寂寞的船2 天前
【Unity知识速递】第6期 URP性能优化完全指南:从内存到GPU的三维调优
unity·性能优化·编辑器
寂寞的船2 天前
【Unity Bug挑战】第10期 SerializeReference 内存泄漏之谜
unity·c#·游戏引擎·bug
AGI_Eval3 天前
AI 3D生成技术:从“可用”走向“可靠”还有多远?
人工智能·3d
翌日阳4 天前
二十分之一的压缩比:Spatial Vivid 1.0 把 3DGS 全链路参考实现按 MIT 开源了
3d·开源
tealcwu4 天前
【Unity基础】为什么游戏需要物理按键映射?从 Unity Input Manager 到跨引擎输入设计
java·游戏·unity