Hi guys, I am an experienced software developer with over 10 years of experience in business solutions. I have developed and designed complicated systems for automative industry and business intelligence. I ve changed my job and now I have extra 2 hours of free time every day. I thought it would be time to make my dream come true and I dove in to game programming.
After a couple of research on the internet I ve come to the conclusion that unity tutorials are all for beginners and include really bad programming or at all “programming”. I cant seem to wrap around my head how certain things should be implemented in Unity. I want to do things professionally (scalability, maintainability, testability are the things i mean professionally).
My first question would be, how do you design your code in unity ? What are the “must have” components ? How do you make Splash screen, intro screen, main menu, loading screen without stopping the music, and the actual game screen. what i ve found on the internet was awfull. so i thought maybe some of you experienced game developers can show me a way to do all of this professionally. any code example or tutorial would be good.
My second question is more like an example. Let me explain that:
I have a scene where I have a top-down camera, a terrain with static objects and navigation area.
a player with navagent and animations
a click to move script attached to the player (this is where it gets dirty)
this all works fine but as I add things to the project, the code gets dirty. As unity does not have a starting point (main method) i am having problems to understand how should i approach this problem.
Lets say I want to implement a logic for highlighting objects and characters, targeting besides click to move mechanism. Where should all of this code go ? Different game objects ? if yes, wouldnt it get processed alongside click to move ? I have all my current code in Update in click to move script. a raycast and a destination being sent to navagent. As I am trying to do an “RPG”, there are more checks and controls for mouse and keyboard input. e.g. abilities, gui, items, stats and so on. I want to design the code once and focuse on game mechanics and features.
I have thought about finite state machine, but I couldnt really implement it or i was not happy with the result. How would I approach this problem ? See it as a common problem that could apply to all systems. code examples would be amazing.
@eisenpony i have read your posts and you seem to know your “sh!t” would you mind taking a look at my questions please ? thanks
My best suggestion is find tutorials on youtube where someone has gone through several videos to produce some sort of game that may fit what you plan to do. Since you have experience, you should be able to apply your own programming skills to what is presented and if you think something can be done better, improve on it. But this should help you learn at least the Unity side of things.
Unfortunately, you have a large amount of questions and there isn’t just one answer. The good news is you already have the knowledge and should be able to understand anything thrown at you on the coding side.
Brackeys, speedtutor, etc may be a starting point, but I’m guessing on the coding side you’ll find them way to easy. (I haven’t looked into their videos yet, I just know of them).
You just may have better luck working at it, then coming back if you run into a question and asking for help on that. Then once you get that solved, ask another question.
That’s very kind but not really true … If you read my posts you’ll notice a common theme: I give general programming advice, advice about patterns, and advice about specific .net components. What I don’t do is talk about Unity in any meaningful way. That’s because, despite my best intentions, I’ve yet to step foot in it!
That aside, I will do my best to help you, not by answering your specific questions (because I think I would lead you astray), but by sharing some of what I’ve learned about this forum.
First of all, don’t feel badly if you don’t get the response you were looking for. There are a lot of different people on the forum at all different times. You might find asking exactly the same question at a different time of day will get you completely different answers. Secondly, if you’re not happy with the results of a thread, just start a new one from a different angle. Ask your question in a different way, or simplify the problem you are trying to solve. Some people get annoyed if you start too many threads asking exactly the same thing but if you put a little thought into changing your questions and wait a bit of time before reposting, you’ll get a lot of unique perspectives. This, I think, is the most valuable part of the forum so I wouldn’t feel badly about asking, essentially, the same question a few times.
Finally, there are some really bright people patrolling here on a regular basis, but due to the volume of questions they are often hesitant to get into a topic labeled “advanced programming” or “design patterns”. For some reason, these threads tend to attract, and then annoy, a very strongly opinionated group of people. I’ve found the, in my opinion, really useful members typically just avoid them except to root out blatantly incorrect statements. Furthermore, being an abstract idea, patterns lead to a lot of hand waving and interpretive dances. I don’t think it’s impossible to have a meaningful conversation about patterns here, but you will find much more useful information when you can ask extremely specific questions.
This brings me to your second question. This is a pretty good example of a specific problem. I’d suggest you think about how you could describe just this question and start a new thread about it. For instance, title it something like: How do you capture mouse clicks on terrain vs different types of objects?
I don’t spend a lot of time here anymore as I’m focusing on some other projects but if you’re actually interested in what I have to say, I will point out one discussion I had almost exactly a year ago. It is probably my personal favorite contribution because it touches on something I see a lot of misunderstanding about. It also helped me to solidify my own understanding, which is one of the main reasons I started posting here to begin with… Sadly, as with all forum posts, it has been long buried and forgotten.
All that said, I wish you success as you chase your dreams. The truth is, nothing will teach you like trying (and occasionally failing) for yourself. Good luck!
Absolutely program for Unity like you would program for anything else. It’s true that a lot of the example code out there (including Unity’s own) is bad code and ignores basic principles like encapsulation. Unfortunately, a lot of the examples for Unity are written by people who taught themselves off of bad examples, and that whole problem just gets carried on down the line.
Question one is actually at least three questions, and hours of explanations…
Question two, don’t just attach a “click to move” script to the player. I typically implement an InputManager class to intercept all input events and route them appropriately, which I do by having other objects register for state changes on certain virtual buttons/axes. This gives you a starting point for everything that implements input.
Develop classes that do specific things for your game. Classes that handle GUI functionality, etc. Just like you would for any other application you’re developing…
Basically, if you have programming experience, just use that, and ignore all the horrible examples.
If you’re asking about Design Patterns, and come from 10+ years of business solutions… I assume you mean heavy OOP.
So I’ll come at you from that angle.
First and foremost, Unity breaks OOP massively.
It really likes its globals.
So usually my first thing is to create object identity for some things.
Example… its Random class is a static class. There is only one of them, and it can’t be referenced. You can use System.Random instead as well. Personally, because I like to make my code work with both I wrote things like IRandom to give unity’s class some identity.
IRandom:
RandomUtil:
Another is their ‘Time’ class… also static with no identity. If you want some code to run scaled vs unscaled, how can you wrap this into an object? So I created my ITimeSupplier interface.
ITimeSupplier:
Various Time implementations:
Note with it now I can create various time identities. Not just scaled and unscaled. And I can stack time as well.
I can now use them with my animation and tween scripts so that I can tell them which time scale to work on. For example if I have a player given a ‘slow-mo’ power up, I create a ‘slow-mo’ timesupplier and its animation/movement scripts use that instead of the global ‘Time’ class.
Yeah, this was a problem I had when I first got into using 3rd party engines. Most of them don’t have an entry point!
You kinda have to just not think in that mindset.
I like to think of it like multi-tasking (not multi-threading, because it’s all on one thread). Each ‘Component’ (monobehaviour) is its own entry point into a multi-tasked system. Your mini-entry point is the ‘Awake’/‘Enable’/‘Start’ methods (oh dear god, mess around with these, you NEED to learn their mannerisms and order to one another… it is soooo weird).
From there you can have several classes that are not components that can branch out from there if you need.
And of course if you need that one script that’s the first thing ever. I usually create a ‘GameStartup’ script, I order as first (-32000) in the execution order, and put it in my ‘GameLoadScene’ which is the first scene loaded when the game starts.
I also create a ‘Debug’ script that uses the compiler symbol ‘UNITY_EDITOR’ to call on GameStartup behaviour for scenes that are loaded at editor time for debugging rather than in the proper order once built. It is of course timed to be just after the ‘GameStartup’.
That’s a lot of stuff there. I’d first start organizing them into their related groups.
I like to go with an ‘entity’ approach. Stuff that relates to the player, that’s my ‘Player’ entity. Stuff related to UI, that’s my ‘UI’ entity. So on, so forth.
OK, I’ll give you an example, I’m putting it behind spoiler tags cause there’s a lot there to read.
Over this last weekend my buddy and I did ‘ludumdare 37’ (a weekend long gamejam where you can 72 hours to make a game). We made a survival horror clone “Murder Mansion”: ludumdare.com/compo data is offline | Ludum Dare
Nothing to big or special, can be beat in all of 5 minutes.
Anyways, I’ll show you my design for it (note I work with an artist, the scene is a bit messy).
Note I give it a root gameobject, then inside several GameObjects to perform various jobs.
PlayerF - its base
Audio - artist put this here, it has audiosources that are played on events, like the hit soundfx
Aspect - this has a single ‘VisualAspect’ component which can be found by ‘VisualSensor’ scripts I use
Events (and children) - artist again, these are complicated event chains that he creates with a visual programming tool I created him that we call our ‘T&I’ system. It’s like UnityEvent, but different, as well as predates it… I probably would have extended that if it existed, but alas… timing. See the classes for it.
Hitbox - a trigger collider for determining hit area… this could be placed wherever, we just put it here.
Rig - this is the actual model, all of its bones are underneath.
AnimData - this is where my Animator classes go, it references the model of course.
Motor - this is all my player logic. Movement script, so on so forth.
DeathCam & AreaLight & GameObject - more artist crap slapped in willy nilly… he gets sloppy
MultiTag - just lets multiple tags on an GameObject
CharacterController - for movement
MovementController - this wraps around CharacterController (or Rigidbody) to give a generalized interface no matter if you use CharacterController or Rigidbody for movement.
HealthMeter - eh, I put it here… not sure why
using UnityEngine;
using System.Collections.Generic;
using com.spacepuppy;
using com.spacepuppy.Anim;
using com.spacepuppy.Collections;
using com.spacepuppy.Scenario;
using com.spacepuppy.Utils;
namespace com.mansion.Entities.Actors.Player
{
public class PlayerAnimator : SPComponent, IEntityAwakeHandler
{
#region Anim Constants
public const int ANIMLAYER_DEFAULT = 0;
public const int ANIMLAYER_WALK = 5;
public const int ANIMLAYER_ACTION = 10;
public const int ANIMLAYER_DEATH = 100;
#endregion
#region Fields
[SerializeField()]
[DefaultFromSelf(UseEntity = true)]
private SPAnimationController _controller;
[SerializeField()]
private DefaultMovementAnimationInfo _defaultMovementAnimations;
[SerializeField()]
private UndeadMovementAnimationInfo _undeadAnimations;
[SerializeField()]
private RangeWeaponAnimationInfo _rangeWeaponAnimations;
[SerializeField()]
private StruckAnimationInfo _struckAnimations;
[Header("Event Triggers")]
[SerializeField()]
private Trigger _onFireWeapon;
[SerializeField()]
private Trigger _onReloadWeapon;
[SerializeField()]
private Trigger _onStruck;
[SerializeField()]
private Trigger _onDeath;
[SerializeField()]
private Trigger _onRebirth;
[SerializeField()]
private Trigger _onUndeadAttack;
[System.NonSerialized()]
private IEntity _entity;
#endregion
#region CONSTRUCTOR
protected override void Awake()
{
base.Awake();
var entity = SPEntity.Pool.GetFromSource<IEntity>(this);
if (entity != null && entity.IsAwake) this.OnAwake(entity);
}
void IEntityAwakeHandler.OnEntityAwake(SPEntity entity)
{
this.OnAwake(entity as IEntity);
}
private void OnAwake(IEntity entity)
{
_entity = entity;
this.InitAnims();
}
#endregion
#region Properties
public IEntity Entity
{
get { return _entity; }
}
public SPAnimationController Controller
{
get { return _controller; }
}
public DefaultMovementAnimationInfo DefaultMovementAnimations
{
get { return _defaultMovementAnimations; }
}
public UndeadMovementAnimationInfo UndeadAnimations
{
get { return _undeadAnimations; }
}
public RangeWeaponAnimationInfo RangeWeaponAnimations
{
get { return _rangeWeaponAnimations; }
}
public StruckAnimationInfo StruckAnimations
{
get { return _struckAnimations; }
}
#endregion
#region Methods
private void InitAnims()
{
_defaultMovementAnimations.Init(this);
_undeadAnimations.Init(this);
_rangeWeaponAnimations.Init(this);
_struckAnimations.Init(this);
}
#endregion
#region Special Types
[System.Serializable()]
public class DefaultMovementAnimationInfo
{
public const string ANIM_MOVE_IDLE = "Idle";
public const string ANIM_MOVE_IDLEACTION = "IdleAction";
public const string ANIM_MOVE_WALK = "Walk";
public const string ANIM_MOVE_STRAFE_F = "StrafeForward";
public const string ANIM_MOVE_STRAFE_B = "StrafeBackward";
public const string ANIM_MOVE_STRAFE_L = "StrafeLeft";
public const string ANIM_MOVE_STRAFE_R = "StrafeRight";
public const string ANIM_MOVE_IDLE_CRIT = "Idle_Crit";
public const string ANIM_MOVE_IDLEACTION_CRIT = "IdleAction_Crit";
public const string ANIM_MOVE_WALK_CRIT = "Walk_Crit";
public enum DefaultMovementState
{
IdleAction = -1,
Idle = 0,
Walk = 1
}
#region Fields
[SerializeField()]
private float _moveAnimSpeedRatio = 1;
[SerializeField()]
private float _critMoveAnimSpeedRatio = 1;
[SerializeField()]
[SPAnimClipCollection.Config(DefaultLayer = PlayerAnimator.ANIMLAYER_DEFAULT)]
[SPAnimClipCollection.StaticCollection(ANIM_MOVE_IDLE,
ANIM_MOVE_IDLEACTION,
ANIM_MOVE_WALK,
ANIM_MOVE_STRAFE_F,
ANIM_MOVE_STRAFE_B,
ANIM_MOVE_STRAFE_L,
ANIM_MOVE_STRAFE_R,
ANIM_MOVE_IDLE_CRIT,
ANIM_MOVE_IDLEACTION_CRIT,
ANIM_MOVE_WALK_CRIT)]
private SPAnimClipCollection _animations;
[System.NonSerialized()]
private PlayerAnimator _owner;
[System.NonSerialized()]
private DefaultMovementState _state;
[System.NonSerialized()]
private SPAnimClip _currentWalk;
#endregion
#region CONSTRUCTOR
#endregion
#region Properties
public DefaultMovementState State { get { return _state; } }
#endregion
#region Methods
internal void Init(PlayerAnimator owner)
{
_owner = owner;
_animations.Init(_owner.Controller, "*move");
this.PlayIdle();
}
public SPAnim Play(string id, QueueMode queuMode = QueueMode.PlayNow, PlayMode playMode = PlayMode.StopSameLayer)
{
return _animations.Play(id, queuMode, playMode);
}
public void PlayIdle()
{
if (_currentWalk != null)
{
_currentWalk.Stop();
_currentWalk = null;
}
SPAnimClip clip;
if (_owner._entity.HealthMeter.Status == HealthMeter.StatusType.Critical)
clip = _animations[ANIM_MOVE_IDLE_CRIT];
else
clip = _animations[ANIM_MOVE_IDLE];
if (clip == null) return;
clip.Layer = ANIMLAYER_DEFAULT;
clip.CrossFadeDirectly(Constants.DEFAULT_CROSSFADE_DUR);
_state = DefaultMovementState.Idle;
}
public void PlayIdleAction()
{
SPAnimClip clip;
if (_owner._entity.HealthMeter.Status == HealthMeter.StatusType.Critical)
clip = _animations[ANIM_MOVE_IDLEACTION_CRIT];
else
clip = _animations[ANIM_MOVE_IDLEACTION];
if (clip == null) return;
_state = DefaultMovementState.IdleAction;
clip.WrapMode = WrapMode.Once;
clip.Layer = ANIMLAYER_DEFAULT + 1;
clip.CrossFade(Constants.DEFAULT_CROSSFADE_DUR).Schedule((a) =>
{
if (_state == DefaultMovementState.IdleAction)
{
_state = DefaultMovementState.Idle;
this.PlayIdle();
}
});
}
public void PlayWalk(float spd = 1f)
{
SPAnimClip clip;
float ratio;
if (_owner._entity.HealthMeter.Status == HealthMeter.StatusType.Critical)
{
clip = _animations[ANIM_MOVE_WALK_CRIT];
ratio = _critMoveAnimSpeedRatio;
}
else
{
clip = _animations[ANIM_MOVE_WALK];
ratio = _moveAnimSpeedRatio;
}
if (clip == null) return;
clip.WrapMode = WrapMode.Loop;
clip.Speed = spd * ratio;
clip.Layer = ANIMLAYER_WALK;
clip.CrossFadeDirectly(Constants.DEFAULT_CROSSFADE_DUR);
_currentWalk = clip;
_state = DefaultMovementState.Walk;
}
public void PlayStrafe(float offAngle, float spd = 1f)
{
SPAnimClip clip;
if (Mathf.Abs(offAngle) >= 135f)
clip = _animations[ANIM_MOVE_STRAFE_B];
else if (offAngle < -45f)
clip = _animations[ANIM_MOVE_STRAFE_L];
else if (offAngle > 45f)
clip = _animations[ANIM_MOVE_STRAFE_R];
else
clip = _animations[ANIM_MOVE_STRAFE_F];
if (clip == null) return;
clip.WrapMode = WrapMode.Loop;
clip.Speed = spd * _moveAnimSpeedRatio;
clip.Layer = ANIMLAYER_WALK;
clip.CrossFadeDirectly(Constants.DEFAULT_CROSSFADE_DUR);
_currentWalk = clip;
_state = DefaultMovementState.Walk;
}
#endregion
}
[System.Serializable()]
public class UndeadMovementAnimationInfo
{
public const string ANIM_UNDEAD_IDLE = "Idle";
public const string ANIM_UNDEAD_IDLEACTION = "IdleAction";
public const string ANIM_UNDEAD_WALK = "Walk";
public const string ANIM_UNDEAD_MELEE = "Melee";
public enum DefaultMovementState
{
IdleAction = -1,
Idle = 0,
Walk = 1
}
#region Fields
[SerializeField()]
private float _moveAnimSpeedRatio = 1;
[SerializeField()]
[SPAnimClipCollection.Config(DefaultLayer = PlayerAnimator.ANIMLAYER_DEFAULT)]
[SPAnimClipCollection.StaticCollection(ANIM_UNDEAD_IDLE,
ANIM_UNDEAD_IDLEACTION,
ANIM_UNDEAD_WALK,
ANIM_UNDEAD_MELEE)]
private SPAnimClipCollection _animations;
[System.NonSerialized()]
private PlayerAnimator _owner;
[System.NonSerialized()]
private DefaultMovementState _state;
[System.NonSerialized()]
private SPAnimClip _currentWalk;
#endregion
#region CONSTRUCTOR
#endregion
#region Properties
public DefaultMovementState State { get { return _state; } }
#endregion
#region Methods
internal void Init(PlayerAnimator owner)
{
_owner = owner;
_animations.Init(_owner.Controller, "*undead");
this.PlayIdle();
}
public SPAnim Play(string id, QueueMode queuMode = QueueMode.PlayNow, PlayMode playMode = PlayMode.StopSameLayer)
{
return _animations.Play(id, queuMode, playMode);
}
public void PlayIdle()
{
if (_currentWalk != null)
{
_currentWalk.Stop();
_currentWalk = null;
}
var clip = _animations[ANIM_UNDEAD_IDLE];
if (clip == null) return;
clip.Layer = ANIMLAYER_DEFAULT;
clip.CrossFadeDirectly(Constants.DEFAULT_CROSSFADE_DUR);
_state = DefaultMovementState.Idle;
}
public void PlayIdleAction()
{
var clip = _animations[ANIM_UNDEAD_IDLEACTION];
if (clip == null) return;
_state = DefaultMovementState.IdleAction;
clip.WrapMode = WrapMode.Once;
clip.Layer = ANIMLAYER_DEFAULT + 1;
clip.CrossFade(Constants.DEFAULT_CROSSFADE_DUR).Schedule((a) =>
{
if (_state == DefaultMovementState.IdleAction)
{
_state = DefaultMovementState.Idle;
this.PlayIdle();
}
});
}
public void PlayWalk(float spd = 1f)
{
var clip = _animations[ANIM_UNDEAD_WALK];
if (clip == null) return;
clip.WrapMode = WrapMode.Loop;
clip.Speed = spd * _moveAnimSpeedRatio;
clip.Layer = ANIMLAYER_WALK;
clip.CrossFadeDirectly(Constants.DEFAULT_CROSSFADE_DUR);
_currentWalk = clip;
_state = DefaultMovementState.Walk;
}
public IRadicalWaitHandle PlayMelee()
{
var clip = _animations[ANIM_UNDEAD_MELEE];
if (clip == null) return RadicalWaitHandle.Null;
_owner._onUndeadAttack.ActivateTrigger();
clip.WrapMode = WrapMode.Once;
clip.Layer = ANIMLAYER_ACTION;
var anim = clip.CrossFade(Constants.DEFAULT_CROSSFADE_DUR, QueueMode.PlayNow);
return anim;
}
#endregion
}
[System.Serializable()]
public class RangeWeaponAnimationInfo
{
public const string ANIM_RANGE_DRAW = "Draw";
public const string ANIM_RANGE_AIM = "Aim";
public const string ANIM_RANGE_HOLSTER = "Holster";
public const string ANIM_RANGE_FIRE = "Fire";
public const string ANIM_RANGE_RELOAD = "Reload";
public enum RangeWeaponState
{
Holstering = -1,
None = 0,
Drawing = 1,
Aim = 2,
Firing = 3,
Reload = 4
}
#region Fields
[SerializeField()]
[SPAnimClipCollection.Config(DefaultLayer = PlayerAnimator.ANIMLAYER_ACTION)]
[SPAnimClipCollection.StaticCollection(ANIM_RANGE_DRAW,
ANIM_RANGE_AIM,
ANIM_RANGE_HOLSTER,
ANIM_RANGE_FIRE,
ANIM_RANGE_RELOAD)]
private SPAnimClipCollection _animations;
[System.NonSerialized()]
private PlayerAnimator _owner;
[System.NonSerialized()]
private RangeWeaponState _state;
[System.NonSerialized()]
private SPAnim _drawAnim;
#endregion
#region CONSTRUCTOR
#endregion
#region Properties
public RangeWeaponState State
{
get { return _state; }
}
#endregion
#region Methods
internal void Init(PlayerAnimator owner)
{
_owner = owner;
_animations.Init(_owner.Controller, "*range");
}
public SPAnim Play(string id, QueueMode queuMode = QueueMode.PlayNow, PlayMode playMode = PlayMode.StopSameLayer)
{
return _animations.Play(id, queuMode, playMode);
}
public bool DrawWeapon()
{
switch (_state)
{
case RangeWeaponState.Holstering:
case RangeWeaponState.None:
{
var clip1 = _animations[ANIM_RANGE_DRAW];
var clip2 = _animations[ANIM_RANGE_AIM];
if (clip1 == null || clip2 == null) return false;
_state = RangeWeaponState.Drawing;
clip1.WrapMode = WrapMode.Clamp;
clip1.Layer = ANIMLAYER_ACTION;
_drawAnim = clip1.Play(QueueMode.PlayNow);
_drawAnim.Schedule((a) =>
{
_drawAnim = null;
_state = RangeWeaponState.Aim;
});
clip2.WrapMode = WrapMode.Loop;
clip2.Layer = ANIMLAYER_ACTION;
clip2.Play(QueueMode.CompleteOthers);
return true;
}
case RangeWeaponState.Drawing:
case RangeWeaponState.Aim:
case RangeWeaponState.Firing:
case RangeWeaponState.Reload:
default:
return false;
}
}
public bool HolsterWeapon()
{
switch(_state)
{
case RangeWeaponState.Holstering:
case RangeWeaponState.None:
return false;
case RangeWeaponState.Drawing:
{
var clip = _animations[ANIM_RANGE_DRAW];
_state = RangeWeaponState.Holstering;
clip.WrapMode = WrapMode.Clamp;
clip.Layer = ANIMLAYER_ACTION;
clip.Time = _drawAnim.Time;
var anim = clip.Play(QueueMode.PlayNow);
anim.Speed = -1f;
anim.Schedule((a) =>
{
_state = RangeWeaponState.None;
});
_drawAnim = null;
return true;
}
case RangeWeaponState.Aim:
{
var clip = _animations[ANIM_RANGE_HOLSTER];
if (clip == null) return false;
_state = RangeWeaponState.Holstering;
clip.WrapMode = WrapMode.Once;
clip.Layer = ANIMLAYER_ACTION;
clip.CrossFade(Constants.DEFAULT_CROSSFADE_DUR, QueueMode.PlayNow).Schedule((a) =>
{
_state = RangeWeaponState.None;
});
return true;
}
case RangeWeaponState.Firing:
case RangeWeaponState.Reload:
default:
return false;
}
}
public bool FireWeapon()
{
switch (_state)
{
case RangeWeaponState.Holstering:
case RangeWeaponState.None:
case RangeWeaponState.Drawing:
return false;
case RangeWeaponState.Aim:
{
var clip = _animations[ANIM_RANGE_FIRE];
if (clip == null) return false;
_state = RangeWeaponState.Firing;
clip.WrapMode = WrapMode.Once;
clip.Layer = ANIMLAYER_ACTION + 1;
clip.Play(QueueMode.CompleteOthers).Schedule((a) =>
{
_state = RangeWeaponState.Aim;
});
_owner._onFireWeapon.ActivateTrigger();
return true;
}
case RangeWeaponState.Firing:
case RangeWeaponState.Reload:
default:
return false;
}
}
public bool ReloadWeapon()
{
switch (_state)
{
case RangeWeaponState.Holstering:
case RangeWeaponState.None:
case RangeWeaponState.Drawing:
return false;
case RangeWeaponState.Aim:
{
var clip = _animations[ANIM_RANGE_RELOAD];
if (clip == null) return false;
_state = RangeWeaponState.Reload;
clip.WrapMode = WrapMode.Once;
clip.Layer = ANIMLAYER_ACTION + 1;
clip.Play(QueueMode.CompleteOthers).Schedule((a) =>
{
_state = RangeWeaponState.Aim;
});
_owner._onReloadWeapon.ActivateTrigger();
return true;
}
case RangeWeaponState.Firing:
case RangeWeaponState.Reload:
default:
return false;
}
}
#endregion
}
[System.Serializable()]
public class StruckAnimationInfo
{
public const string ANIM_STRUCK_DEATH = "Death";
public const string ANIM_STRUCK_REBIRTH = "Rebirth";
#region Fields
[SerializeField()]
[SPAnimClipCollection.Config(DefaultLayer = PlayerAnimator.ANIMLAYER_ACTION)]
private SPAnimClipCollection _struckAnimations;
[SerializeField()]
[SPAnimClipCollection.Config(DefaultLayer = PlayerAnimator.ANIMLAYER_DEATH)]
[SPAnimClipCollection.StaticCollection(ANIM_STRUCK_DEATH,
ANIM_STRUCK_REBIRTH)]
private SPAnimClipCollection _deathAnimations;
[System.NonSerialized()]
private PlayerAnimator _owner;
#endregion
#region Methods
internal void Init(PlayerAnimator owner)
{
_owner = owner;
_struckAnimations.Init(_owner.Controller, "*struck");
_deathAnimations.Init(_owner.Controller, "*death");
}
public SPAnim PlayStruck()
{
var clip = _struckAnimations.PickRandom();
if (clip == null) return null;
_owner._onStruck.ActivateTrigger();
return clip.Play();
}
public SPAnim PlayDeath()
{
var clip = _deathAnimations[ANIM_STRUCK_DEATH];
if (clip == null) return null;
_owner._onDeath.ActivateTrigger();
clip.Layer = PlayerAnimator.ANIMLAYER_DEATH;
clip.WrapMode = WrapMode.ClampForever;
return clip.CrossFade(0.5f);
}
public SPAnim PlayRebirth()
{
var clip = _deathAnimations[ANIM_STRUCK_REBIRTH];
if (clip == null) return null;
_owner._onRebirth.ActivateTrigger();
clip.Layer = PlayerAnimator.ANIMLAYER_DEATH;
return clip.CrossFade(1f);
}
#endregion
}
#endregion
}
}
Note it references the ‘Rig’ so it can actually play the animations.
It also has Event Triggers at the bottom. This is that ‘com.spacepuppy.Scenario’ stuff I linked earlier. My artist can hook into those events (the Events gameobject) and perform actions when they occur. As you can see here, the ‘OnDeath’ event references the ‘e.Death’ object. That object plays some soundfx, and prints some stuff to screen, and other fun stuff for on death.
This is the true guts of the player. The rest of that stuff is to look pretty, this is the gameplay here.
MovementMotor is a finite state machine, I can have various ‘MovementStyles’ that can be used. In this case I have ‘PlayeWalkMovementStyle’ and ‘PlayerUndeadWalkMovementStyle’ (when you die, you come back to life as a zombie).
Here is PlayerWalkMovementStyle:
using UnityEngine;
using System.Collections.Generic;
using com.spacepuppy;
using com.spacepuppy.Cameras;
using com.spacepuppy.Movement;
using com.spacepuppy.UserInput;
using com.spacepuppy.Utils;
using com.mansion.Entities.Cameras;
using com.mansion.Entities.UI;
using com.mansion.UserInput;
namespace com.mansion.Entities.Actors.Player
{
public class PlayerWalkMovementStyle : SPComponent, IMovementStyle
{
#region Fields
[SerializeField()]
private float _speed = 1f;
[SerializeField()]
private float _runSpeed = 2f;
[SerializeField()]
[Range(0f,1f)]
private float _turnSlerpRatio = 0.5f;
[Range(0f, 1f)]
private float _aimTurnSlerpRatio = 0.5f;
[SerializeField()]
[Range(0f, 1f)]
private float _injuredSpeedDamper = 0.75f;
[SerializeField()]
[Range(0f, 1f)]
private float _criticalSpeedDamper = 0.75f;
[SerializeField()]
[DefaultFromSelf(UseEntity = true)]
private PlayerAnimator _animator;
[System.NonSerialized()]
private IEntity _entity;
[System.NonSerialized()]
private MovementMotor _motor;
#endregion
#region CONSTRUCTOR
protected override void Awake()
{
base.Awake();
_entity = SPEntity.Pool.GetFromSource<IEntity>(this);
_motor = this.GetComponent<MovementMotor>();
}
#endregion
#region Properties
public float Speed
{
get { return _speed; }
set { _speed = value; }
}
public float RunSpeed
{
get { return _runSpeed; }
set { _runSpeed = value; }
}
public float TurnSlerpRatio
{
get { return _turnSlerpRatio; }
set { _turnSlerpRatio = value; }
}
public float AimTurnSlerpRatio
{
get { return _aimTurnSlerpRatio; }
set { _aimTurnSlerpRatio = value; }
}
#endregion
#region Methods
private Vector3 GetCurrentCameraForward()
{
var cam = CameraManager.Main;
var forw = cam.transform.forward.SetY(0f).normalized;
if(CameraZone.LastCamera != null && CameraZone.LastCamera != cam)
{
const float LERP_TIME = 0.5f;
var t = Time.time - CameraZone.LastCamerSwapTime;
if(t < LERP_TIME)
{
var oldForw = CameraZone.LastCamera.transform.forward.SetY(0f).normalized;
forw = Vector3.Slerp(oldForw, forw, t / LERP_TIME);
}
}
return forw;
}
#endregion
#region IMovementStyle Interface
void IMovementStyle.OnActivate(IMovementStyle lastStyle, bool stateIsStacking)
{
}
void IMovementStyle.OnDeactivate(IMovementStyle nextStyle, bool stateIsStacking)
{
}
void IMovementStyle.OnPurgedFromStack()
{
}
void IMovementStyle.UpdateMovement()
{
if (Game.Paused) return;
if (_entity.Stalled || InGameUIController.Instance.MessageBox.IsShowing || _entity.HealthMeter.Health == 0f)
{
//idle
_motor.Controller.Move(Vector3.up * Game.GRAV * Time.deltaTime);
_animator.DefaultMovementAnimations.PlayIdle();
return;
}
var input = Game.InputManager.GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
if (input == null) return;
var forw = this.GetCurrentCameraForward();
var right = Vector3.Cross(Vector3.up, forw);
var dir = input.GetCurrentDualAxleState(MansionInputs.Move);
var strength = dir.magnitude;
float speed = 0f;
if (strength < 0.1f)
speed = 0f;
else if (strength < 0.6f)
speed = 0.5f * _speed;
else if (input.GetCurrentButtonState(MansionInputs.Run) <= ButtonState.None)
speed = _speed;
else
speed = _runSpeed;
switch(_entity.HealthMeter.Status)
{
case HealthMeter.StatusType.Injured:
speed *= _injuredSpeedDamper;
break;
case HealthMeter.StatusType.Critical:
speed *= _criticalSpeedDamper;
break;
}
dir.Normalize();
var walk = forw * dir.y + right * dir.x;
var mv = walk * speed + Vector3.up * Game.GRAV;
_motor.Controller.Move(mv * Time.deltaTime);
bool isAiming = (_animator.RangeWeaponAnimations.State > PlayerAnimator.RangeWeaponAnimationInfo.RangeWeaponState.None);
if (speed > 0f)
{
if(isAiming)
{
var a = VectorUtil.AngleOffAroundAxis(walk, _motor.Controller.transform.forward.SetY(0f), Vector3.up);
_animator.DefaultMovementAnimations.PlayStrafe(a, speed);
}
else
{
_motor.Controller.transform.rotation = Quaternion.Slerp(_motor.Controller.transform.rotation,
Quaternion.LookRotation(walk, Vector3.up),
_turnSlerpRatio);
_animator.DefaultMovementAnimations.PlayWalk(speed);
}
}
else
{
_animator.DefaultMovementAnimations.PlayIdle();
}
}
void IMovementStyle.OnUpdateMovementComplete()
{
}
#endregion
}
}
It’s a fairly simple movement script for player motion around the game world.
And lastly, PlayerActionMotor:
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using com.spacepuppy;
using com.spacepuppy.AI.Sensors;
using com.spacepuppy.Cameras;
using com.spacepuppy.Movement;
using com.spacepuppy.UserInput;
using com.spacepuppy.Utils;
using com.mansion.Entities.GamePlay;
using com.mansion.Entities.Weapons;
using com.mansion.Entities.UI;
using com.mansion.UserInput;
namespace com.mansion.Entities.Actors.Player
{
public class PlayerActionMotor : SPComponent
{
#region Fields
[SerializeField()]
private Sensor _actionSensor;
[SerializeField()]
private Sensor _lockOnSensor;
[SerializeField()]
private GunWeapon _gun;
[SerializeField()]
private GameObject _undeadAttackWeapon;
[SerializeField()]
[DefaultFromSelf(UseEntity = true)]
private PlayerAnimator _animator;
[SerializeField()]
private float _onStruckStallDuration = 1f;
[System.NonSerialized()]
private IEntity _entity;
[System.NonSerialized()]
private PlayerWalkMovementStyle _walkMotor;
[System.NonSerialized()]
private float _idleActionTicker;
[System.NonSerialized()]
private float _idleActionTimeout;
[System.NonSerialized()]
private IEntity _lockedOnEntity;
[System.NonSerialized()]
private bool _inUndeadAttack;
#endregion
#region CONSTRUCTOR
protected override void Awake()
{
base.Awake();
_entity = SPEntity.Pool.GetFromSource<IEntity>(this);
_walkMotor = this.GetComponent<PlayerWalkMovementStyle>();
_undeadAttackWeapon.SetActive(false);
_entity.HealthMeter.OnStrike.TriggerActivated += this.OnStruck;
_entity.HealthMeter.OnDeath.TriggerActivated += this.OnDeath;
}
protected override void Start()
{
base.Start();
this.ResetIdleActionTicker();
}
#endregion
#region Properties
public Sensor ActionSensor
{
get { return _actionSensor; }
set { _actionSensor = value; }
}
public Sensor LockOnSensor
{
get { return _lockOnSensor; }
set { _lockOnSensor = value; }
}
public GunWeapon Gun
{
get { return _gun; }
set { _gun = value; }
}
public PlayerAnimator Animator
{
get { return _animator; }
set { _animator = value; }
}
public float OnStruckStallDuration
{
get { return _onStruckStallDuration; }
set { _onStruckStallDuration = value; }
}
#endregion
#region Methods
protected void Update()
{
var input = Game.InputManager.GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
if (input == null) return;
if(Game.Paused || _entity.Stalled || InGameUIController.Instance.MessageBox.IsShowing || _entity.HealthMeter.Health == 0f)
{
//do nothing
}
else if(_entity.Type == IEntity.EntityType.UndeadPlayer)
{
if (input.GetCurrentButtonState(MansionInputs.Action) == ButtonState.Down)
{
if (input.GetCurrentButtonState(MansionInputs.Aim) > ButtonState.None)
{
//do attack
if(!_inUndeadAttack) this.StartCoroutine(this.DoUndeadMeleeAttack());
}
else
{
//attempt to activate whatever
this.AttemptActivate();
_idleActionTicker = 0f;
}
}
if (!_entity.Stalled &&
_animator.DefaultMovementAnimations.State == PlayerAnimator.DefaultMovementAnimationInfo.DefaultMovementState.Idle)
{
_idleActionTicker += Time.deltaTime;
if (_idleActionTicker >= _idleActionTimeout)
{
_animator.UndeadAnimations.PlayIdleAction();
this.ResetIdleActionTicker();
}
}
else
{
_idleActionTicker = 0f;
}
//hack fix - make sure gun is always holstered
if(_animator.RangeWeaponAnimations.State > PlayerAnimator.RangeWeaponAnimationInfo.RangeWeaponState.None)
{
_animator.RangeWeaponAnimations.HolsterWeapon();
}
}
else if(_animator.RangeWeaponAnimations.State > PlayerAnimator.RangeWeaponAnimationInfo.RangeWeaponState.None)
{
if(input.GetCurrentButtonState(MansionInputs.Aim) <= ButtonState.None)
{
_lockedOnEntity = null;
_animator.RangeWeaponAnimations.HolsterWeapon();
}
else if(input.GetCurrentButtonState(MansionInputs.Action) == ButtonState.Down)
{
if(_gun.AmmoInClip <= 0)
{
if (_animator.RangeWeaponAnimations.ReloadWeapon())
_gun.Reload();
}
else if(_animator.RangeWeaponAnimations.FireWeapon())
{
if(_gun.Fire(_entity.transform.forward.SetY(0f)))
{
_lockedOnEntity = null;
}
}
}
else if (input.GetCurrentButtonState(MansionInputs.Reload) == ButtonState.Down)
{
if (_animator.RangeWeaponAnimations.ReloadWeapon())
_gun.Reload();
}
if (_lockedOnEntity == null)
{
this.AttemptFindTarget();
}
else
{
var dir = (_lockedOnEntity.transform.position - _entity.transform.position).SetY(0f);
_entity.transform.rotation = Quaternion.Slerp(_entity.transform.rotation,
Quaternion.LookRotation(dir, Vector3.up),
_walkMotor.AimTurnSlerpRatio);
}
_idleActionTicker = 0f;
}
else
{
if(input.GetCurrentButtonState(MansionInputs.Aim) > ButtonState.None)
{
//draw weapon
_animator.RangeWeaponAnimations.DrawWeapon();
this.ResetIdleActionTicker();
}
else if (input.GetCurrentButtonState(MansionInputs.Action) == ButtonState.Down)
{
//attempt to activate whatever
this.AttemptActivate();
_idleActionTicker = 0f;
}
else if (input.GetCurrentButtonState(MansionInputs.Reload) == ButtonState.Down)
{
//reload weapon
if (_animator.RangeWeaponAnimations.ReloadWeapon())
{
if (_animator.RangeWeaponAnimations.ReloadWeapon())
_gun.Reload();
}
_idleActionTicker = 0f;
}
if (!_entity.Stalled &&
_animator.DefaultMovementAnimations.State == PlayerAnimator.DefaultMovementAnimationInfo.DefaultMovementState.Idle)
{
_idleActionTicker += Time.deltaTime;
if (_idleActionTicker >= _idleActionTimeout)
{
_animator.DefaultMovementAnimations.PlayIdleAction();
this.ResetIdleActionTicker();
}
}
else
{
_idleActionTicker = 0f;
}
}
if (input.GetCurrentButtonState(MansionInputs.Menu) == ButtonState.Down)
{
//pause game
Game.TogglePause();
}
}
private void AttemptActivate()
{
var trans = _entity.transform;
var pos = trans.position;
var forw = trans.forward.SetY(0f);
var aspect = (from a in _actionSensor.SenseAll()
where a.gameObject.EntityHasComponent<IEntity>()
let p = a.transform.position.SetY(0f)
orderby VectorUtil.AngleBetween(p, forw), Vector3.Distance(p, pos) ascending
select a).FirstOrDefault();
if (aspect != null)
{
var entity = SPEntity.Pool.GetFromSource(aspect);
if (entity.EntityHasComponent<PlayerInteractable>())
{
var comp = entity.FindComponent<PlayerInteractable>();
comp.Trigger();
}
}
}
private void ResetIdleActionTicker()
{
_idleActionTicker = 0f;
_idleActionTimeout = RandomUtil.Standard.Range(15f, 10f);
}
private void AttemptFindTarget()
{
var forw = _entity.transform.forward.SetY(0f);
var pos = _entity.transform.position.SetY(0f);
var aspect = (from a in _lockOnSensor.SenseAll()
let e = SPEntity.Pool.GetFromSource<IEntity>(a)
where e != null && e.Type == IEntity.EntityType.Mob && e.HealthMeter != null && e.HealthMeter.Health > 0f
let p = a.transform.position.SetY(0f)
orderby Vector3.Distance(p, pos), VectorUtil.AngleBetween(p, forw) ascending
select a).FirstOrDefault();
if(aspect != null)
{
_lockedOnEntity = SPEntity.Pool.GetFromSource<IEntity>(aspect);
}
}
private System.Collections.IEnumerator DoUndeadMeleeAttack()
{
_inUndeadAttack = true;
_entity.Stalled = true;
var a = _animator.UndeadAnimations.PlayMelee();
yield return WaitForDuration.Seconds(0.5f);
_undeadAttackWeapon.SetActive(true);
yield return WaitForDuration.Seconds(0.65f);
_undeadAttackWeapon.SetActive(false);
yield return a;
_entity.Stalled = false;
_inUndeadAttack = false;
}
#endregion
#region Event Handlers
private void OnStruck(object sender, TempEventArgs e)
{
this.StartRadicalCoroutine(this.PlayStruckRoutine());
}
private void OnDeath(object sender, TempEventArgs e)
{
//if normal mode
this.StartRadicalCoroutine(this.PlayNormalDeathRoutine());
//todo - if zombie
}
private System.Collections.IEnumerator PlayStruckRoutine()
{
_idleActionTicker = 0f;
_animator.StruckAnimations.PlayStruck();
_entity.Stalled = true;
yield return WaitForDuration.Seconds(_onStruckStallDuration);
_entity.Stalled = false;
}
private System.Collections.IEnumerator PlayNormalDeathRoutine()
{
_entity.Stalled = true;
_entity.Type = IEntity.EntityType.UndeadPlayer;
//DUMMY - this is how we deal with zombies once you're undead, we make them chase stupid bait around
var go = new GameObject("PlayerBait");
go.transform.position = _entity.transform.position.SetY(0.5f);
go.AddComponent<PlayerBait>();
//END DUMMY
_animator.StruckAnimations.PlayDeath();
//TODO - fade out camera with 'YOU DIED'
yield return WaitForDuration.Seconds(12f);
//TODO - fade camera back in
yield return _animator.StruckAnimations.PlayRebirth();
//TODO - change movement style
_entity.HealthMeter.Health = float.PositiveInfinity;
_entity.FindComponent<MovementMotor>().States.ChangeState<PlayerUndeadWalkMovementStyle>();
_entity.Stalled = false;
_idleActionTicker = 0f;
}
private System.Collections.IEnumerator PlayZombieDeathRoutine()
{
//TODO - need animations and what not for this!
yield break;
}
#endregion
}
}
Now, I ain’t going to lie. This one is VERY SLOPPY.
I had 72 hours to make a game, so I kind of just slapped stuff in here as we moved along to just get it working. If I had more time, I would have made this nicer.
But you can see things in here like how I use the 'Sensor’s I talked about earlier to lock onto zombies, or to activate things around the scene (see method ‘AttemptActivate’).
Some of the logic could have been broken up better… especially the determining which state we’re in, undead/paused/aiming/whatnot.
And you can see here that I organize my scripts namespace wise together.
So, now you can see how I organize my logic.
This may not be the best option, but it’s what works for me… and I too come from a business solutions type background.
Finite state machines can be fine for certain things. For instance I use one for my movement styles.
Anyways, hopefully what I showed is of some help.
The links to my github is the opensource portion of my library (not all of it is released, my AI/Animation/Movement and what not stuff is stilled closed source). You’re more than welcome to look around it. It’s not perfect though… poorly commented and some of it a little slap-dash as I iron kinks out and what not.
Yup, there’s a lot of bad Unity tutorials out there.
A big issue is that a lot of them are geared towards teaching both Unity as the game engine, and basic C# scripting. Almost none of them assume that you know what a for-loop is when you start out. Unity’s official tutorials have the same problem.
I’ve been meaning to create Unity tutorials for programmers, but there’s no time now. I’ve also just worked professionally in the field for 3 years, so idk if I’d be the best one for that.
There’s a bunch of things that are counter-intuitive to programmers that are never explained because non-programmers won’t even notice. The ones I remember off the top of my head (random order) are:
1: Almost all of the objects Unity uses inherit from UnityEngine.Object. This class has overridden the == method so that it returns true for obj == null if obj is an existing object that has been destroyed. Unity also throws faux nullReferences (“MissingReferenceException”) if you try to access fields of a destroyed object.
This is super-strange, and breaks everything you know about how OO languages with a GC works. There’s been a bunch of talk about if it should be changed or not. I’ll not go into that, but the practical implications are:
you can do if (x != null) to guard both against not initialized objects and against destroyed objects. There’s almost no instances where you want to treat those two things differently, so it’s a convenience
If you’re working with an object with a reference to it’s interface on the other hand, the == is statically dispatched to the interface’s ==, which is System.Object, which doesn’t do Unity’s is-destroyed check. That’s a caveat you’ll have to be aware of.
similarly, ?? doesn’t use the object’s == override, so these two are not the same:
a = b != null ? b : c;
//and
a = b ?? c;
Finally, there’s instances where (this == null) will return true. That’s incredibly insane.
If you really need to know if an object is actually null or not, object.ReferenceEquals doesn’t lie. I find this extension useful for when working with an interface reference:
public static bool IsNullOrUnityNull(this object obj) {
if (obj == null) { //C# null check
return true;
}
if (obj is UnityEngine.Object) {
if (((UnityEngine.Object) obj) == null) { //Unity null check
return true;
}
}
return false;
}
1.5: Extension methods are super-useful since you don’t have access to the engine’s source.
2: The Unity engine is a c++ program. You interface with it in C# code. This ties back to the UnityEngine.Object thing; everything that’s a UnityEngine.Object also exists in c++ land.
The practical effect of this is that you can’t really work with Unity’s objects (like MonoBehaviours) on other threads, otherwise the two “versions” of the thing would get disconnected. You can do multithreading just fine, but you’ll want to gather up data as native C#-objects or primitives, and work on those.
3: Unity’s on a really old version of Mono. They’re in the process of upgrading it as we speak (the first updates were rolled out in 5.5, which launched like a week ago), and they’re planning on getting it up to date. In the meantime, be aware that you’re on C# 4. 4.5? Something like that. You don’t have async/await or the x?.y operator. The GC is an outdated piece of garbage.
3: This isn’t Unity-specific: Object Pooling is actually not horrible. The rest of the programming world moved away from that years ago, due to faster gc’s and higher costs of managing pooled objects than instantiation,
But game objects - not an OO object, but an actual thing in the game like a monster or a bullet - are huge things, with large attached 3d models and meshes and textures and whatnot. If you are rapidly spawning and destroying things like projectiles or enemies, you either have to solve them with particles or other effects, or pool them. Otherwise you’re looking at GC spikes, which will cause lag spikes.
4: Unity sucks at Unit testing. There’s no good way to mock things like physics events or “wait for the next frame”. There’s integration tests where you set up a small test scene and check some condition after time, but my experience is that framerate differences and randomness wrecks that.
This means that when you build the framework for your game, you should really, really, really think hard about making it unit-testable. I’ve not got a silver bullet for this, but moving to separate the game logic from Unity’s messaging system is probably a good start.
5: Editor scripting is incredibly powerful! It’s easily the best feature of Unity, and Unity is from what I can tell from how they’re advertising themselves completely unaware of the fact. The API for writing custom windows for your editor window is incredibly easy and fast to use, and you use exactly the same API for working with objects in the scene at edit time as at runtime. Making something like a button that replaces all the selected objects in the scene with a prefab (for things like replacing a bunch of props with a newer version or replacing the enemies in the scene with an other type) takes 2 minutes if you know the process.
6: Unity’s serializer does some really neat things, and is what makes the iteration time fast. This blog post sums it up nicely. There are some drawbacks, though:
It doesn’t handle inheritance… at all. If you have an Animal[ ], and put a bunch of Cat’s and Dog’s in there, they’re coming out as Animal instances when you come back from deserialization (ie. when you go from editing to playing). This drawback doesn’t hold for Unity’s own objects, so you can safely have a MonoBehaviour[ ] and put different MonoBehaviour subclasses in there. (This is because those are stored by object reference in the scene)
It doesn’t handle collections very well. It stores List and T[ ], but no multi-dimensional structures are supported. So no T[,] or Dictionary<T,V>.
There’s a built-in fix for this in the ISerializationCallbackReceiver interface, which allows you to hook into the serialization process and read/write data. Once you get into things, you’ll end up using this a bunch.
7: Coroutines are a really cool pattern, which abuses the hell out of C#'s already really cool IEnumerator yield-pattern. It’s how you’ll be doing things that need to happen over time - like a bar filling up or whatnot. Learn them!
By all means, ask follow-up questions! I’ve you want immediate feedback, we’ve got a discord channel (see my sig). You’d be the one in the channel with the longest professional experience (I believe), but we know a thing or two about Unity. Of course, post questions in the forum as well!
i am having problems on what to search on the internet. i dont know the terms, i dont know game programming. is there any book explaining the tricks or best practices on game programming ? how things work ?
its really hard to dive into game programming without any knowledge.
It’s actually not hard at all. I had a few years of programming knowledge with c# and had played around with a few smaller game engines, but went to Unity when I discovered it used c#. Then I just started with some tutorials. Yeah, they were basic stuff, but they help you learn a bit how things are done. Working with components and gameobjects and such.
Next step was just to start trying to make a game and when I got stuck, I’d google what it was I was trying to do. I now work for a small game company and have a few titles under by belt, so it did work out some how.
Just find a starting point and work up from there. Experiment, practice and google or come here if you get stuck.
Good answers from @lordofduct, its been years to answer but let me add something here:_ 1. In unity u dont need to worry about start point, the engine of unity can handle every classes which they inherits from MonoBehavior class and u can do handle ur code by methods like “Start() and Awake()”.
2. If u have experience with computer programming u should be able to manage bad codes and learn the basic of gaming programming from them and then implement ur own system for 2D or 3D games in unity.
3. Unity visualized the boring staff like create animation or design ur scene, but if u think u need to work with really OOP then u can try “MonoGame, UrhoEngine, SlimDX” and they are just like unity but u have to manage all ur code by urself.