Unity3D实现坦克大战

一、效果图演示

二、逻辑剖析

从界面上:

  • 需要一个Canvas满屏对着用户,该Canvas上展示用户的游戏数据,比如血条。
  • 需要一个Canvas放在蓝色坦克上方,也需要实时对着用户,显示敌人的血条信息
  • 两个坦克
  • 一个平面Plane放草地的纹理

从逻辑上:

  • 前后箭头键控制玩家前进或后退
  • 左右箭头键控制玩家左右转向
  • 鼠标左键或空格键控制玩家发射炮弹
  • 玩家血条希纳是在屏幕左上角
  • 相机在玩家后上方的位置,始终跟随玩家,朝玩家正前方看
  • 玩家移动时,敌人转向玩家,当偏离玩家的角度小于5度时,发射炮弹
  • 敌人血条显示在其上方,并且始终看向相机

三、界面组件信息

(1)游戏对象层级结构

(2)组件参数信息

1.玩家Player组件参数

NameTypePositionRotationScaleColor
PlayerEmpty(0, 0.25, -5)(0, 0, 0)(1, 1, 1)#228439
ButtonCube(0, 0, 0)(0, 0, 0)(2, 0.5, 2)#228439
TopCube(0, 0.5, 0)(0, 0, 0)(1, 0.5, 1)#228439
GunCylinder(0, 0, 1.5)(90, 0, 0)(0.2, 1, 0.4)#228439
FirePointEmpty(0, 1.15, 0)(0, 0, 0)(1, 1, 1)--

Player 游戏对象添加了刚体组件,并修改 Mass = 100,Drag = 1,AngularDrag = 0.1,Freeze Rotation 中勾选 X 和 Z。 

2.玩家HP组件参数

NameTypePositionWidth/HeightColor
PlayerHPCanvas(960, 540, 0)1920/1080--
Panel     Panel位置信息全是0#FFFFFF
HealthBGImage(-809,464,0)200/20#FFFFFF
HealthImage(-809,464,0)200/20#FF2230

玩家 PlayerHP 的 Canvas 渲染模式是 Screen Space - Overlay。

制作一个红色的图片放入Health的Source Image中,Health 的 ImageType 设置为 Filled,Fill Method 设置为 Horizontal。

3.敌人组件参数

NameTypePositionRotationScaleColor
EnemyEmpty(0, 0.25, 5)(0, 180, 0)(1, 1, 1)#15D3F9
ButtonCube(0, 0, 0)(0, 0, 0)(2, 0.5, 2)#15D3F9
TopCube(0, 0.5, 0)(0, 0, 0)(1, 0.5, 1)#15D3F9
GunCylinder(0, 0, 1.5)(90, 0, 0)(0.2, 1, 0.4)#15D3F9
FirePointEmpty(0, 1.15, 0)(0, 0, 0)(1, 1, 1)--

Enemy 游戏对象添加了刚体组件,并修改 Mass = 100,Drag = 0.5,AngularDrag = 0.1,Freeze Rotation 中勾选 X 和 Z。  

4.敌人HP组件参数

NameTypePositionWidth/HeightColor
HPCanvas(0, 0.85, 0)2/0.2--
HealthBGImage(0,0,0)2/0.2#FFFFFF
HealthImage(0,0,0)2/0.2#FF2230

敌人 HP 的 Canvas 渲染模式是 World Space,将刚才的红色底图也放入Health的Source Image中,Health 的 ImageType 设置为 Filled,Fill Method 设置为 Horizontal。

5.地面和炮弹的组件参数

NameTypePositionRotationScaleColor
PlanePlane(0, 0, 0)(0, 0, 0)(10, 10, 10)GrassRockyAlbedo
BulletSphere(0, 0.5, -5)(0, 0, 0)(0.3, 0.3, 0.3)#228439

炮弹作为预设体拖拽到 Assets/Resources/Prefabs 目录下,并且添加了刚体组件。

四、脚本代码:

1.CameraController

 CameraController 脚本组件挂在 MainCamera 游戏对象上。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    // Start is called before the first frame update
    private Transform player; // 玩家
    private Vector3 relaPlayerPos; // 相机在玩家坐标系中的位置
    private float targetDistance = 15f; // 相机看向玩家前方的位置

    void Start()
    {
        relaPlayerPos = new Vector3(0, 4, -8);
        player = GameObject.Find("Player/Top").transform; // 世界坐标系位置
    }

    private void LateUpdate()
    {
        ComCameraPos();
    }

    // 计算相机坐标
    private void ComCameraPos()
    {
        Vector3 target = player.position + player.forward * targetDistance;
        transform.position = transformVecter(relaPlayerPos, player.position, player.right, player.up, player.forward);
        transform.rotation = Quaternion.LookRotation(target - transform.position);
    }

    // 以origin为原点,已知vec在坐标轴locX/locY/locZ中的位置,将vec转为世界坐标系的位置
    private Vector3 transformVecter(Vector3 vec, Vector3 origin, Vector3 locX, Vector3 locY, Vector3 locZ) 
    { 
        return vec.x * locX + vec.y * locY + vec.z * locZ + origin;
        
    }
}

