仿保卫萝卜Unity塔防游戏开发


前段时间笔记都写在平板上了哈哈 后面发现还是电脑方便好用 以后还是写在电脑上吧

框架的高层设计

包含策划设计的应用层明显需求,程序需要自己设计框架性需求
在这里插入图片描述

新建项目与项目结构

在这里插入图片描述
建立如图所示的项目结构,将其中的导出设置依次设置其中四个scenes

编写框架

对象池

在这里插入图片描述
对象池结构

IReusable interface

作为ReusableObject的上层接口供给ReusableObject挂在Gameobject上面

知识补充:abstract和virtual的区别

virtual和abstract都是用来修饰父类的,通过覆盖父类的定义,让子类重新定义。
(1)virtual修饰的方法必须有实现(哪怕是仅仅添加一对大括号),而abstract修饰的方法一定不能实现。
(2)virtual可以被子类重写,而abstract必须被子类重写。
(3)如果类成员被abstract修饰,则该类前必须添加abstract,因为只有抽象类才可以有抽象方法。
(4)无法创建abstract类的实例,只能被继承无法实例化。

代码实现

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public inteface IReusable
{
    //生成时调用的方法
    void OnSpawn();
    //删除时调用的方法
    void OnUnSpawn();
}

UseableObjects

代码实现

/*
 * @Author: Tongz
 * @Date: 2022-07-25 15:32:33
 * @LastEditors: Tongz
 * @LastEditTime: 2022-07-25 16:05:24
 * @FilePath: \undefinedd:\Unity\luobo\Assets\Game\Scripts\Reusableobject.cs
 * @Description: [email protected]
 * 
 */
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class Reusableobject : MonoBehaviour,IReusable
{
    //定义抽象类和抽象方法,子类必须对其进行重写
    public abstract void OnSpawn();
    public abstract void OnUnSpawn();
}

Subpool

作为各个物体的子池,提供了Spawn,UnSpawn和UnSpawnAll方法

Gameobject.instantiate

instantiate是对场景中已经存在的的对象进行一份复制的方法,resources.load和instantiate的区别是第一个GameObject prefab的Resources.Load是“解释”你要加载的东西是个GameObject并且加载到内存中。第二个GameObject instance是对prefab进行一个深度copy克隆到场景中然后从Instantiate的返回值中持有他的引用。

在这里插入图片描述

代码实现

/*
 * @Author: Tongz
 * @Date: 2022-07-25 16:30:46
 * @LastEditors: Tongz
 * @LastEditTime: 2022-07-26 14:49:36
 * @FilePath: \luobo\Assets\Game\Scripts\Subpool.cs
 * @Description: [email protected]
 * 
 */
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Text;
public class Subpool
{
    //声明prefab以及集合作为对象池
    GameObject m_prefab;
    List<GameObject> m_objects = new List<GameObject>();
    //构造函数为m_prefab赋值
    public Subpool(GameObject prefab)
    {
        m_prefab=prefab;
    }
    //子池中Gameobj的生成方法
    public GameObject Spawn()
    {
        GameObject go = null;
        foreach (GameObject item in m_objects)
        {
            if(!item.activeSelf)
            {
                go = item;
                break;
            }
        }
        if(go == null)
        {
        go = GameObject.Instantiate<GameObject>(m_prefab);
        m_objects.Add(go);              
        }
        go.SetActive(true);
        go.SendMessage("OnSpawn",SendMessageOptions.DontRequireReceiver);
        return go;

    }
    //移除子池中一个gameobject
    public void Unspawn(GameObject go)
    {
        if(m_objects.Contains(go))
        {
            go.SetActive(false);
            go.SendMessage("OnUnSpawn",SendMessageOptions.DontRequireReceiver);
        }
    }
    //移除子池中所有的gameobject
    public void UnspawnAll()
    {
        foreach (GameObject item in m_objects)
        {
            if(item.activeSelf)
            {
                Unspawn(item);
            }
        }
    }
    //供给上层使用验证是否包含gameobj
    public bool Contains(GameObject go )
    {
        return m_objects.Contains(go);
    }
}

