350人加入学习
(6人评价)
Steam模拟经营游戏《冒险村的商人日记》从开发到上架全流程

开发 策划 美术 音乐 发行 全部教给你

价格 ¥ 890.00

说一个基础知识吧:MonoBehaviour的构造函数由unity引擎自己调用,什么时候调用,调用几次都不知道的~所以任何关于游戏的逻辑(初始化逻辑)都应该写在Awake或者Start上!

[展开全文]

点击弹窗口的时候 加个变量存一下不就完了嘛

[展开全文]

继承方式,代码更规范

父类

using System;
using UnityEngine;

namespace Production
{
    public abstract class ProductionBuilding : MonoBehaviour
    {
        // 生产产品ID
        public int productId = -1;
        // 生产周期
        public int productPeriod  = 7;
        // 生产数量
        public int productCount  = 5;
        // 维护费用
        public int costMoney  = 5;
        // 生长周期
        public int growPeriod = -1;
    
        // 是否已长大
        protected bool IsGrown;
        // 是否需要生长(growPeriod > 0)
        protected bool IsNeedGrow;    
        // 已经过去的天数
        protected int PassingDays;
        
        public int GrownRemainingDays()
        {
            // 成熟剩余天数
            return growPeriod - PassingDays - 1;
        }

        public int ProductRemainingDays()
        {
            // 生产剩余天数
            return productPeriod - PassingDays - 1;
        }
        
        protected virtual void Awake()
        {
            WorldTimer.DoThisDay += NewDay;
            IsNeedGrow = growPeriod > 0;
        }
    
        public bool IsGrowing()
        {
            // 正在生长:如果生长周期 > 0,且没有长大(默认),且过去的天数不大于生长周期
            // 否则,返回没有在生长了
            return growPeriod > 0 && !IsGrown && PassingDays <= growPeriod;
        }

        // 每个子类需要重写每天的过法
        protected abstract void NewDay();
        
        protected virtual void OnDestroy()
        {
            WorldTimer.DoThisDay -= NewDay;
        }
        
    }
}

农场类

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

namespace Production
{
    public class Farm : ProductionBuilding
    {
        // 到达阶段2的天数
        public int stage2 = 2;
        // 到达阶段3的天数
        public int stage3 = 3;

        // 当前阶段
        private int _currentStage = 1;
        // 当前显示的阶段
        private int _showingStage = 1;
        private readonly Dictionary<int, GameObject> _stages = new Dictionary<int, GameObject>();
        
        protected override void Awake()
        {
            base.Awake();
            for (var i = 1; i <= 3; i++)
            {
                // 加载不同阶段
                _stages[i] = transform.Find(i.ToString()).gameObject;
                if (i >= 2)
                {
                    _stages[i].SetActive(false);
                }
            }
        }
        
        private void Update()
        {
            // 如果当前显示场景等于当前场景,不处理
            if (_showingStage == _currentStage) return;
            // 否则,当前显示场景隐藏,显示当前场景
            _stages[_showingStage].SetActive(false);
            _stages[_currentStage].SetActive(true);
            // 当前显示场景等于当前场景
            _showingStage = _currentStage;
        }

        protected override void NewDay()
        {
            // 农场无条件直接过一天
            PassingDays += 1;
            if (IsNeedGrow && !IsGrown)
            {
                // 如果需要生长且没长大,处理成长逻辑
                if (PassingDays < growPeriod) return;
                IsGrown = true;
                PassingDays = 0;
            }
            else
            {
                // 如果已经长大了,处理生产逻辑
                // 注意这里当前阶段,需要生长的直接跳过第一阶段(如:苹果树)
                _currentStage = PassingDays < stage2 && !IsNeedGrow ? 1 : PassingDays < stage3 ? 2 : 3;
                if (PassingDays < productPeriod) return;
                InstanceManager.Instance.knapsack.AddGoods(productId, productCount);
                _currentStage = 1;
                PassingDays = 0;
            }
        }
    }
}

牧场类

using System;

namespace Production
{
    public class Pasture : ProductionBuilding
    {
        // 消耗的产品ID
        public int consumeProductId;
        // 消耗的数量
        public int consumeCount;

        // 食物是否足够,用于UI展示
        public bool NoFood { get; private set; } = true;

        protected override void NewDay()
        {
            // 减少食物,食物不足不算一天
            var consumeSuccess = InstanceManager.Instance.knapsack.AddGoods(consumeProductId,consumeCount * -1);
            NoFood = !consumeSuccess;
            if (NoFood) return;
            
            // 成长和生产
            PassingDays += 1;
            if (IsNeedGrow && !IsGrown)
            {
                if (PassingDays < growPeriod) return;
                IsGrown = true;
                PassingDays = 0;
            }
            else
            {
                if (PassingDays < productPeriod) return;
                InstanceManager.Instance.knapsack.AddGoods(productId, productCount);
                PassingDays = 0;
            }
        }

        
    }
}

 

