par Mike Ton Il y a 9 années
953
Plus de détails
Essentially Rx is built upon the foundations of the Observer pattern. .NET already exposes some other ways to implement the Observer pattern such as multicast delegates or events (which are usually multicast delegates). Multicast delegates are not ideal however as they exhibit the following less desirable features;
In C#, events have a curious interface. Some find the += and -= operators an unnatural way to register a callback
Events are difficult to compose
Events don't offer the ability to be easily queried over time
Events are a common cause of accidental memory leaks
Events do not have a standard pattern for signaling completion
Events provide almost no help for concurrency or multithreaded applications. e.g. To raise an event on a separate thread requires you to do all of the plumbing
Rx looks to solve these problems. Here I will introduce you to the building blocks and some basic types that make up Rx.
UF_MyProject/
(root)
(scripts for diagram)
UF_Prototype_DgViews.designer.cs
UF_Prototype_DgControllers.designer.cs
UF_Prototype_Dg.designer.cs
(diagram)
UF_Prototype_Dg.asset
Views/
ViewModels/
SceneManagers/
Controllers/
(prefab)
Resources/
prefab name MUST match collection modelView type to autopopulate
Scenes/
UF_Prototype.unity
(go)
(component)
Binding
??? Causes view to spawn instances ???
if want unique value at each view level, set up MonoBehaviour container
value at model SAME/STATIC for all view instances!
(ViewModel Properties)
Score
0
State
Menu
Init
initialize ViewModel
true
Resolve Name
Game
Subsystem
Controller
Elements
(data source/class)
(mton)
(display/instance)
(GameObject Component)
View
mtonHUDView.cs
?(ViewComponents)?
ItemMenu
EnergyBar
mtonView.cs
(UFRAME_UI)
Behaviours
iFlapCountChanged
2-Way Properties
(reacting to changingstate)
(MonoBehaviour)
(Execute{CommandName})
ExecuteFlap(args...);
float valueForThisInstance;
//value here can be locally assigned per instance in unity
mton
mtonController.cs
(implements Controller methods)
(iFlapCount)
dynamic
(required)
must be set to true
must Bind to view
public override void Flap (BirdViewModel bird){ ... }
bird.iFlapCount++;
static
BirdController.cs
public override void Flap (){ ... }
Bird.iFlapCount++;
(extends mtonDiagram.designer.cs)
config
Has Multiple Instances
mtonViewModel.cs
Flap
mtonHuman[]
mEnemies
mtonHuman
mPlayer
Computed
(state change based on value)
Health < 50%
Enum
Enum.Alert -> Enum.Tired
iFlapCount
(from direct child subsystem)
(View)
(InspectorSettings)
Id
Initialize View Model
//Overrides what gets set in controller initialization
//Can cause instance errors
Inject This View
Save & Load
Force Resolve
(input)
(uFRAME)
(element)
(viewModel)
players
gLEVELController.cs
SpawnPlayer (gLEVELViewModel gLEVEL, Vector3 arg){ ... }
gLEVEL.players.Add(player);
var player = this.UF_PLAYERController.CreateUF_PLAYER();
base.SpawnPlayer(gLEVEL, arg);
//init ViewModel values here
gLEVELView.cs
//CAN BE OVERRIDEN BY
unity view Initialization/Initialize ViewModel
(UF_PLAYER NOT gLEVEL)
ViewBase CreateplayersView(UF_PLAYERViewModel item) { ... }
return _gPlayer_;
//must return view
_gPlayer_.transform.position = this.playerSpawnPoint.position;
//position playerView
_gPlayer_.InitializeData(item);
//takes inspector information and applies it to the viewmodel
this.transform.InitializeView(_gPlayer_.name, (ViewModel) item, _gPlayer_.gameObject, _gPlayer_.name);
//does something string manip that BossPool needs???
var _gPlayer_ = base.CreateplayersView(item).GetComponent
(else)
var _gPlayer_ = this.InstantiateView("UF_PLAYER_Player2", item).GetComponent
//searches Resources Folder for "UF_PLAYER_Player2" prefab
//if in subfolder "Resources/Char" then "Char/UF_PLAYER2"
//searches Resource Folder for "UF_PLAYER" prefab
//intercept/get player instancedView
(Prefab)
How to inject variant prefabs???
Must also have variant Controller ???
NO
//MUST
Exact same name as Controller
Else:
Ex:
UF_PLAYERController
prefab => "UF_PLAYER"
Resources folder with settings
(unity)
(Resource/)
UF_PLAYER_Player2
UF_PLAYER
LevelSystem
(sys)
WeaponSystem
BaseProjectile
LaserBolt
LaserBoltView.cs
DestroyExecuted() { ... }
base.DestroyExecuted();
//executes after controller command is executed
ExecuteDestroy() { ... }
Destroy(this.gameObject);
//why do this in both???
base.ExecuteDestroy();
//executes command in controller
Observable.Interval(TimeSpan.FromMilliseconds(10000)).Subscribe(x =>{ ... }).DisposeWith(this);
if (this != null){ this.ExecuteDestroy(); }
this.BindComponentTriggerWith
//asteroid should be responsible for destroying himself
//this creates null exceptions later becouse it executes in the same time as the binding from the asteroid view
//and creates a race condition where we get null exception for parent level manager in asteroid, becouse we have
//a subscription to destroy command from level manager where the asteroid is removed from collection
//asteroidView.ExecuteDestroy();
this.ExecuteDestroy();
this.ExecuteHit();
GetComponent
//for firing this bolt should be rotated by emitter
this.transform.rotation = this.ParentView.transform.rotation;
//What if no parent view???
BaseWeapon
BasicLaser
BasicLaserView.cs
public
CreateProjectilesView(BaseProjectileViewModel item){ ... }
return laser;
laser.transform.position = spawnPoint.position;
// move to spawn position
var laser = base.CreateProjectilesView(item);
Transform
spawnPoint;
(controllers)
BasicLaserController.cs
Fire(BaseWeaponViewModel baseWeapon){ ... }
baseWeapon.ParentPlayerShip.IsAliveProperty.Where(isAlive => !isAlive).Subscribe(_ => { laser.Destroy.Execute(null); }).DisposeWith(baseWeapon);
//destroys laser when ship dies. Also disposes of weapons???
laser.Destroy.Subscribe(_ => { baseWeapon.Projectiles.Remove(laser); }).DisposeWith(laser);
//on laser destroyed, remove from projectile list
laser.Hit.Subscribe(_ => { baseWeapon.ParentPlayerShip.AsteroidsDestroyed++; }).DisposeWith(laser);
//adds +1 asteroid point to parent of weapon on hit
baseWeapon.Projectiles.Add(laser);
var laser = LaserBoltController.CreateLaserBolt();
//where is CreateLaserBolt defined???
base.Fire(baseWeapon);
InitializeBasicLaser(BasicLaserViewModel basicLaser) { ... }
LaserBoltController
LaserBoltController { get; set; }
[Inject]
//wth is this?
//!!! must have prefab in Resources/LaserBolt !!!
Fire
BaseProjectiles
Projectiles
FireRate
PlayerShipSystem
PlayerShipView.cs
OnFire() { ... }
this.PlayerShip.Weapon.Fire.Execute(null);
//Why Fire.Execute???
base.OnFire();
OnStop() { ... }
base.OnStop();
FireStateMachineChanged(Invert.StateMachine.State value) { ... }
base.FireStateMachineChanged(value);
Restart
(c# prim)
IsAlive
ShouldFire
FireTimeOutElapsed
FiringCommand
IsMoving
AsteroidsDestroyed
MovementSpeed
BaseWeaponViewModel
Weapon
FireStateMachine
MovementStateMachine
PowerUpSystem
AsteroidView.cs
AsteroidViewModel.cs
AsteroidController.cs
public override void InitializeAsteroid(AsteroidViewModel asteroid) { ... }
Destroy
Damage
Position
Life
PowerUpBaseViewModel
PowerUp
(abstract)
PowerUpBase
FireRatePowerUp
SpeedPowerUp
PlayerShip
ApplyPowerUp
Description
Modifier
(elements)
(scripts)
LevelManagerView.cs
(func)
ScoreChanged(Int32 value){ ... }
ScoreText.text = string.Format("Score: {0}", value);
base.ScoreChanged(value);
(unchanged)
SpawnPointChanged(Vector3 value){ ... }
RestartLevelExecuted(){ ... }
GameOverChanged(Boolean value){ ... }
PlayerChanged(PlayerShipViewModel ship){ ... }
NotificationTextChanged(String value) { ... }
Observable.Timer(TimeSpan.FromMilliseconds(500)).Subscribe(x =>{ ... });
InfoText.text = string.Empty;
//500 mss later, clear message
InfoText.text = value;
//onChange, update message
base.NotificationTextChanged(value);
UpdateAsObservable().Where(_ => Input.GetKey(KeyCode.R)).Subscribe(_ => { this.ExecuteRestartLevel(); });
this.BindInputButton(LevelManager.RestartLevel, "Restart", InputButtonEventType.ButtonDown);
//NOTE: this requires that we setup input button restart in Edit -> Project Settings -> Input
??? On input keyCode.R, restart level ???
asteroids
AsteroidsRemoved(ViewBase item){ ... }
AsteroidsAdded(ViewBase item){ ... }
ViewBase
CreateAsteroidsView(AsteroidViewModel item){ ... }
return ast;
ast.transform.position = new Vector3(UnityEngine.Random.Range(-this.LevelManager.SpawnPoint.x, this.LevelManager.SpawnPoint.x), this.LevelManager.SpawnPoint.y, this.LevelManager.SpawnPoint.z);
ast.InitializeData(ast.ViewModelObject);
??? Inits ViewModel based on what's in scene ???
var ast = InstantiateView(prefabName, item);
//must have a prefab in "SpaceShooterTutorial-uFrame/Resources/" that matches name
var prefabName = string.Format("{0}{1}", "Asteroid", UnityEngine.Random.Range(1,6));
//generates prefab name
(var)
GUIText
RestartText
GameOverText
ScoreText
InfoText
(viewmodel)
LevelManagerViewModel.cs
ComputeScore(){ ... }
return Player.AsteroidsDestroyed;
//asteroid destroyed count is stored on player property; get it and display it as function of LevelManager
if(Player == null){ return 0; }
(controller)
LevelManagerController.cs
ShowNotification(LevelManagerViewModel levelManager, string arg) { ... }
base.ShowNotification(levelManager, arg);
levelManager.NotificationText = arg;
RestartLevel(LevelManagerViewModel levelManager) { ... }
if (!levelManager.Player.IsAlive){ ... }
this.ExecuteCommand(levelManager.Player.Restart);
base.RestartLevel(levelManager);
InitializeLevelManager(LevelManagerViewModel levelManager) { ... }
levelManager.PlayerProperty
.DisposeWith(levelManager);
.Subscribe(player => player.IsAliveProperty.Where(isAlive => !isAlive).Subscribe(_ => { levelManager.GameOver = true; }))
.Where(player => player != null)
//we should not chage the gameover property from outside of the level manager !
//we should make a game over computed property which will determine if the game is over
(spawn Asteroids)
Observable.Interval(TimeSpan.FromMilliseconds(1000)).Subscribe(x =>{ ... });
levelManager.Asteroids.Add(asteroid);
//controller adds item to collection; view inits and handles that item
asteroid.Destroy.Subscribe(_ => { levelManager.Asteroids.Remove(asteroid); });
var asteroid = AsteroidController.CreateAsteroid();
(alt)
//What is the difference???
var asteroid = new AsteroidViewModel(AsteroidController);
levelManager.GameOver = false;
(config)
ShowNotification
RestartLevel
Asteroid
Asteroids
//And this is Asteroid type; not AsteroidViewModel???
string
NotificationText
GameOver
SpawnPoint
PlayerShipViewModel
Player
//Why is this ViewModel type as prop
(instances)
LevelManagerViewModel
LevelManager
ViewModels
AvatarPosition
AimTriangle
(Views)
ViewComponent
TargetingStrategy
(ClosestTargetingStrategy.cs)
(implementation class)
FindTarget() { ... } ;
return FindObjectsOfType
.FirstOrDefault();
.Select( ... )
(v => v.SqrTarget)
.OrderBy( ... )
( v => Vector3.Distance(v.transform.position, transform.position))
(TargetingStrategy.cs)
(abstract class)
abstract
FindTarget() ;
Update(){ ... }
LastTargetProperty.OnNext(target);
//??? what is OnNext() ????
var target = FindTarget();
LastTarget{ ... };
return LastTargetProperty.Value;
//shortcut to get current last target value
LastTargetProperty{ ... };
get { ... }
return _lastTarget ?? (_lastTarget = new P
???
P
_lastTarget;
//P properties are properties and observables at the same time. Cool.
ComputedProperties
IsTriggerCloseEnough
//ComputedProperties why??? vs. ViewProperties
SqrTargetView
Bindings
AutoTargetChanged
Scene Properties
(AimTriangleView.cs)
AutoTargetChanged(Boolean value){ ... }
else{ ...}
ExecuteSetTarget(null);
if(value){ ... }
this.BindProperty( ... ).DisposeWhenChanged(AimTriangle.AutoTargetProperty);
target => ExecuteSetTarget(target);
TargetingStrategy.LastTargetProperty
???Where does this come from???
Commands
SqrTarget
SetTarget
//Why define as command here vs. in view???
//Not siloed to view
//But let's view access!
//Defined here so that GameRoot can access
Properties
AutoTarget
DistanceToTarget
SqrTargetViewModel
Target
//Whereas only a property, AimTriangle only references target??
GameRoot
Collections
SqrTargets
AimTriangles
//If connected to collections...GameRoots owns both AimTriangles and SqrTargets ??
Pickup
//??? Pickup command is never implemented...only a placeholder to subscribe to
(prop)
playerElementView.cs
//handle spike collision
this.BindComponentCollisionWith
_ =>{ ... }
ExecuteOnHit();
//handle coin collision
this.BindViewTriggerWith
(lambda)
coinview =>{ ... }
(event)
CollisionEventType.Enter
playerElementController.cs
OnHit( playerElementViewModel player) { ... }
else{ ... }
Observable.Timer(TimeSpan.FromMilliseconds(1500)).Subscribe( ... );
l => {character.bInvulnerable = false;}
//change state to be eligible for damage in function called at 1500ms
//what is l???
//subscribe to event 1500 ms from now
//couroutine???
character.bInvulnerable = true;
//character no longer eligible for damage
if(character.lives <= 0){ character.bAlive=false; }
//if no lives left die => gameover
character.lives--;
if(character.bInvulnerable==true){ return; }
//return without damage is character not eligible for damage
PickupCoin( playerElementViewModel player) { ... }
player.numCoin++;
(command)
OnHit
PickupCoin
int
numCoin
Untiy3D
Spike.cs
//arbitrary Unity Script Object
io_View.cs
(binding)
On{NameofState}
Onright(){}
Onleft(){}
Ondown(){}
Onup(){}
Onneutral(){}
// GetInputObservable???
IObservable
GetCalculate{NameOfProperty}AsObservable(){ ... }
//only invokes every x seconds
return PositionAsObservable.Sample(TimeSpan.FromSeconds(1.0f)).Select(p=>CalculatevPos());
//only invokes if postion has changed
return PositionAsObservable.Select(p=>CalculatevPos());
// calculates on each update
if(GetInput.Event){ dpad == dir.left}
UFrame
io_Controller.cs
io_ViewModel.cs
(computed properties)
(boolean only)
(dpad_SM)
(states)
right
left
down
up
nuetral
(transitions)
doRight
doLeft
doDown
doUp
doNeutral
(dpad)
bool
Compute{NameofProperty}
cRight
cLeft
cDown
cUp
cNeutral
dpad
_DesignerFiles
SubSystem
Element
(Controller)
ElementController.cs
(Implement ViewModel Commands)
JumpStateChanged(ElementViewModel myElement, State state){ ... }
else if(state is Rise){ ... }
Observable.Timer(TimeSpan.FromMilliseconds(100)).Subscribe(...);
l=>{ myElement.JumpLock = false; }
//l=> ; l==long???
//unlock 100 ms from call
myElement.JumpLock = true;
myElement.JumpCount++;
//Only changes values in ViewModel...doesn't do anything with view
if(state is Ground){ ... }
myElement.JumpCount = 0;
//reset jumpCount
//WTH is "is" ???
(Init ViewModelController)
InitializeElement( ElementViewModel myElement ){ ... }
(Instantiate ChildViewModel)
new myChildElementViewModel();
// public empty constructor method WARNING : NO INIT or Controller !
myChildElementController.CreateMyChildElement();
(illustration of ChildViewModel)
//will link to element controller and call it's initial methods: MyChildElementController.cs -> InitializeMyChildElement(){ ... }
myElement.JumpStateProperty.Subscribe( ... );
state=>JumpStateChanged(myElement, state)
Controller Code Connects ViewModel -> View relationships
??? Commands
Implements
Subscribes
ViewModel Property
Instantiates
Child ViewModels
(ViewModel)
ElementViewModel.cs
(MainDiagram.designer.cs)
protected CommandWithSender
//command == object that can trigger an event
public ModelCollection
// collection
public P
// property can be of
_ItemsProperty = new P
(property)
_NameProperty = new P
(GUI)
(properties)
(commands)
(exe/call syntax)
(other)
controller
this.ExecuteCommand(vm.command[ , arg]);
(ex:)
this.ExecuteCommand(turret.Kill);
TurretViewModel turret = null;
(own)
view
this.Execute{CommandName};
(collections)
(int)
JumpCount
(bool)
JumpLock
(computed variable)
IEnumberable
Get{NameofComputeProperty}Dependents(){...}
return base.Get{NameofComputeProperty}Dependents()
//returns all properties that compute is dependent on
bool (can return other types; must be bool for statemachine transition)
Compute{NameofComputeProperty}(){ ... }
(generated functions)
//Recomputes property and all dependents
return
Element.target.distToTarget;
//All variables used in computed property, must be attached to computed property
//MUST BE BOOL TO OPERATE STATEMACHINE transitions
//Computed property updated on each input variable change
(class)
ElementTargetViewModel
ElementTargetView.cs
Vector3
CalculatevPos(){ ... }
return this.transform.position;
//TargetView must have a scene property vPos???
Calculate{SceneProperty})(){ ... }
//defaults : updates on every Update()
target
//TargetViewModel must have a view : TargetView
(float)
(statemachine)
(enum)
(view)
(bindings)
(Collections)
(StateMachine)
//Only available to state machine?
//Will generate On{StateName} function for each state!
(Standard)
//for state machine : will initiate as if standard property
//must check value and do if/else/switch functions manually
(Code)
ViewModel
Compute{NameOfComputeProperty}
Binding(){ ... }
Reset{NameofComputeProperty}();
base.Bind();
//Instance???
// NOTE : Also make sure bindings are enabled on the unity instance property!
//allows ElementView to react to ElementViewmodel property changes
(scene properties)
distToTarget
//Updating ViewModel Properties using View calculations : Calculate{ViewModelPropertyName}
(code)
ElementView.cs
ElementViewComponent.cs
private
Landed(){ ... }
_audioSource.PlayOneShot(LandedSound);
JumpStateChanged(){ ... }
Bind(ViewBase view){ ... }
(direct)
.DisposedWith(view);
//on this view destroy, dispose observer
.Subscribe(value => Landed())
//if value true call Landed()
.Where(value => value)
//???
.DistinctUntilChanged().
//Only calls on value change
Character.IsOnGroundProperty.
(built-in)
view.BindProperty(Character.JumpStateProperty, JumpStateChanged);
(Unity MonoB)
(ViewBase view)
//Points to viewbase update based on viewmodel properties
protected
float
CalculatedistToPlayer (){ ... }
return Vector3.Distance(transform.position, Element.target.vPos);
if(Element.target == null){ return 0.0f; }
//Calclate{ScenePropertyName}
public
override
void
Bind(){ ... }
public override void Bind (){
base.Bind ();
this.BindViewCollisionWith<CoinView>(
CollisionEventType.Enter, coinview =>{
coinview.ExecutePickup();
ExecutePickupCoin();
});
}
//collision
this.BindViewCollisionWith
//lambda
coinview =>{ ... }
ExecutePickupCoin();
// call character's coin pickup function
coinview.ExecutePickup();
// call coin's pickup function
JumpStateChange(Invert.StateMachine.State value) { ... }
base.JumpStateChanged(value);
//if commented out; won't call base from ElementController.cs
SceneManager
(creates UnityScene)
myLevelScene_00.unity
(duplicate)
myLevelScene_03.unity
myLevelScene_02.unity
myLevelScene_01.unity
myMenuScene.unity
_SceneManager
_GameManager