ObjectPool

作为总池来管理子池,统筹subpool并且能动态创建subpool,实现指哪打哪的在内存池中生成物体和销毁物体的效果

代码实现

/*
 * @Author: Tongz
 * @Date: 2022-07-26 14:54:32
 * @LastEditors: Tongz
 * @LastEditTime: 2022-07-26 15:47:06
 * @FilePath: \luobo\Assets\Game\Scripts\ObjectPool.cs
 * @Description: [email protected]
 * 
 */

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

public class ObjectPool : MonoBehaviour
{
    //字典存储子池和他对应的名字
    Dictionary<string,Subpool> m_pools = new Dictionary<string,Subpool>();
    //资源相对于Resources目录所在的的相对位置
    public string ResourceDir = "";
    public GameObject Spawn(string name)
    {
        //若无子池创建子池
        if(!m_pools.ContainsKey(name))
        {
            CreateSubpool(name);
        }
        //调用子池方法生成物体
        return m_pools[name].Spawn();
    }
    public void CreateSubpool(string name)
    {
        //先令path为空
        string path = null;
        //判断相对路径是否为空,设定目录位置
        if(string.IsNullOrEmpty(ResourceDir))
            path = name;
        else
            path = ResourceDir + "/" + name;
        GameObject prefab = Resources.Load<GameObject>(path);
        Subpool subpool = new Subpool(prefab);
        m_pools.Add(name,subpool);
    }
    //采取这种方法的目的是要取到目标的subpool并且调用其中方法所以逐个subpool遍历
    public void Unspawn(GameObject go)
    {
        Subpool subpool = null;
        foreach (Subpool item in m_pools.Values)
        {
            if(item.Contains(go))
            {
                subpool=item;
                break;
            }
        }
        subpool.Unspawn(go);
    }
    //直接逐个释放每个subpool
    public void UnspawnAll()
    {
        foreach (Subpool item in m_pools.Values)
        {
            item.UnspawnAll();
        }
    }
}

Mono的单例模式基类

之前框架的里面都写过了不明白了回去看这里就不解释了

代码实现

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

public class SingletonMono <T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;
    public static T GetInstance()
    {
        return instance;
    }
    protected virtual void Awake() 
    {
        instance = this as T;
    }
}

音乐音效播放模块

代码实现

/*
 * @Author: Tongz
 * @Date: 2022-07-26 17:54:04
 * @LastEditors: Tongz
 * @LastEditTime: 2022-07-26 18:41:59
 * @FilePath: \luobo\Assets\Scripts\MusicMgr.cs
 * @Description: [email protected]
 * 
 */
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MusicMgr : SingletonMono<MusicMgr>
{
    //分开播放背景音乐和效果音乐
    public string ResourceDir ="";
    public AudioSource m_BgMusic;
    public AudioSource m_EffectMusic;
    protected override void Awake() 
    {
        base.Awake();
        m_BgMusic = this.gameObject.AddComponent<AudioSource>();
        m_BgMusic.loop=true;
        m_BgMusic.playOnAwake=false;

        m_EffectMusic = this.gameObject.AddComponent<AudioSource>();
    }
    public float BgVolume
    {
        get{return m_BgMusic.volume;}
        set{m_BgMusic.volume = value;}
    }
    public float EffectVolume
    {
        get{return m_EffectMusic.volume;}
        set{m_EffectMusic.volume = value;}
    }
    public void PlayBgMusic(string name)
    {
        //先检查音乐有没有在播放,若没有在播放则更换切片后播放,注意路径问题
        string oldName;
        //先取得在播放的Audioname 判断是否正在播放 若正在播放则不播放
        if(m_BgMusic.clip == null)
            oldName="";
        else
            oldName =m_BgMusic.clip.name;
        if(oldName!=name) 
        {
            //先令path为空
            string path = null;
            //判断相对路径是否为空,设定目录位置
            if(string.IsNullOrEmpty(ResourceDir))
                path = name;
            else
                path = ResourceDir + "/" + name;
            AudioClip clip=Resources.Load<AudioClip>(path);
            m_BgMusic.clip=clip;
            m_BgMusic.Play();
        }

    }
    public void StopBgMusic()
    {
        m_BgMusic.Stop();
        m_BgMusic.clip = null;
    }
    public void PlayEffectMusic(string name)
    {
        //先令path为空
        string path = null;
        //判断相对路径是否为空,设定目录位置
        if(string.IsNullOrEmpty(ResourceDir))
            path = name;
        else
            path = ResourceDir + "/" + name;
        AudioClip clip=Resources.Load<AudioClip>(path);
        m_EffectMusic.PlayOneShot(clip);
    }


}

