制作手机游戏的原型,从哪里开始以及如何做。 第一部分

碰巧我经常制作原型(用于工作和个人项目)
我想分享我的经验。 我想写一篇有趣的文章来阅读我自己,首先我想知道一个人在项目实施中做出这个或那个决定,他如何开始这个项目时所受到的指导通常是最困难的。

我想和您一起考虑的一个新例子是基于物理学的休闲拼图的概念。 有时我会提到一些显而易见的事情,以便为初学者扩展话题。

想法看起来像这样




第二部分
第三部分

我们在运动场上布置各种修改器,这些修改器会改变火箭的方向,吸引,加速,击退等等。 任务是为恒星之间通往我们需要的下一个行星铺平道路。 着陆/触及星球痕迹时会考虑获胜。 运动场是垂直的,有几个屏幕,假设轨道可能会左右占据屏幕地板。 迷失-如果您错过了与另一行星相撞的目标行星,则飞越了该区域。

我希望我们所有人都知道,在进入开发和实施我们的计划之前,我们必须首先制造一个原型-一种非常粗糙且快速的产品,使您能够快速尝试基本的技巧并了解游戏玩法。 原型是做什么用的? 在我们头脑中和实践中的游戏过程是完全不同的事情,对我们来说似乎很酷,对于其他人而言,这将是完全的恐怖,此外,项目中经常会出现一些有争议的时刻-管理,游戏规则,游戏动态等。 等等。 跳过原型阶段,遍历整个体系结构,交付图形,调整级别并最终发现游戏很糟糕,这将是非常愚蠢的。 从生活开始-在一场比赛中有大约10件迷你游戏,在原型制作后,结果证明它们非常无聊,一半被扔掉,一半被重做。

提示-除了基本机制外,还要将有争议的地方隔离开,写下对原型的特定期望。 这样做是为了在困难时刻重新加载。 例如,该原型所面临的任务之一是,运动场由多个屏幕组成并且需要缠绕,这是多么方便和容易理解。 有必要实施计划A-svayp。 计划B-缩放比赛场地的能力(如有必要)。 修饰符也有几个选项。 第一个想法在屏幕截图中可见-我们公开了修饰符及其影响的方向。 结果,修改器被简单地替换为球体,当它们接触球体时会改变火箭的方向。 我们认为这会更加随意,没有轨迹等。

我们实现的常规功能:

  1. 您可以设置火箭的初始轨迹,并限制与垂直线的偏离度(火箭向侧面的旋转角度不能超过一个度)
  2. 应该有一个开始按钮,通过它我们可以将火箭发送到路上
  3. 放置修饰符时滚动屏幕(开始)
  4. 摄像机在播放器后方移动(启动后)
  5. 界面面板,将在现场使用拖放修饰符
  6. 原型必须具有两个修改器-排斥力和加速度
  7. 当你触摸到死亡时一定有行星
  8. 当您触摸时,必定有行星

建筑学


非常重要的一点是,在大型开发中,人们普遍认为原型的编写速度会更快,这太可怕了,然后仅从头开始编写项目。 在许多项目的严酷现实中,支腿从原型中脱颖而出,这不利于大型开发-曲线架构,遗留代码,技术债务,额外的重构时间。 好了,整个独立开发都从原型顺利发展到Beta。 我为什么呢?即使是原始的,也必须将架构放到原型中,这样您才不会在以后哭,哭泣或在同事面前脸红。

在开始任何工作之前,我们总是重复–固体,亲吻,干,雅尼。 即使是经验丰富的程序员也忘记了Kiss和Yagni。

我坚持什么基本架构

