我试图建立一个简单游戏的更新循环,构建时考虑到了observables。顶级组件是模型,它接收输入命令并产生更新;和视图,显示收到的更新,并生成输入。孤立地,两者都很好,有问题的部分是把两者放在一起,因为两者都依赖于另一个。
将组件简化为以下内容:
var view = function (updates) {
return Rx.Observable.fromArray([1,2,3]);
};
var model = function (inputs) {
return inputs.map(function (i) { return i * 10; });
};
我将事情联系在一起的方式是:
var inputBuffer = new Rx.Subject();
var updates = model(inputBuffer);
var inputs = view(updates);
updates.subscribe(
function (i) { console.log(i); },
function (e) { console.log("Error: " + e); },
function () { console.log("Completed"); }
);
inputs.subscribe(inputBuffer);
也就是说,我添加一个主题作为输入流的占位符,并将模型附加到该主题。然后,在构造视图之后,我将实际输入传递给占位符主题,从而关闭循环。
但是,我无法帮助,但觉得这不是正确的做事方式。使用一个主题似乎是矫枉过正。有没有办法用publish()或defer()或其他东西做同样的事情?更新:这是一个不太抽象的例子来说明我遇到的问题。下面你会看到一个简单的#34;游戏"的代码,玩家需要点击目标才能点击它。目标可以出现在左侧或右侧,无论何时被击中,它都会切换到另一侧。看起来很简单,但我仍然觉得我错过了一些东西......
//-- Helper methods and whatnot
// Variables to easily represent the two states of the target
var left = 'left';
var right = 'right';
// Transition from one side to the other
var flip = function (side) {
if (side === left) {
return right;
} else {
return left;
}
};
// Creates a predicate used for hit testing in the view
var nearby = function (target, radius) {
return function (position) {
var min = target - radius;
var max = target + radius;
return position >= min && position <= max;
};
};
// Same as Observable.prototype.scan, but it also yields the initial value immediately.
var initScan = function (values, init, updater) {
var initValue = Rx.Observable.return(init);
var restValues = values.scan(init, updater);
return initValue.concat(restValues);
};
//-- Part 1: From input to state --
var process = function (inputs) {
// Determine new state based on current state and input
var update = function(current, input) {
// Input value ignored here because there's only one possible state transition
return flip(current);
};
return initScan(inputs, left, update);
};
//-- Part 2: From display to inputs --
var display = function (states) {
// Simulate clicks from the user at various positions (only one dimension, for simplicity)
var clicks = Rx.Observable.interval(800)
.map(function (v) {return (v * 5) % 30; })
.do(function (v) { console.log("Shooting at: " + v)})
.publish();
clicks.connect();
// Display position of target depending on the model
var targetPos = states.map(function (state) {
return state === left ? 5 : 25;
});
// Determine which clicks are hits based on displayed position
return targetPos.flatMapLatest(function (target) {
return clicks
.filter(nearby(target, 10))
.map(function (pos) { return "HIT! (@ "+ pos +")"; })
.do(console.log);
});
};
//-- Part 3: Putting the loop together
/**
* Creates the following feedback loop:
* - Commands are passed to the process function to generate updates.
* - Updates are passed to the display function to generates further commands.
* - (this closes the loop)
*/
var feedback = function (process, display) {
var inputBuffer = new Rx.Subject(),
updates = process(inputBuffer),
inputs = display(updates);
inputs.subscribe(inputBuffer);
};
feedback(process, display);
答案 0 :(得分:2)
我想我明白你要在这里实现的目标:
我相信这里的答案是你可能想要翻转你的设计。假设MVVM样式设计,而不是让模型知道输入序列,它变得不可知。这意味着您现在拥有一个具有InputRecieved / OnInput / ExecuteCommand方法的模型,View将使用输入值调用该方法。现在,您可以更轻松地在一个方向处理&#34;命令&#34;和#34;另一个方向的事件&#34;图案。这是CQRS的一小部分。
我们在过去4年中在WPF / Silverlight / JS的Views + Models上广泛使用了这种风格。
也许是这样的事情;
var model = function()
{
var self = this;
self.output = //Create observable sequence here
self.filter = function(input) {
//peform some command with input here
};
}
var viewModel = function (model) {
var self = this;
self.filterText = ko.observable('');
self.items = ko.observableArray();
self.filterText.subscribe(function(newFilterText) {
model.filter(newFilterText);
});
model.output.subscribe(item=>items.push(item));
};
<强>更新强>
感谢您发布完整的示例。看起来不错。我喜欢你的新initScan
运算符,这似乎是Rx的明显遗漏。
我把你的代码改编成了我可能写的方式。我希望它有所帮助。我做的主要事情是将逻辑封装到模型中(翻转,附近等),并让视图将模型作为参数。然后我也必须添加一些成员到模型而不是它只是一个可观察的序列。然而,这确实允许我从视图中删除一些额外的逻辑并将其放入模型中(命中逻辑)
//-- Helper methods and whatnot
// Same as Observable.prototype.scan, but it also yields the initial value immediately.
var initScan = function (values, init, updater) {
var initValue = Rx.Observable.return(init);
var restValues = values.scan(init, updater);
return initValue.concat(restValues);
};
//-- Part 1: From input to state --
var process = function () {
var self = this;
var shots = new Rx.Subject();
// Variables to easily represent the two states of the target
var left = 'left';
var right = 'right';
// Transition from one side to the other
var flip = function (side) {
if (side === left) {
return right;
} else {
return left;
}
};
// Determine new state based on current state and input
var update = function(current, input) {
// Input value ignored here because there's only one possible state transition
return flip(current);
};
// Creates a predicate used for hit testing in the view
var isNearby = function (target, radius) {
return function (position) {
var min = target - radius;
var max = target + radius;
return position >= min && position <= max;
};
};
self.shoot = function(input) {
shots.onNext(input);
};
self.positions = initScan(shots, left, update).map(function (state) {
return state === left ? 5 : 25;
});
self.hits = self.positions.flatMapLatest(function (target) {
return shots.filter(isNearby(target, 10));
});
};
//-- Part 2: From display to inputs --
var display = function (model) {
// Simulate clicks from the user at various positions (only one dimension, for simplicity)
var clicks = Rx.Observable.interval(800)
.map(function (v) {return (v * 5) % 30; })
.do(function (v) { console.log("Shooting at: " + v)})
.publish();
clicks.connect();
model.hits.subscribe(function(pos)=>{console.log("HIT! (@ "+ pos +")");});
// Determine which clicks are hits based on displayed position
model.positions(function (target) {
return clicks
.subscribe(pos=>{
console.log("Shooting at " + pos + ")");
model.shoot(pos)
});
});
};
//-- Part 3: Putting the loop together
/**
* Creates the following feedback loop:
* - Commands are passed to the process function to generate updates.
* - Updates are passed to the display function to generates further commands.
* - (this closes the loop)
*/
var feedback = function (process, display) {
var model = process();
var view = display(model);
};
feedback(process, display);
答案 1 :(得分:1)
我认为,因为您在创建模型后没有“分配”输入,所以您的目标是实现模型和视图的非变异方法。但是,您的模型和视图似乎彼此依赖。要解决此问题,您可以使用第三方来促进两个对象之间的关系。在这种情况下,您可以简单地使用函数进行依赖注入...
var log = console.log.bind(console),
logError = console.log.bind(console, 'Error:'),
logCompleted = console.log.bind(console, 'Completed.'),
model(
function (updates) {
return view(updates);
}
)
.subscribe(
log,
logError,
logCompleted
);
通过为模型提供工厂来创建视图,您可以通过实例化视图来为模型提供完全实例化的能力,但不知道 视图是如何实例化的。
答案 2 :(得分:0)
根据我对问题本身的评论,这里是您在Windows中使用调度程序完成的相同类型的代码。我希望在RxJS中有类似的界面。
var scheduler = new EventLoopScheduler();
var subscription = scheduler.Schedule(
new int[] { 1, 2, 3 },
TimeSpan.FromSeconds(1.0),
(xs, a) => a(
xs
.Do(x => Console.WriteLine(x))
.Select(x => x * 10)
.ToArray(),
TimeSpan.FromSeconds(1.0)));
我得到的输出,每秒有三个新数字,是:
1
2
3
10
20
30
100
200
300
1000
2000
3000
10000
20000
30000