2.BulletInfo

using UnityEngine;

public class BulletInfo
{
    public string name; // 炮弹名
    public Color color; // 炮弹颜色
    public Vector3 flyDir; // 炮弹飞出方向
    public float speed; // 炮弹飞行速度
    public float fireRange; // 炮弹射程

    public BulletInfo(string name, Color color, Vector3 flyDir, float speed, float fireRange)
    {
        this.name = name;
        this.color = color;
        this.flyDir = flyDir;
        this.speed = speed;
        this.fireRange = fireRange;
    }
}

3.BulletController

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class BulletController : MonoBehaviour
{
    private BulletInfo bulletInfo; // 炮弹信息
    private volatile bool isDying = false;

    // Start is called before the first frame update
    void Start()
    {
        gameObject.name = bulletInfo.name;
        GetComponent<MeshRenderer>().material.color = bulletInfo.color;
        float lifeTime = bulletInfo.fireRange / bulletInfo.speed; // 存活时间
        Destroy(gameObject, lifeTime);
    }

    // Update is called once per frame
    void Update()
    {
        transform.GetComponent<Rigidbody>().velocity = bulletInfo.flyDir * bulletInfo.speed;
    }

    public void SetBulletInfo(BulletInfo bulletInfo)
    {
        this.bulletInfo = bulletInfo;
    }

    private void OnCollisionEnter(Collision other)
    {
        if (isDying)
        {
            return;
        }

        if(IsHitEnemy(gameObject.name, other.gameObject.name))
        {
            other.transform.Find("HP/Health").GetComponent<Image>().fillAmount -= 0.1f;
            isDying = true;
            Destroy(gameObject, 0.1f);
        }else if(IsHitPlayer(gameObject.name, other.gameObject.name))
        {
            GameObject.Find("PlayerHP/Panel/Health").GetComponent<Image>().fillAmount -= 0.1f;
            isDying = true;
            Destroy(gameObject, 0.1f);
        }
    }

    private bool IsHitEnemy(string name, string otherName)
    {
        return name.Equals("PlayerBullet") && otherName.Equals("Enemy");
    }

    private bool IsHitPlayer(string name, string otherName)
    {
        return name.Equals("EnemyBullet") && otherName.Equals("Player");
    }
}

4.PlayerController

PlayerController 脚本组件挂在 Player 游戏对象上。 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    private Transform firePoint; // 开火点
    private GameObject bulletPrefab; // 炮弹预设体
    private float tankMoveSpeed = 4f; // 坦克移动速度
    private float tankRotateSpeed = 2f; // 坦克转向速度
    private float fireWaitTime = float.MaxValue; // 距离上次开火已等待的时间
    private float bulletCoolTime = 0.15f; // 炮弹冷却时间

    void Start()
    {
        firePoint = transform.Find("Top/Gun/FirePoint");
        bulletPrefab = (GameObject)Resources.Load("Prefabs/Bullet");
    }

    // Update is called once per frame
    void Update()
    {
        fireWaitTime += Time.deltaTime;
        float hor = Input.GetAxis("Horizontal");
        float ver = Input.GetAxis("Vertical");
        Move(hor, ver);
        if(Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space))
        {
            Fire();
        }

    }

    // 坦克移动
    private void Move(float hor, float ver) 
    {
        if (Mathf.Abs(hor) > 0.1f || Mathf.Abs(ver) > 0.1f) {
            GetComponent<Rigidbody>().velocity = transform.forward * tankMoveSpeed * ver;
            GetComponent<Rigidbody>().angularVelocity = Vector3.up * tankRotateSpeed * hor;
        }
    }

    // 开炮
    private void Fire()
    {
        if (fireWaitTime > bulletCoolTime) { 
            BulletInfo bulletInfo = new BulletInfo("PlayerBullet", Color.red, transform.forward, 10f, 15f);
            GameObject bullet = Instantiate(bulletPrefab, firePoint.position, Quaternion.identity);
            bullet.AddComponent<BulletController>().SetBulletInfo(bulletInfo);
            fireWaitTime = 0f;
        }
    }
}

5.EnemyController