场景中有一个带有相应标签的空GameObject GameController,组件/单车挂在其上,最好将其制成预制件,然后根据需要将组件添加到预制件中:

  1. GameController-(负责游戏的状态,直接负责逻辑(赢,输,多少生命等)
  2. InputController-与玩家管理,跟踪tachi,点击次数,点击者,控制状态等相关的所有信息
  3. TransformManager-在游戏中,您经常需要知道谁在哪里,与玩家/敌人的位置有关的各种数据。 例如,如果我们飞过一颗行星,那么玩家将被击败,游戏控制器对此负责,但他必须从哪里知道玩家的位置。 转换管理器正是了解事物的本质
  4. AudioController-这里很清楚,与声音有关
  5. InterfacesController-显而易见,这是关于UI的

整体情况显现-为每个可理解的任务创建一个控制器/实体来解决这些问题,这将避免使用google之类的对象,它可以了解挖掘的位置,我们将数据提供给控制器,我们可以随时更改接收数据的实现。 不允许使用公共字段,我们仅通过公共属性/方法提供数据。 我们在本地计算/更改数据。

有时,由于各种特定的逻辑和计算,会导致GameController膨胀。 如果我们需要处理数据-为此,最好创建一个单独的类GameControllerModel并在那里进行处理。

这样代码就开始了


火箭基础课
using GlobalEventAggregator; using UnityEngine; using UnityEngine.Assertions; namespace PlayerRocket { public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, } [RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper { [SerializeField] protected RocketConfig config; protected Rigidbody rigidbody; protected InputController inputController; protected RocketHolder rocketHolder; protected RocketState rocketState; public Transform Transform => transform; public Rigidbody RigidbodyForForce => rigidbody; RocketState IPlayerHelper.RocketState => rocketState; protected ForceModel<IUseForces> forceModel; protected virtual void Awake() { Injections(); EventAggregator.AddListener<ButtonStartPressed>(this, StartEventReact); EventAggregator.AddListener<EndGameEvent>(this, EndGameReact); EventAggregator.AddListener<CollideWithPlanetEvent>(this, DestroyRocket); rigidbody = GetComponent<Rigidbody>(); Assert.IsNotNull(rigidbody, "    " + gameObject.name); forceModel = new ForceModel<IUseForces>(this); } protected virtual void Start() { Injections(); } private void DestroyRocket(CollideWithPlanetEvent obj) { Destroy(gameObject); } private void EndGameReact(EndGameEvent obj) { Debug.Log("     "); rocketState = RocketState.STOP; } private void Injections() { EventAggregator.Invoke(new InjectEvent<InputController> { inject = (InputController obj) => inputController = obj}); EventAggregator.Invoke(new InjectEvent<RocketHolder> { inject = (RocketHolder holder) => rocketHolder = holder }); } protected abstract void StartEventReact(ButtonStartPressed buttonStartPressed); } public interface IPlayerHelper { Transform Transform { get; } RocketState RocketState { get; } } } 


让我们来上课:

  [RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper 

首先,为什么类是抽象的? 我们不知道我们将拥有哪种火箭,它们将如何移动,将如何被动画化,将提供哪些游戏功能(例如,火箭弹向侧面的可能性)。 因此,我们将基类抽象化,在其中放置标准数据,然后放置抽象方法,具体导弹的实现方法可能有所不同。

还可以看到,该类已经具有接口的实现和属性,该属性将在游戏上悬挂必需的组件,没有它,火箭就不是火箭。

 [SerializeField] protected RocketConfig config; 

这个属性告诉我们,检查器有一个可序列化的字段,在大多数课程中(包括Unity),对象被塞入该字段中。这些字段是公开的,如果您是独立开发人员,则不要这样做。 使用私有字段和此属性。 在这里,我想稍微介绍一下该类的内容及其作用。

Rocketconfig
 using UnityEngine; namespace PlayerRocket { [CreateAssetMenu(fileName = "RocketConfig", menuName = "Configs/RocketConfigs", order = 1)] public class RocketConfig : ScriptableObject { [SerializeField] private float speed; [SerializeField] private float fuel; public float Speed => speed; public float Fuel => fuel; } } 


这是一个可存储火箭设置的ScriptableObject。 这使游戏设计师需要的数据池超出了课堂。 因此,游戏设计人员无需搞乱并使用特定的火箭来设置特定的游戏项目,他们只需要修复此配置即可,该配置存储在单独的资产/文件中。 他们可以配置运行时配置并将其保存,也有可能为火箭购买不同的外观,并且参数是相同的-该配置只是在需要的地方徘徊。 这种方法正在很好地扩展-您可以添加任何数据,编写自定义编辑器等。

 protected ForceModel<IUseForces> forceModel; 

我也想对此进行详细介绍,这是一个将修饰符应用于对象的通用类。

力模型
 using System.Collections.Generic; using System.Linq; using UnityEngine; public enum TypeOfForce { Push = 0, AddSpeed = 1, } public class ForceModel<T> where T : IUseForces { readonly private T forceUser; private List<SpaceForces> forces = new List<SpaceForces>(); protected bool IsHaveAdditionalForces; public ForceModel(T user) { GlobalEventAggregator.EventAggregator.AddListener<SpaceForces>(this, ChangeModificatorsList); forceUser = user; } private void ChangeModificatorsList(SpaceForces obj) { if (obj.IsAdded) forces.Add(obj); else forces.Remove(forces.FirstOrDefault(x => x.CenterOfObject == obj.CenterOfObject)); if (forces.Count > 0) IsHaveAdditionalForces = true; else IsHaveAdditionalForces = false; } public void AddModificator() { if (!IsHaveAdditionalForces) return; foreach (var f in forces) { switch (f.TypeOfForce) { case TypeOfForce.Push: AddDirectionForce(f); break; case TypeOfForce.AddSpeed: forceUser.RigidbodyForForce.AddRelativeForce(Vector3.up*f.Force); break; } } } private void AddDirectionForce(SpaceForces spaceForces) { //Debug.Log(""); //var t = AngleDir(forceUser.TransformForForce.position, spaceForces.CenterOfObject); forceUser.RigidbodyForForce.AddForce(Push(spaceForces)); } private Vector3 Push(SpaceForces spaceForces) { var dist = Vector2.Distance(forceUser.Transform.position, spaceForces.CenterOfObject); var coeff = 1 - (spaceForces.ColliderBound / dist); if (forceUser.Transform.position.x > spaceForces.CenterOfObject.x) return (Vector3.right * spaceForces.Force) * coeff; else return (-Vector3.right * spaceForces.Force) * coeff; } public static float AngleDir(Vector2 A, Vector2 B) { return -Ax * By + Ay * Bx; } } public interface IUseForces { Transform Transform { get; } Rigidbody RigidbodyForForce { get; } } public struct SpaceForces { public TypeOfForce TypeOfForce; public Vector3 CenterOfObject; public Vector3 Direction; public float Force; public float ColliderBound; public bool IsAdded; } 


这就是我上面写的-如果您需要进行某种计算/复杂的逻辑,请将其放在单独的类中。 有一个非常简单的逻辑-有一系列作用于火箭的力。 我们对列表进行迭代,查看它的功能并应用特定的方法。 该列表由事件更新,事件在修改器字段中输入/退出时发生。 该系统非常灵活,首先它与一个接口(hi封装)一起工作,修改器的用户不仅可以是火箭/玩家。 其次,一个通用类-您可以使用各种后代来扩展IUseForces以进行需求/实验,并且仍然使用此类/模型。

足够的第一部分。 在第二部分中,我们将考虑事件,依赖注入,输入控制器,火箭类本身的系统,并尝试启动它。

Source: https://habr.com/ru/post/zh-CN453056/


All Articles