在第一部分中,我谈到了为什么进行原型设计以及通常从何处开始
-
第1部分在第二部分中,简要介绍了关键类
和建筑-
第2部分第三部分-实际上,将进行一些讨论,我们将分析修饰符的工作原理,并涉足竞争领域(这并不难,但有细微差别)。 关于体系结构的一些知识,我会尽量避免乏味。
有关该火箭的屏幕截图通常比较枯燥,因此我建议观看另一个原型的视频,该视频与图形一起在2周内组装完毕,由于平台游戏类型无法解决,因此被放弃了。 顺便说一下,这是围绕原型的关键思想之一-组装,看看是否很烂,是否有必要? 如果答案似乎不令人信服,请诚实地投入购物篮。 但是! 这不适用于创意项目-有时为了创意而发生创意)。
因此,vidosik,您需要查看按级别排列的文本,而不是游戏性):
小题外话
在上一篇文章中,我收到了审阅代码,并对此表示感谢;即使您不同意批评,批评也会有所助益。
但是我想针对原型的体系结构和语法进行部署:- 无论您有多酷,都不能仅仅预见到它不会抵押,而且不需要的东西也有很多抵押。 因此,在任何情况下,都必须进行重构或扩展。 如果您无法描述代码/方法的特定好处,则最好不要花大量时间在此代码上。
- 在我看来,为什么原型的OOP /事件模型/组合比ECS,Unity COOP,DI FrameWorks,Reactive Frameworks等更容易。 较少的乱写,所有连接在代码中都是可见的,因为原型的主要任务是回答主要问题-是否可以玩,还有许多次要问题-对于这种或那样的游戏性更好。 因此,您需要尽快实现必要的功能。 为什么要在一个小项目上引入一个框架,规定所有垃圾以实现三个游戏实体? 每个实际上是一类50-100行。 应该将架构视为原型任务的一部分,以及作为可能的alpha扩展的一部分,但是第二个需要比代码更重要的内容,以免在添加代码时费时
关于修饰符:
最后是修饰符本身:在这里和更早的时候,我将修改器称为火箭接触并影响其路径的力场,在原型中,它们有两种类型:加速和偏转。
修饰语类public class PushSideModificator : MonoBehaviour { [SerializeField] TypeOfForce typeOfForce = TypeOfForce.Push; [SerializeField] private float force; [SerializeField] DropPanelConfig dropPanelConfig; private float boundsOfCollider; private void OnTriggerEnter(Collider other) { boundsOfCollider = other.bounds.extents.x; GlobalEventAggregator.EventAggregator.Invoke(new SpaceForces { TypeOfForce = typeOfForce, Force = force, ColliderBound = boundsOfCollider, CenterOfObject = transform.position, IsAdded = true }); } private void OnTriggerExit(Collider other) { GlobalEventAggregator.EventAggregator.Invoke(new SpaceForces { CenterOfObject = transform.position, IsAdded = false }); } }
这是一个非常简单的类,其任务是传递两个事件:
- 玩家进入该区域(实际上是一个物理上的统一触发器),以及所有必要的上下文(修改器的类型,其位置,对撞机的大小,修改器的强度等等)。 在这种情况下,再加上聚合器,他可以将任何上下文传达给感兴趣的各方。 在这种情况下,这是处理修改器的火箭模型。
- 第二事件-玩家离开了场地。 消除其对播放器轨迹的影响
事件模型有什么用? 还有谁可能需要此活动?
在当前项目中,尚未实现,但是:- 语音表演(收到某人进入现场的事件-我们播放相应的声音,某人出去-类似)
- UI标记,例如,对于每个领域,我们都会从火箭中移除一些燃料,应该显示工具提示我们进入该领域并失去燃料,或者为该领域中的每次命中赚取积分,界面上有很多选项让玩家感兴趣领域。
- 特别的 特效-在不同类型的战场上命中时,可以在火箭本身以及火箭/战场周围的空间上叠加不同的特效。 特别的 效果可以由单独的实体/控制器处理,该实体/控制器也将订阅修饰符的事件。
- 好吧,这是最少的代码,不需要服务定位器,聚合,依赖项等。
游戏基础
在此原型中,游戏玩法的实质是在战场上放置修改器,调整火箭的飞行路径,绕过障碍物飞行并击中目的地/行星。 为此,我们在右侧有一个面板,修改器图标位于该面板上。