EnemyController 脚本组件挂在 Enemy 游戏对象上。  

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    private Transform target; // 目标
    private Transform top; // 炮头
    private Transform firePoint; // 开火点
    private Transform hp; // 血条
    private GameObject bulletPrefab; // 炮弹预设体
    private float rotateSpeed = 0.4f; // 坦克转向速度
    private float fireWaitTime = float.MaxValue; // 距离上次开火已等待时间
    private float bulletCoolTime = 1f; // 炮弹冷却时间

    // Start is called before the first frame update
    void Start()
    {
        target = GameObject.Find("Player/Top").transform;
        top = transform.Find("Top");
        firePoint = transform.Find("Top/Gun/FirePoint");
        hp = transform.Find("HP");
        bulletPrefab = (GameObject)Resources.Load("Prefabs/Bullet");
    }

    // Update is called once per frame
    void Update()
    {
        fireWaitTime += Time.deltaTime;
        if (LookAtTarget()) {
            Fire();
        }
        HPLookAtCamera();
    }

    private bool LookAtTarget()
    {
        Vector3 dir = target.position - top.position;
        float angle = Vector3.Angle(dir, top.forward);
        if(angle > 5)
        {
            int axis = Vector3.Dot(Vector3.Cross(dir, top.forward), Vector3.up) > 0 ? -1 : 1;
            GetComponent<Rigidbody>().angularVelocity = axis * Vector3.up * rotateSpeed;
            return false;
        }
        GetComponent<Rigidbody>().velocity = Vector3.zero;
        return true;
    }

    private void HPLookAtCamera()
    {
        Vector3 cameraPos = Camera.main.transform.position;
        Vector3 target = new Vector3(cameraPos.x, hp.position.y, cameraPos.z);
        hp.LookAt(target);
    }

    private void Fire()
    {
        if(fireWaitTime > bulletCoolTime)
        {
            BulletInfo bulletInfo = new BulletInfo("EnemyBullet", Color.yellow, top.forward, 5f, 10f);
            GameObject bullet = Instantiate(bulletPrefab, firePoint.position, Quaternion.identity); // 通过预设体创建炮弹
            bullet.AddComponent<BulletController>().SetBulletInfo(bulletInfo);
            fireWaitTime = 0;
        }
    }

}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/371866.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

协程模式在Android中的应用及工作原理

协程模式在Android中的应用及工作原理 在Android开发中&#xff0c;很多开发者通过代码模式学习协程&#xff0c;通常这已经足够应付了。但这种学习方式忽略了协程背后的精髓&#xff0c;事实上&#xff0c;它们的原理非常简单。那么&#xff0c;是什么使得这些模式起作用呢&a…

SpringBoot整合Flowable最新教程(一)Flowable介绍

一、Flowable 入门介绍 代码实现文章&#xff1a;SpringBoot整合Flowable最新教程&#xff08;二&#xff09; 官网地址&#xff1a;https://www.flowable.org/   Flowable6.3中文教程&#xff1a;中文教程地址   可以在官网下载对应的jar包在本地部署运行&#xff0c;官方…

Qt设计师中(没有现成的控件):如何添加QToolBar工具栏

1、在QtCreator设计师界面中,在MainWindow上右键,有“添加工具栏”菜单项 2、但只有在MainWindow上右键才有&#xff0c;在其它控件上方点击则没有&#xff0c;那么怎么在对话框上添加呢&#xff1f; 可以添加一个QWidget&#xff0c;然后手动在ui文件里把class改为QToolBar就…

PS一键磨皮插件Delicious Retouch for mac中文 支持PS2024

Delicious Retouch for Mac是一款优秀的Photoshop插件&#xff0c;专注于人像修饰。以下是该插件的一些主要特点和功能&#xff1a; 软件下载&#xff1a;Delicious Retouch for mac中文 支持PS2024 人像修饰工具&#xff1a;Delicious Retouch专注于人像修饰&#xff0c;提供了…

react和antd学习笔记

概论 react是前端框架&#xff0c;antd是组件库。前端框架和组件库的区别与联系 nodejs 脚本语言需要一个解析器才能运行&#xff0c;JavaScript是脚本语言&#xff0c;在不同的位置有不一样的解析器&#xff0c;如写入html的js语言&#xff0c;浏览器是它的解析器角色。而对…

寒假 day4

1.请编程实现哈希表的创建存储数组(12,24,234,234,23,234,23),输入key查的值&#xff0c;实现查找功能。 #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> typedef int datatype; typedef struct Node {//数据域datatype…

总结—elasticsearch启动失败的几种情况及解决

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 摘要 本文主要梳理从ES初学以来所遇到的启动失败的几种情况。 1、使用root用户启动失败 在有一次搭建elasticsearch的时候&am…

16-Verilog实现二线制I2C CMOS串行EEPROM的读写操作

Verilog实现二线制I2C CMOS串行EEPROM的读写操作 1&#xff0c;二线制I2C CMOS串行EEPROM的简单介绍2&#xff0c;I2C总线特征介绍3&#xff0c;二线制I2C、CMOS串行EEPROM的读写操作4&#xff0c;EEPROM的Verilog HDL程序4.1&#xff0c;EEPROM的行为模型思路如下&#xff1a;…