[展开全文]

换个思路,点击按钮时不要侵入UI逻辑,UI组件自己找该显示什么

using System;
using Architecture;
using Class;
using Game;
using UnityEngine;
using UnityEngine.UI;

namespace UI
{
    public class FarmInfoWin : Window<FarmInfoWin>
    {
        public Text titleText;
        public Text productNameText;
        public Image productIconImage;
        public Text outputText;
        public Text timerText;
        public Button destroyBtn;

        private Building _curShowingBuilding;
        private Farm _curShowingFarm;

        protected override void Awake()
        {
            base.Awake();
            destroyBtn.onClick.AddListener(DestroyFarm);
            WorldTimer.DoThisDay += UpdateTimerText;
        }

        public override void Show()
        {
            base.Show();
            UpdateInfo();
        }

        private void UpdateInfo()
        {
            // 获取点击时的建筑物
            var curGround = GameController.Instance.CurrentSelectGroundProperties;
            _curShowingBuilding = Dict.BuildingDict[curGround.BuildingId];
            _curShowingFarm = curGround.transform.Find("Building")
                .GetComponent<Farm>();

            var curGoods = Dict.GoodsDict[_curShowingFarm.productId];
            
            titleText.text = _curShowingBuilding.Name;
            productNameText.text = curGoods.Name;
            productIconImage.sprite = Resources.Load<Sprite>("Icon/" + curGoods.ResName);

            var prefix = _curShowingFarm.growPeriod > 0 ? _curShowingFarm.growPeriod + "天成熟,然后" : "";
            outputText.text = prefix + "每" + _curShowingFarm.productPeriod + "天产出" + _curShowingFarm.count + "个" + curGoods.Name;
            
            UpdateTimerText();
        }

        private void UpdateTimerText()
        {
            timerText.text = _curShowingFarm.IsGrowing()
                ? "未成熟,距离成熟还有" + (_curShowingFarm.growPeriod - _curShowingFarm.PassingDays) + "天"
                : "距离收获还有" + (_curShowingFarm.productPeriod - _curShowingFarm.PassingDays) + "天";
        }

        private void DestroyFarm()
        {
            Hide();
            // TODO 拆除建筑,改变GroundProperties的State
        }
    }
    
}

 

[展开全文]

有点乱,个人觉得更好的实现

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

namespace Architecture
{
    public class Farm : MonoBehaviour
    {
        public int productId = -1;
        public int productPeriod  = 7;
        public int count  = 5;
        public int cost  = 5;
        public int growPeriod = -1;


        public int stage2 = 2;
        public int stage3 = 3;

        private int _currentStage = 1;
        private int _showingStage = 1;
        private bool _isGrown;
        private bool _isNeedGrow;
        private int _passingDays;
        private readonly Dictionary<int, GameObject> _stages = new Dictionary<int, GameObject>();


        private void Awake()
        {
            WorldTimer.Instance.DoThisDay += NewDay;
            for (var i = 1; i <= 3; i++)
            {
                _stages[i] = transform.Find(i.ToString()).gameObject;
                if (i >= 2)
                {
                    _stages[i].SetActive(false);
                }
            }
            _isNeedGrow = growPeriod > 0;
        }
        
        private void Update()
        {
            if (_showingStage == _currentStage) return;
            _stages[_showingStage].SetActive(false);
            _stages[_currentStage].SetActive(true);
            _showingStage = _currentStage;
        }

        private void NewDay()
        {
            _passingDays += 1;

            if (growPeriod > 0 && !_isGrown)
            {
                if (_passingDays <= growPeriod) return;
                _isGrown = true;
                _passingDays = 0;
            }
            else
            {
                _currentStage = _passingDays < stage2 && !_isNeedGrow ? 1 : _passingDays < stage3 ? 2 : 3;
                if (_passingDays < productPeriod) return;
                _passingDays = 0;
                Knapsack.Instance.AddGoods(productId, count);
            }
        }

        private void OnDestroy()
        {
            WorldTimer.Instance.DoThisDay -= NewDay;
        }
    }
}

 

[展开全文]

更好的实现方法

using UnityEngine;

public class CameraMove : MonoBehaviour
{
    private const float MoveSpeed = 0.15f;
    private static readonly Vector3 MinPos = new Vector3(-14, 14, -12);
    private static readonly Vector3 MaxPos = new Vector3(11, 14, 15);
    private static readonly Vector3 InitPos = new Vector3(0, 14, 0);