MVC框架

MVC原理

在这里插入图片描述

新建场景编辑器

声明各个类

Tile格子类

定义了格子和其中存储的能否放塔,和其中存储的数据

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

//格子信息
public class Tile
{
    public int X;
    public int Y;
    public bool CanHold; //是否可以放置塔
    public object Data; //格子所保存的数据

    public Tile(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }

    public override string ToString()
    {
        return string.Format("[X:{0},Y:{1},CanHold:{2}]",
            this.X,
            this.Y,
            this.CanHold
            );
    }
}

Round怪物波数类

包含了怪物的id和数量并提供了构造方法来设置数值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

public class Round
{
    public int Monster; //怪物类型ID
    public int Count;   //怪物数量

    public Round(int monster, int count)
    {
        this.Monster = monster;
        this.Count = count;
    }
}

Point各个格子的中心点类

包含每个格子中心的x,y对,用作路径和可放塔点的存储

using UnityEngine;
using System.Collections;

//格子坐标
public class Point
{
    public int X;
    public int Y;

    public Point(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

level关卡类

包含要读取的路径的图片,包含要读取的背景的图片名,level关卡的名字,可放塔点数组,路径数组,怪物波数组,初始的金币数

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

public class Level
{
    //名字
    public string Name;

    //背景
    public string Background;

    //路径
    public string Road;

    //金币
    public int InitScore;

    //炮塔可放置的位置
    public List<Point> Holder = new List<Point>();

    //怪物行走的路径
    public List<Point> Path = new List<Point>();

    //出怪回合信息
    public List<Round> Rounds = new List<Round>();
}

Map类实现各个功能

计算地图大小,格子大小

通过camera.viewporttoworldpoint,来通过摄像机的观察坐标获得对应的世界坐标,通过相减算出width和height,除以列数和行数来计算出对应的格子宽和格子高

    void CalculateSize()
    {
        Vector3 leftDown = new Vector3(0, 0);
        Vector3 rightUp = new Vector3(1, 1);

        Vector3 p1 = Camera.main.ViewportToWorldPoint(leftDown);
        Vector3 p2 = Camera.main.ViewportToWorldPoint(rightUp);

        MapWidth = (p2.x - p1.x);
        MapHeight = (p2.y - p1.y);

        TileWidth = MapWidth / ColumnCount;
        TileHeight = MapHeight / RowCount;
    }

得到格子中心点的世界坐标

将格子的坐标remap到摄像机中心在世界中心原点的position上

    Vector3 GetPosition(Tile t)
    {   
        return new Vector3(
                -MapWidth / 2 + (t.X + 0.5f) * TileWidth,
                -MapHeight / 2 + (t.Y + 0.5f) * TileHeight,
                0
            );
    }

根据格子的行和列还得到对应的格子

     Tile GetTile(int tileX, int tileY)
    {
        int index = tileX + tileY * ColumnCount;

        if (index < 0 || index >= m_grid.Count)
            return null;

        return m_grid[index];
    }

获取鼠标所在的世界位置

利用input.mouse并将其从screentoviewportpoint转换到view视图,再通过viewport转换到worldposition

    Vector3 GetWorldPosition()
    {
        Vector3 viewPos = Camera.main.ScreenToViewportPoint(Input.mousePosition);
        Vector3 worldPos = Camera.main.ViewportToWorldPoint(viewPos);
        return worldPos;
    }

计算鼠标所在的格子

用鼠标的x加上二分之一宽除以格子宽 再取int值舍去小数部分就可以得到鼠标所在的格子
用鼠标的y加上二分之一高除以格子高 再取int值舍去小数部分就可以得到鼠标所在的格子

    Tile GetTileUnderMouse()
    {
        Vector2 wordPos = GetWorldPosition();
        int col = (int)((wordPos.x + MapWidth / 2) / TileWidth);
        int row = (int)((wordPos.y + MapHeight / 2) / TileHeight);
        return GetTile(col, row);
    }

在编辑器中执行的OnDrawGizmos

提供一个画出辅助线的方法
在可以放置塔的格子放置加号图标
在初始路线放置起点图标
在路线尽头放置结束的图标
在中间的点位之间画上连线


    void OnDrawGizmos()
    {
    // 提供一个画线的布尔值判断是否画线
        if (!DrawGizmos)
            return;

        //计算地图和格子大小
        CalculateSize();


        Gizmos.color = Color.green;
        //绘制格子
        //绘制行
        for (int row = 0; row <= RowCount; row++)
        {
            Vector2 from = new Vector2(-MapWidth / 2, -MapHeight / 2 + row * TileHeight);
            Vector2 to = new Vector2(-MapWidth / 2 + MapWidth, -MapHeight / 2 + row * TileHeight);
            Gizmos.DrawLine(from, to);
        }

        //绘制列
        for (int col = 0; col <= ColumnCount; col++)
        {
            Vector2 from = new Vector2(-MapWidth / 2 + col * TileWidth, MapHeight / 2);
            Vector2 to = new Vector2(-MapWidth / 2 + col * TileWidth, -MapHeight / 2);
            Gizmos.DrawLine(from, to);
        }
                foreach (Tile t in m_grid)
        {
            if (t.CanHold)
            {
                Vector3 pos = GetPosition(t);
                Gizmos.DrawIcon(pos, "holder.png", true);
            }
        }

        Gizmos.color = Color.red;
        for (int i = 0; i < m_road.Count; i++)
        {
            //起点
            if (i == 0)
            {
                Gizmos.DrawIcon(GetPosition(m_road[i]), "start.png", true);
            }

            //终点
            if (m_road.Count > 1 && i == m_road.Count - 1)
            {
                Gizmos.DrawIcon(GetPosition(m_road[i]), "end.png", true);
            }

            //红色的连线
            if (m_road.Count > 1 && i != 0)
            {
                Vector3 from = GetPosition(m_road[i - 1]);
                Vector3 to = GetPosition(m_road[i]);
                Gizmos.DrawLine(from, to);
            }
        }

UnityWetRequest加载图片

    public static IEnumerator LoadImage(string url, Image image)
    {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture(url);
        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.Log(www.error);
        }

        else
        {
            Texture2D texture = DownloadHandlerTexture.GetContent(www);
            Sprite sp = Sprite.Create(
             texture,
             new Rect(0, 0, texture.width, texture.height),
             new Vector2(0.5f, 0.5f));
            image.sprite = sp;
        }
    }

修改spriterender上的sprite

    public string BackgroundImage
    {
        set
        {
            SpriteRenderer render = transform.Find("Background").GetComponent<SpriteRenderer>();
            StartCoroutine(Tools.LoadImage(value, render));
        }
    }

    public string RoadImage
    {
        set
        {
            SpriteRenderer render = transform.Find("Road").GetComponent<SpriteRenderer>();
            StartCoroutine(Tools.LoadImage(value, render));
        }
    }
            //加载图片
        this.BackgroundImage = "file://" + Consts.MapDir + "/" + level.Background;
        this.RoadImage = "file://" + Consts.MapDir + "/" + level.Road;

地图编辑器MapEditor

作为地图编辑器的图形接口显示在Inspector上面
加头文字*[CustomEditor(typeof(Map))]* 声明要对Map进行inspector修改

猜你喜欢

转载自blog.csdn.net/TongOuO/article/details/125960669