Jmeter性能测试: Jmeter 5.6.3 分布式部署

目录 一、实验 1.环境 2.jmeter 配置 slave 代理压测机 3.jmeter配置master控制器压测机 4.启动slave从节点检查 5.启动master主节点检查 6.运行jmeter 7.观察jmeter-server主从节点变化 二、问题 1.jmeter 中间请求和响应乱码 一、实验 1.环境 &#xff08;1&#…

kubesphere部署k8s-v1.23.10

功能&#xff1a; &#x1f578; 部署 Kubernetes 集群 &#x1f517; Kubernetes 多集群管理 &#x1f916; Kubernetes DevOps &#x1f50e; 云原生可观测性 &#x1f9e9; 基于 Istio 的微服务治理 &#x1f4bb; 应用商店 &#x1f4a1; Kubernetes 边缘节点管理 &#x1…

[职场] 财务共享是什么 #笔记#笔记#知识分享

财务共享是什么 财务共享作为一种创新的财务管理模式&#xff0c;已经得到了我国众多企业的认可和实践。通过实施财务共享&#xff0c;企业可以有效提高财务管理效率&#xff0c;降低成本&#xff0c;优化资源配置&#xff0c;从而为企业的可持续发展提供有力保障。本文会进行…

myql 项目数据库和表的设计

1.表的设计和创建 2.在navicate运行这些代码 create table user(id int not null auto_increment primary key,name varchar(50) not null unique,password varchar(50) not null,state enum(online,offline) default offline ); create table friend(userid int not null,…

操作系统基础:文件系统基础【下】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;OS从基础到进阶 ⚔️1 文件的基本操作⚖️1.1 总览⚖️1.2 几种基本操作&#x1f52d;1.2.1 创建文件&#x1f52d;1.2.2 删除文件&#x1f52d;1.2.3 打开文件&#x1f52d;1.2.4 关闭文件…

mysql:事务的特性ACID、并发事务(脏读、不可重复读、幻读、如何解决、隔离级别)、undo log和redo log的区别、相关面试题和答案

事务是一组操作的集合&#xff0c;它会把所有的操作作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 事务的特性&#xff08;ACID&#xff09; 原子性&#xff08;Atomicity&#xff09;&#xff1a;事务是不可分割的…

React16源码: React中处理hydrate的核心流程源码实现

hydrate 1 &#xff09;概述 hydrate 在react当中不算特别重要, 但是很多时候会用到的一个API这个 API 它主要作用就是在进入第一次渲染的时候&#xff0c;如果本身 dom 树上面已经有一个dom结构存在是否可以去利用这一部分已经存在的dom&#xff0c;然后去避免掉在第一次渲染…

2024/2/4 备战蓝桥杯 5-1 前缀和

目录 求和 0求和 - 蓝桥云课 (lanqiao.cn) 可获得的最小取值 0可获得的最小取值 - 蓝桥云课 (lanqiao.cn) 领地选择 P2004 领地选择 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 求和 0求和 - 蓝桥云课 (lanqiao.cn) 思路&#xff1a;先对公式进行合并同类相&#x…

浅压缩、深压缩、双引擎、计算机屏幕编码……何去何从?

专业视听领域尤其显示控制和坐席控制领域&#xff0c;最近几年最激动人心的技术&#xff0c;莫过于分布式了。 分布式从推出之日就备受关注&#xff1a;担心稳定性的&#xff0c;质疑同步性能的&#xff0c;怀疑画面质量的…… 诚然&#xff0c;我们在此前见多了带着马赛克的…

windows安装Visual Studio Code,配置C/C++运行环境(亲测可行)

一.下载 Visual Studio Code https://code.visualstudio.com/ 二.安装 选择想要安装的位置: 后面的点击下一步即可。 三.下载编译器MinGW vscode只是写代码的工具&#xff0c;使用编译器才能编译写的C/C程序&#xff0c;将它转为可执行文件。 MinGW下载链接&#xff1a;…

day31 JS执行机制

目录 前言同步和异步JS执行机制 前言 JavaScript语言的一大特点是单线程。 JavaScript是为处理页面中用户的交互&#xff0c;以及操作DOM而诞生的。比如对某个DOM元素进行添加和删除操作&#xff0c;不能同时进行&#xff0c;应该先进行添加再继续删除。 示例&#xff08;解…

【日常总结】SourceTree 1.5.2.0 更换用户名称和密码

一、场景 二、问题 三、解决方案 > 方案一&#xff1a;删除缓存文件 > 方案二&#xff1a;更新最新版本&#xff0c;可以直接修改密码&#xff08;推荐&#xff09; 方案一&#xff1a;删除缓存文件 Stage 1&#xff1a;设置显示隐藏文件 Stage 2&#xff1a;打开…