    private void Start()
    {
        transform.position = InitPos;
    }

    private void Update()
    {
        var trans = transform;
        var position = trans.position;
        
        var x = Input.GetAxis("Horizontal");
        var z = Input.GetAxis("Vertical");

        var newX = position.x + x * MoveSpeed;
        var newZ = position.z + z * MoveSpeed;

        newX = Mathf.Clamp(newX, MinPos.x, MaxPos.x);
        newZ = Mathf.Clamp(newZ, MinPos.z, MaxPos.z);
        
        trans.position = new Vector3(newX, position.y, newZ);
    }
}
[展开全文]

判断指定的碰撞对象  要修改对象的标签

[展开全文]

C# 事件监听操作

 

https://blog.csdn.net/Joyeishappy/article/details/96469090

 

//窗口1 Form1

注册监听事件


 
  1. public delegate void ListenerHandler();

  2. public event ListenerHandler Listener=null;

  3. public void DoSomeThing() {

  4. if (Listener!=null)//确定事件已被订阅,也就是已被注册

  5. {

  6. Listener();//触发事件

  7. }

  8. }

//窗口2 Form2


 
  1. Form1 f1=new Form1();

  2. f1. Listener+=new ListenerHandler(noteMe);//订阅(注册)窗口1的Listener事件

  3. //事件处理方法

  4. private void noteMe(){

  5. //定义窗口1的Listener事触发后执行的动作

  6. }

//执行

f1. DoSomeThing();//执行

//触发事件是个主动的过程,没有什么监听,就像你执行一个方法一样

[展开全文]

txt记得另存为UTF-8编码

[展开全文]

摄像头设置移动速度过快的时候有个问题, 那就是摄像头的 `Transform.position` 移动的时候出现误差.

比如最大值设定为 `15` 的时候, 移动速度过快直接移动到角落会出现 `15.0001` 之类的值, 这时候就会卡死没办法移动, 所以这里需要判断移动位置的极限值.

using UnityEngine;

/// <summary>
/// 摄像头移动
/// </summary>
public class CameraMove : MonoBehaviour{
    
    public Camera target;
    public float moveSpeed = 100f;
    
    
    public Vector2 mixPos = new Vector2(-15, -15);// 最小距离
    public Vector2 maxPos = new Vector2(15 , 15);// 最大距离

    /// <summary>
    /// 程序初始化方法
    /// </summary>
    void Awake() {
        if (target == null) target = Camera.main;    
    }

    void Update() {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        // 获取当前的摄像头位置
        var pos = target.transform.position;

        // 判断目前的是否达到了最大速度
        if (pos.x < mixPos.x || pos.z < mixPos.y) x = 0 ;
        if (pos.x > maxPos.x || pos.z > maxPos.y) z = 0 ;

        Debug.Log(pos);
        // 利用开始进行对象移动
        target.transform.position = new Vector3(
            pos.x + x * moveSpeed ,
            pos.y,
            pos.z + z * moveSpeed 
        );

        // 为了防止出现 15.0001 这种情况需要直接判断最小值和最大值
        pos = target.transform.position;// 再次获取位置准备校验距离
        if (pos.x < mixPos.x) pos.x = mixPos.x;
        if (pos.z < mixPos.y) pos.z = mixPos.y;

        if (pos.x > maxPos.x) pos.x = maxPos.x;
        if (pos.z > maxPos.y) pos.z = maxPos.y;

        // 检查完成重置位置
        target.transform.position = pos;

    }
}

 

[展开全文]

最后点击 UI 的时候会出现射线被锁定到 UI 上面的问题, 也就是点击 Ground 的时候弹出窗口点击购买无法更新玩家的价格, 这里先采用比较简单的遮挡 UI 方法后续看看是否有其他方法处理: 

using UnityEngine;
using UnityEngine.EventSystems;// 引入 Unity 事件系统

// <summary>
/// 游戏控制器类
/// </summary>
public class GameController : MonoBehaviour{

    /// <summary>
    /// 更新方法
    /// </summary>
    void Update(){
        // EventSystem.current.IsPointerOverGameObject 用来判断目前鼠标是不是锁定在 UI 层上
        // 实际上判断鼠标是否在放置在含有 Canvas 组件对象上( 创建 UI 时候自动附加在 UI 的根部件 )
        if (!EventSystem.current.IsPointerOverGameObject()) {
            // 这样就能成功锁定到射线指定的 Ground 对象
            _ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(_ray , out _hit , rayDistance)) {
                if (_hit.collider.CompareTag("Ground")) {
                    activeGroundProp = _hit.collider.GetComponent<GroundProperties>();
                }
            }
        }
    }
}

 

 

 

 

 

 

 

