Categorii: Tot - events

realizată de Mike Ton 9 ani în urmă

960

UFRAME:OVERVIEW

The text discusses the implementation of the Observer pattern through Rx in the .NET framework, highlighting its advantages over traditional methods like multicast delegates and events.

UFRAME:OVERVIEW

UFRAME

Rx

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.

OVERVIEW

UNITY
(project)

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

(hiearchy)

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

DIAGRAM

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)

insert code

UFrame1p5

(Unity)

(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)

//HOW TO SPAWN

(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

//THERE ARE TWO OVERIDABLE FUNCTIONS FOR EACH COMMAND !!!!!!! //EXECUTE{CNAME} => Executes comand in the controller ! //{CNAME}EXECUTED => Executed after controller command is executed !
UF_Asteroids
LevelSceneManager

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(CollisionEventType.Enter, asteroidView =>{ ... }).DisposeWith(this);

//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().velocity = transform.forward * 10;

//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

ADVANCED:
Targetting System Example

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 ??

2DGameExample

Pickup

//??? Pickup command is never implemented...only a placeholder to subscribe to

(prop)

playerElementView.cs

//handle spike collision

this.BindComponentCollisionWith(event, lambda);

_ =>{ ... }

ExecuteOnHit();

//handle coin collision

this.BindViewTriggerWith(event, lambda);

(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

Calculate{NameOfProperty}(){ ... }

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

MainDiagram

_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 _Kill;

//command == object that can trigger an event

public ModelCollection _ItemsProperty;

// collection

public P _NameProperty;

// property can be of not just string

_ItemsProperty = new P(this, "Items");

(property)

_NameProperty = new P(this, "name");

(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(CollisionEventType.Enter, lambda);

//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