面板类 [RequireComponent (typeof(CanvasGroup))] public class DragAndDropModifiersPanel : MonoBehaviour { [SerializeField] private DropModifiersIcon iconPrfb; [SerializeField] private DropPanelConfig config; private CanvasGroup canvasGroup; private void Awake() { GlobalEventAggregator.EventAggregator.AddListener<ButtonStartPressed>(this, RocketStarted); canvasGroup = GetComponent<CanvasGroup>(); } private void RocketStarted(ButtonStartPressed obj) { canvasGroup.DOFade(0, 1); (canvasGroup.transform as RectTransform).DOAnchorPosX(100, 1); } private void Start() { for (var x = 0; x< 3; x++) { var mod = config.GetModifierByType(TypeOfForce.Push); var go = Instantiate(iconPrfb, transform); go.Init(mod); } for (var x = 0; x< 1; x++) { var mod = config.GetModifierByType(TypeOfForce.AddSpeed); var go = Instantiate(iconPrfb, transform); go.Init(mod); } } }
预期问题:
for (var x = 0; x< 3; x++) for (var x = 0; x< 1; x++)
3和1-应该简单地避免所谓的魔术数字从头上插入并插入到代码中,但是为什么在这里呢? 形成右面板的原理尚未确定,仅决定在原型上使用如此多的修饰符来测试原型。
怎么做对? -至少将其放在可序列化的字段中,并通过检查器设置所需的数量。 为什么我太懒了,你应该这样做吗? 在这里,我们必须从全局出发,为了形成所需数量的修饰符,仍将负责单独的实体和配置,因此在这里我太懒了,期望将来会进行大量的重构。 但是最好不要偷懒! )
关于配置-关于ScriptableObject的第一堂课出现时,我喜欢将数据存储为资产的想法。 您可以在需要的地方获得必要的数据,而不必绑定到单副本实例。 接下来是关于ScriptableObject的游戏开发方法的讲座,他们用来存储实例设置。 实际上,资产中保存的内容的预设/设置是配置。
考虑配置类:配置类 [CreateAssetMenu(fileName = "DropModifiersPanel", menuName = "Configs/DropModifier", order = 2)] public class DropPanelConfig : ScriptableObject { [SerializeField] private ModifierBluePrintSimple[] modifierBluePrintSimples; public DropModifier GetModifierByType(TypeOfForce typeOfModifiers) { return modifierBluePrintSimples.FirstOrDefault(x => x.GetValue.TypeOfModifier == typeOfModifiers).GetValue; } } [System.Serializable] public class DropModifier { public TypeOfForce TypeOfModifier; public Sprite Icon; public GameObject Modifier; public Material Material; }
他的工作本质是什么? 它存储一个自定义的修饰符数据类。
public class DropModifier { public TypeOfForce TypeOfModifier; public Sprite Icon; public GameObject Modifier; public Material Material; }
识别器需要使用修饰符的类型,界面的图标,修饰符的游戏对象的游戏模式以及此处的材质,以便可以在配置期间进行更改。 修改器可能已经位于运动场上了,比方说,游戏设计师更改了它的类型,现在它提供了加速,配置中的修改器初始化,并根据这种修改器类型更新了包括材料在内的所有字段。
使用配置非常简单-我们转向配置以获取特定类型的数据,然后从该数据中获取该数据,直观的视觉效果以及可能的设置。
利润在哪里?好处是很大的灵活性,例如,如果您想更改加速修改器上的材质和图标,或者说您可以替换整个游戏项目。 无需重写并转发到检查人员的字段,我们只需在一个配置和更改中更改此数据-一切将随我们在所有场景/级别/面板上进行更新。
并且在配置中是否有多个用于加速器修改器的数据?在原型中,您可以轻松地手动对其进行跟踪,以使数据不会重复;在工作草案中,您需要测试和数据验证。
从图标到运动场
修饰符图标类 public class DropModifiersIcon : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler { [SerializeField] private Image icon; [Header(" ")] [SerializeField] private RectTransform canvas; private CanvasGroup canvasGroup; private DropModifier currentModifier; private Vector3 startPoint; private Vector3 outV3; private GameObject currentDraggedObj; private void Start() { canvasGroup = GetComponent<CanvasGroup>(); startPoint = transform.position; canvas = GetComponentInParent<Canvas>().transform as RectTransform; } public void Init(DropModifier dropModifier) { icon.sprite = dropModifier.Icon; currentModifier = dropModifier; } public void OnBeginDrag(PointerEventData eventData) { BlockRaycast(false); currentDraggedObj = Instantiate(currentModifier.Modifier, WorldSpaceCoord(), Quaternion.identity); GlobalEventAggregator.EventAggregator.Invoke(new ImOnDragEvent { IsDragging = true }); } private void BlockRaycast(bool state) { canvasGroup.blocksRaycasts = state; } public void OnDrag(PointerEventData eventData) { Vector2 outV2; RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas, Input.mousePosition, null, out outV2); transform.position = canvas.transform.TransformPoint(outV2); if (currentDraggedObj != null) currentDraggedObj.transform.position = WorldSpaceCoord(); } private Vector3 WorldSpaceCoord() { RectTransformUtility.ScreenPointToWorldPointInRectangle(canvas, Input.mousePosition, Camera.main, out outV3); return outV3; } public void OnEndDrag(PointerEventData eventData) { GlobalEventAggregator.EventAggregator.Invoke(new ImOnDragEvent { IsDragging = false }); if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.layer == 5) { Destroy(currentDraggedObj); transform.SetAsLastSibling(); canvasGroup.blocksRaycasts = true; } else Destroy(gameObject); } } public struct ImOnDragEvent { public bool IsDragging; }
这是怎么回事我们从面板上抓取图标,在面板下创建修改器本身的游戏博客。 实际上我们是在设置单击/独轮车到游戏空间的坐标,因此我们将游戏空间中的修饰符与UI中的图标一起移动,我建议您阅读有关RectTransformUtility的方式,这是一个很好的帮助程序类,其中该界面具有很多功能。
假设我们改变了主意,将修饰符放回面板,
if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.layer == 5)
这段代码使我们可以了解点击后的内容。 为什么还要在此处显示图层检查? 又为什么魔术数字是5? 正如我们在第二部分中所记得的,我们不仅在用户界面上使用rakester图表,还在场景中的按钮上使用rakester图表,如果我们添加了删除字段上已经放置的修改器或移动它们的功能,那么它们也将落入瑞克图表之下,因此还需要检查是否属于UI层。 这是默认层,其顺序不会更改,因此此处的数字5通常不是幻数。
结果,事实证明,如果我们释放面板上方的图标,它将返回面板,如果在比赛场地上方,修饰符仍保留在场地上,则图标将被删除。
原型代码花了1个工作日。 加上文件和图形上的小事。 总的来说,尽管在艺术和游戏设计芯片上有很多疑问,但游戏玩法还是合适的。 任务完成。
结论与建议
- 放置最少的架构,但是架构
- 遵循基本原则,但不要狂热)
- 选择简单的解决方案
- 在多功能性和速度之间-最好为原型选择速度
- 对于大/中型项目,意味着最好从头开始重写项目。 例如,现在Unity中的趋势是DOTS,您必须编写许多组件和系统,这对于短期运行,时间浪费,长期运行都是不利的-当所有组件和系统都注册后,时间就会增加。 我认为花很多时间研究趋势架构并找出原型是什么很酷
成功的原型给所有人。