[展开全文]

这里要对游戏要素进行拆分, 需要细分游戏的要素( 农场游戏来说 ):

  • 地块需要包含以下的内容:

    • 类型, 用于区分是否可以被购买, 是否可以进行建造, 是否可以移除

    • 价格, 用于土地是否可以被玩家购买下来

  • 玩家需要包含以下的信息:

    • 身上的拥有多少金币, 是否可以进行购买/建造操作

  • 界面需要包含以下信息:

    • 点击土地的时候, 是弹出还是显示对应窗口

    • 判断是购买弹出窗口界面, 还是建造/升级窗口界面

 

对于类型可以利用数值来处理类型, 也可以通过 `enum` 枚举来定义选择类型, 按照使用情况来进行选择即可.

 

GroundProperties 脚本内容:

using UnityEngine;

/// <summary>
/// 土地属性类型
/// </summary>
public class GroundProperties : MonoBehaviour{

    /// <summary>
    /// 土地的通用类型标识( 公开的类型对象 )
    /// 可以通过 GroundProperties.StatusFlags.Empty 直接获取
    /// </summary>
    public enum StatusFlags {
        Empty, // 空地
        Payed, // 已购买
        Build, // 已经建造
    }

    /// <summary>
    /// 土地当前状态
    /// </summary>
    public StatusFlags Status { get; private set; }

    /// <summary>
    /// 土地的价格
    /// </summary>
    public readonly int Price = 200;
}

 

GameController  脚本内容:

using UnityEngine;

/// <summary>
/// 游戏控制器类
/// </summary>
public class GameController : MonoBehaviour{
    // 其他代码略
    void Update(){
        // 获取土地状态, 并判断土地状态
        switch (activeGroundProp.Status) {
            case GroundProperties.StatusFlags.Empty:
                Debug.Log("弹出购买土地");

                break;
            case GroundProperties.StatusFlags.Payed:
                Debug.Log("弹出购买土地");

                break;
            case GroundProperties.StatusFlags.Build:
                Debug.Log("弹出建造土地");

                break;
        }  
    }
}

 

[展开全文]

Unity 之中的委托应用十分广泛, 这里假设背包类对象编写, 一般背包道具更新会设置以下事件:

  • 记录背包道具变动

  • 如果界面是通用金币, 则需要更新界面金币数量

  • 如果涉及敌人也需要通知并且针对的类型, 需要去通知敌人, 玩家已经获得某个道具

 

using System;
using System.Collections;
using UnityEngine;


/// <summary>
/// 背包对象类
/// </summary>
public class Bag : MonoBehaviour{

    // 假设目前的背包对象列表
    private ArrayList items = default;

    // 注意委托默认不对外则为 private, 如果要手动对外绑定则直接声明 public
    // 这里默认返回 bool 是为了考虑到如果背包满了需要弹出提示错误
    public delegate bool ItemChange(int val);

    // 声明背包的委托事件[ 对外开放, 可以被其他脚本绑定 ]
    public ItemChange changeItem;


    // 初始化方法
    private void Awake() {
        changeItem += AddItem;// 追加添加道具委托方法
        changeItem += UpdateUI;// 追加获取道具的之后更新界面
        changeItem += NotifyMessage;// 追加推送全局通知道具消息[ 可以同步推送给敌人/好友 ]
    }


    // 对外方法, 用于唤醒所有的道具获取的更新事件
    public void UpdateItem(int val) {
        if (changeItem.Invoke(val)) {
            Debug.Log("获取成功");
        } else {
            Debug.Log("获取失败");
        };
    }


    // 追加道具[ 默认背包对象脚本本体成员,名为 `Bag` ]
    private bool AddItem(int val) {
        items.Add(val);
        return true;
    }


    // 以下为假设情况
    // ===================================================

    // 更新界面获取道具[ 假设为界面脚本的类成员, 名为 `GUI` ]
    private bool UpdateUI(int val) {
        Debug.Log("获取道具 = " + val);
        return true;
    }

    // 弹出内部消息提示[ 假设为弹出消息框架提示用户, 名为 `Message` ]
    private bool NotifyMessage(int val) {
        Debug.Log("敌对对象注意, 玩家XXX获取道具 = " + val);
        return true;
    }
}

 

[展开全文]

添加监听事件

Button.onClick.AddListener(delegate{函数名();});

移除监听事件

Button.onClick.RemoveAllListeners();

[展开全文]

授课教师

独立游戏制作人,业余程序

课程特色

视频(245)
下载资料(22)
图文(1)