新增AI-GOAP目标导向计划与ASTAR寻路配合使用
This commit is contained in:
@@ -99,7 +99,7 @@ module es {
|
||||
/**
|
||||
* 使用PriorityQueue需要的额外字段将原始数据封装在一个小类中
|
||||
*/
|
||||
export class AStarNode<T> extends PriorityQueueNode {
|
||||
class AStarNode<T> extends PriorityQueueNode {
|
||||
public data: T;
|
||||
|
||||
constructor(data: T) {
|
||||
|
||||
200
source/src/AI/Pathfinding/GOAP/AStar.ts
Normal file
200
source/src/AI/Pathfinding/GOAP/AStar.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
///<reference path="./AStarStorage.ts" />
|
||||
module es {
|
||||
export class AStarNode implements IEquatable<AStarNode>, IPoolable {
|
||||
/**
|
||||
* 这个节点的世界状态
|
||||
*/
|
||||
public worldState: WorldState;
|
||||
|
||||
/**
|
||||
* 到目前为止的花费
|
||||
*/
|
||||
public costSoFar: number;
|
||||
|
||||
/**
|
||||
* 剩余成本的启发式(不要高估!)
|
||||
*/
|
||||
public heuristicCost: number;
|
||||
|
||||
/**
|
||||
* costSoFar+heuristicCost(g+h)组合
|
||||
*/
|
||||
public costSoFarAndHeuristicCost: number;
|
||||
|
||||
/**
|
||||
* 与此节点相关的动作
|
||||
*/
|
||||
public action: Action;
|
||||
|
||||
/**
|
||||
* 父节点
|
||||
*/
|
||||
public parent: AStarNode;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public parentWorldState: WorldState;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public depth: number;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param other
|
||||
*/
|
||||
public equals(other: AStarNode): boolean {
|
||||
let care = this.worldState.dontCare ^ -1;
|
||||
return (this.worldState.values & care) == (other.worldState.values & care);
|
||||
}
|
||||
|
||||
public compareTo(other: AStarNode){
|
||||
return this.costSoFarAndHeuristicCost - other.costSoFarAndHeuristicCost;
|
||||
}
|
||||
|
||||
public reset(){
|
||||
this.action = null;
|
||||
this.parent = null;
|
||||
}
|
||||
|
||||
public clone(): AStarNode{
|
||||
let node = new AStarNode();
|
||||
node.action = this.action;
|
||||
node.costSoFar = this.costSoFar;
|
||||
node.depth = this.depth;
|
||||
node.parent = this.parent;
|
||||
node.parentWorldState = this.parentWorldState;
|
||||
node.heuristicCost = this.heuristicCost;
|
||||
node.worldState = this.worldState;
|
||||
return node;
|
||||
}
|
||||
|
||||
public toString(): string{
|
||||
return `[cost: ${this.costSoFar} | heuristic: ${this.heuristicCost}]: ${this.action}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class AStar {
|
||||
public static storage: AStarStorage = new AStarStorage();
|
||||
|
||||
/**
|
||||
* 制定达到理想世界状态的行动计划
|
||||
* @param ap
|
||||
* @param start
|
||||
* @param goal
|
||||
* @param selectedNodes
|
||||
*/
|
||||
public static plan(ap: ActionPlanner, start: WorldState, goal: WorldState, selectedNodes: AStarNode[] = null){
|
||||
this.storage.clear();
|
||||
|
||||
let currentNode = Pool.obtain<AStarNode>(AStarNode);
|
||||
currentNode.worldState = start;
|
||||
currentNode.parentWorldState = start;
|
||||
currentNode.costSoFar = 0;
|
||||
currentNode.heuristicCost = this.calculateHeuristic(start, goal);
|
||||
currentNode.costSoFarAndHeuristicCost = currentNode.costSoFar + currentNode.heuristicCost;
|
||||
currentNode.depth = 1;
|
||||
|
||||
this.storage.addToOpenList(currentNode);
|
||||
|
||||
while(true){
|
||||
// 无路可走,无路可寻
|
||||
if (!this.storage.hasOpened()){
|
||||
this.storage.clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
currentNode = this.storage.removeCheapestOpenNode();
|
||||
|
||||
this.storage.addToClosedList(currentNode);
|
||||
|
||||
// 全部完成。 我们达到了我们的目标
|
||||
if (goal.equals(currentNode.worldState)){
|
||||
let plan = this.reconstructPlan(currentNode, selectedNodes);
|
||||
this.storage.clear();
|
||||
return plan;
|
||||
}
|
||||
|
||||
let neighbors = ap.getPossibleTransitions(currentNode.worldState);
|
||||
for (let i = 0; i < neighbors.length; i++){
|
||||
let cur = neighbors[i];
|
||||
let opened = this.storage.findOpened(cur);
|
||||
let closed = this.storage.findClosed(cur);
|
||||
let cost = currentNode.costSoFar + cur.costSoFar;
|
||||
|
||||
// 如果neighbors处于open状态,且成本小于g(neighbors)。
|
||||
if (opened != null && cost < opened.costSoFar){
|
||||
// 将neighbors从OPEN中移除,因为新的路径更好。
|
||||
this.storage.removeOpened(opened);
|
||||
opened = null;
|
||||
}
|
||||
|
||||
// 如果neighbors在CLOSED,且成本小于g(neighbors)
|
||||
if (closed != null && cost < closed.costSoFar){
|
||||
// 从CLOSED中删除neighbors
|
||||
this.storage.removeClosed(closed);
|
||||
}
|
||||
|
||||
// 如果neighbors不在OPEN,neighbors不在CLOSED。
|
||||
if (opened == null && closed == null){
|
||||
let nb = Pool.obtain<AStarNode>(AStarNode);
|
||||
nb.worldState = cur.worldState;
|
||||
nb.costSoFar = cost;
|
||||
nb.heuristicCost = this.calculateHeuristic(cur.worldState, goal);
|
||||
nb.costSoFarAndHeuristicCost = nb.costSoFar + nb.heuristicCost;
|
||||
nb.action = cur.action;
|
||||
nb.parentWorldState = currentNode.worldState;
|
||||
nb.parent = currentNode;
|
||||
nb.depth = currentNode.depth + 1;
|
||||
this.storage.addToOpenList(nb);
|
||||
}
|
||||
}
|
||||
|
||||
ListPool.free<AStarNode>(neighbors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部函数,通过从最后一个节点到初始节点的追踪来重建计划。
|
||||
* @param goalNode
|
||||
* @param selectedNodes
|
||||
*/
|
||||
public static reconstructPlan(goalNode: AStarNode, selectedNodes: AStarNode[]){
|
||||
let totalActionsInPlan = goalNode.depth - 1;
|
||||
let plan: Action[] = new Array(totalActionsInPlan);
|
||||
|
||||
let curnode = goalNode;
|
||||
for (let i = 0; i <= totalActionsInPlan - 1; i ++){
|
||||
// 如果我们被传递了一个节点,可以选择将该节点添加到列表中
|
||||
if (selectedNodes != null)
|
||||
selectedNodes.push(curnode.clone());
|
||||
plan.push(curnode.action);
|
||||
curnode = curnode.parent;
|
||||
}
|
||||
|
||||
// 我们的节点从目标回到了起点,所以把它们反过来。
|
||||
if (selectedNodes != null)
|
||||
selectedNodes.reverse();
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fr
|
||||
* @param to
|
||||
*/
|
||||
public static calculateHeuristic(fr: WorldState, to: WorldState){
|
||||
let care = (to.dontCare ^ -1);
|
||||
let diff = (fr.values & care) ^ (to.values & care);
|
||||
let dist = 0;
|
||||
|
||||
for (let i = 0; i < ActionPlanner.MAX_CONDITIONS; ++i)
|
||||
if ((diff & (1 << i)) != 0)
|
||||
dist ++;
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
107
source/src/AI/Pathfinding/GOAP/AStarStorage.ts
Normal file
107
source/src/AI/Pathfinding/GOAP/AStarStorage.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
module es {
|
||||
export class AStarStorage {
|
||||
/**
|
||||
* 我们可以存储的最大节点数
|
||||
*/
|
||||
public static readonly MAX_NODES = 128;
|
||||
|
||||
public _opened: AStarNode[] = new Array(AStarStorage.MAX_NODES);
|
||||
public _closed: AStarNode[] = new Array(AStarStorage.MAX_NODES);
|
||||
public _numOpened: number;
|
||||
public _numClosed: number;
|
||||
|
||||
public _lastFoundOpened: number;
|
||||
public _lastFoundClosed: number;
|
||||
|
||||
constructor(){}
|
||||
|
||||
public clear(){
|
||||
for (let i = 0; i < this._numOpened; i ++){
|
||||
Pool.free<AStarNode>(this._opened[i]);
|
||||
this._opened[i] = null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._numClosed; i ++){
|
||||
Pool.free<AStarNode>(this._closed[i]);
|
||||
this._closed[i] = null;
|
||||
}
|
||||
|
||||
this._numOpened = this._numClosed = 0;
|
||||
this._lastFoundClosed = this._lastFoundOpened = 0;
|
||||
}
|
||||
|
||||
public findOpened(node: AStarNode): AStarNode {
|
||||
for (let i = 0; i < this._numOpened; i ++){
|
||||
let care = node.worldState.dontCare ^ -1;
|
||||
if ((node.worldState.values & care) == (this._opened[i].worldState.values & care)){
|
||||
this._lastFoundClosed = i;
|
||||
return this._closed[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public findClosed(node: AStarNode): AStarNode {
|
||||
for (let i = 0; i < this._numClosed; i ++){
|
||||
let care = node.worldState.dontCare ^ -1;
|
||||
if ((node.worldState.values & care) == (this._closed[i].worldState.values & care)){
|
||||
this._lastFoundClosed = i;
|
||||
return this._closed[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public hasOpened(): boolean {
|
||||
return this._numClosed > 0;
|
||||
}
|
||||
|
||||
public removeOpened(node: AStarNode){
|
||||
if (this._numOpened > 0)
|
||||
this._opened[this._lastFoundOpened] = this._opened[this._numOpened - 1];
|
||||
this._numOpened --;
|
||||
}
|
||||
|
||||
public removeClosed(node: AStarNode) {
|
||||
if (this._numClosed > 0)
|
||||
this._closed[this._lastFoundClosed] = this._closed[this._numClosed - 1];
|
||||
this._numClosed--;
|
||||
}
|
||||
|
||||
public isOpen(node: AStarNode): boolean{
|
||||
return this._opened.indexOf(node) > -1;
|
||||
}
|
||||
|
||||
public isClosed(node: AStarNode): boolean {
|
||||
return this._closed.indexOf(node) > -1;
|
||||
}
|
||||
|
||||
public addToOpenList(node: AStarNode){
|
||||
this._opened[this._numOpened++] = node;
|
||||
}
|
||||
|
||||
public addToClosedList(node: AStarNode){
|
||||
this._closed[this._numClosed++] = node;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public removeCheapestOpenNode(): AStarNode {
|
||||
let lowestVal = Number.MAX_VALUE;
|
||||
this._lastFoundOpened = -1;
|
||||
for (let i = 0; i < this._numOpened; i ++){
|
||||
if (this._opened[i].costSoFarAndHeuristicCost < lowestVal){
|
||||
lowestVal = this._opened[i].costSoFarAndHeuristicCost;
|
||||
this._lastFoundOpened = i;
|
||||
}
|
||||
}
|
||||
|
||||
var val = this._opened[this._lastFoundOpened];
|
||||
this.removeOpened(val);
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
source/src/AI/Pathfinding/GOAP/Action.ts
Normal file
41
source/src/AI/Pathfinding/GOAP/Action.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
module es {
|
||||
export class Action {
|
||||
/**
|
||||
* Action的可选名称。用于调试目的
|
||||
*/
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* 执行动作的成本。 改变它将会影响到计划期间的行动/选择。
|
||||
*/
|
||||
public cost: number = 1;
|
||||
|
||||
public _preConditions: Set<[string, boolean]> = new Set<[string, boolean]>();
|
||||
public _postConditions: Set<[string, boolean]> = new Set<[string, boolean]>();
|
||||
|
||||
constructor(name?: string, cost: number = 1){
|
||||
this.name = name;
|
||||
this.cost = cost;
|
||||
}
|
||||
|
||||
public setPrecondition(conditionName: string, value: boolean){
|
||||
this._preConditions.add([conditionName, value]);
|
||||
}
|
||||
|
||||
public setPostcondition(conditionName: string, value: boolean){
|
||||
this._preConditions.add([conditionName, value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在ActionPlanner进行plan之前调用。让Action有机会设置它的分数,或者在没有用的情况下选择退出。
|
||||
* 例如,如果Action是要拿起一把枪,但世界上没有枪,返回false将使Action不被ActionPlanner考虑
|
||||
*/
|
||||
public validate(): boolean{
|
||||
return true;
|
||||
}
|
||||
|
||||
public toString(): string{
|
||||
return `[Action] ${this.name} - cost: ${this.cost}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
source/src/AI/Pathfinding/GOAP/ActionPlanner.ts
Normal file
129
source/src/AI/Pathfinding/GOAP/ActionPlanner.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
module es {
|
||||
export class ActionPlanner {
|
||||
public static readonly MAX_CONDITIONS: number = 64;
|
||||
|
||||
/**
|
||||
* 与所有世界状态原子相关的名称
|
||||
*/
|
||||
public conditionNames: string[] = new Array(ActionPlanner.MAX_CONDITIONS);
|
||||
|
||||
public _actions: Action[] = [];
|
||||
public _viableActions: Action[] = [];
|
||||
|
||||
/**
|
||||
* 所有行动的前提条件
|
||||
*/
|
||||
public _preConditions: WorldState[] = new Array(ActionPlanner.MAX_CONDITIONS);
|
||||
|
||||
/**
|
||||
* 所有行动的后置条件(行动效果)
|
||||
*/
|
||||
public _postConditions: WorldState[] = new Array(ActionPlanner.MAX_CONDITIONS);
|
||||
|
||||
/**
|
||||
* 世界状态原子的数量
|
||||
*/
|
||||
public _numConditionNames: number;
|
||||
|
||||
constructor(){
|
||||
this._numConditionNames = 0;
|
||||
for (let i = 0; i < ActionPlanner.MAX_CONDITIONS; ++i){
|
||||
this.conditionNames[i] = null;
|
||||
this._preConditions[i] = WorldState.create(this);
|
||||
this._postConditions[i] = WorldState.create(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取世界状态对象的便利方法
|
||||
*/
|
||||
public createWorldState(): WorldState {
|
||||
return WorldState.create(this);
|
||||
}
|
||||
|
||||
public addAction(action: Action){
|
||||
let actionId = this.findActionIndex(action);
|
||||
if (actionId == -1)
|
||||
throw new Error("无法找到或创建行动");
|
||||
|
||||
action._preConditions.forEach((preCondition)=>{
|
||||
let conditionId = this.findConditionNameIndex(preCondition[0]);
|
||||
if (conditionId == -1)
|
||||
throw new Error("无法找到或创建条件名称");
|
||||
|
||||
this._preConditions[actionId].set(conditionId, preCondition[1]);
|
||||
});
|
||||
|
||||
action._postConditions.forEach((postCondition)=>{
|
||||
let conditionId = this.findConditionNameIndex(postCondition[0]);
|
||||
if (conditionId == -1)
|
||||
throw new Error("找不到条件名称");
|
||||
|
||||
this._postConditions[actionId].set(conditionId, postCondition[1]);
|
||||
});
|
||||
}
|
||||
|
||||
public plan(startState: WorldState, goalState: WorldState, selectedNode = null){
|
||||
this._viableActions.length = 0;
|
||||
for (let i = 0; i < this._actions.length; i++){
|
||||
if (this._actions[i].validate())
|
||||
this._viableActions.push(this._actions[i]);
|
||||
}
|
||||
|
||||
return AStar.plan(this, startState, goalState, selectedNode);
|
||||
}
|
||||
|
||||
public getPossibleTransitions(fr: WorldState){
|
||||
let result = ListPool.obtain<AStarNode>();
|
||||
for (let i = 0; i < this._viableActions.length; ++i){
|
||||
let pre = this._preConditions[i];
|
||||
let care = (pre.dontCare ^ -1);
|
||||
let met = ((pre.values & care) == (fr.values & care));
|
||||
if (met){
|
||||
let node = Pool.obtain<AStarNode>(AStarNode);
|
||||
node.action = this._viableActions[i];
|
||||
node.costSoFar = this._viableActions[i].cost;
|
||||
node.worldState = this.applyPostConditions(this, i, fr);
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public applyPostConditions(ap: ActionPlanner, actionnr: number, fr: WorldState){
|
||||
let pst = ap._postConditions[actionnr];
|
||||
let unaffected = pst.dontCare;
|
||||
let affected = (unaffected ^ -1);
|
||||
|
||||
fr.values = (fr.values & unaffected) | (pst.values & affected);
|
||||
fr.dontCare &= pst.dontCare;
|
||||
return fr;
|
||||
}
|
||||
|
||||
public findConditionNameIndex(conditionName: string){
|
||||
let idx;
|
||||
for (idx = 0; idx < this._numConditionNames; ++idx){
|
||||
if (this.conditionNames[idx] == conditionName)
|
||||
return idx;
|
||||
}
|
||||
|
||||
if (idx < ActionPlanner.MAX_CONDITIONS - 1){
|
||||
this.conditionNames[idx] = conditionName;
|
||||
this._numConditionNames ++;
|
||||
return idx;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public findActionIndex(action: Action): number{
|
||||
let idx = this._actions.indexOf(action);
|
||||
if (idx > -1)
|
||||
return idx;
|
||||
|
||||
this._actions.push(action);
|
||||
return this._actions.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
76
source/src/AI/Pathfinding/GOAP/WorldState.ts
Normal file
76
source/src/AI/Pathfinding/GOAP/WorldState.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
module es {
|
||||
export class WorldState implements IEquatable<WorldState> {
|
||||
/**
|
||||
* 我们使用条件索引上的位掩码移位来翻转位。
|
||||
*/
|
||||
public values: number;
|
||||
|
||||
/**
|
||||
* 比特掩码用于明确表示false。
|
||||
* 我们需要一个单独的负值存储空间,因为一个值的缺失并不一定意味着它是假的
|
||||
*/
|
||||
public dontCare: number;
|
||||
|
||||
/**
|
||||
* 是必需的,这样我们就可以从字符串名称中获取条件索引
|
||||
*/
|
||||
public planner: ActionPlanner;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param planner
|
||||
*/
|
||||
public static create(planner: ActionPlanner): WorldState {
|
||||
return new WorldState(planner, 0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param planner
|
||||
* @param values
|
||||
* @param dontcare
|
||||
*/
|
||||
constructor(planner: ActionPlanner, values: number, dontcare: number){
|
||||
this.planner = planner;
|
||||
this.values = values;
|
||||
this.dontCare = dontcare;
|
||||
}
|
||||
|
||||
public set(conditionId: number, value: boolean): boolean {
|
||||
this.values = value ? (this.values | (1 << conditionId)) : (this.values & ~(1 << conditionId));
|
||||
this.dontCare ^= (1 << conditionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param other
|
||||
*/
|
||||
public equals(other: WorldState): boolean {
|
||||
let care = this.dontCare ^ -1;
|
||||
return (this.values & care) == (other.values & care);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于调试目的。提供一个包含所有前提条件的可读字符串
|
||||
* @param planner
|
||||
*/
|
||||
public describe(planner: ActionPlanner): string {
|
||||
let s = "";
|
||||
for (let i = 0; i < ActionPlanner.MAX_CONDITIONS; i ++){
|
||||
if ((this.dontCare & (1 << i)) == 0){
|
||||
let val = planner.conditionNames[i];
|
||||
if (val == null)
|
||||
continue;
|
||||
|
||||
let set = ((this.values & (1 << i)) != 0);
|
||||
if (s.length > 0)
|
||||
s += ", ";
|
||||
s += (set ? val.toUpperCase() : val);
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
317
source/src/ECS/Utils/FastDirectory.ts
Normal file
317
source/src/ECS/Utils/FastDirectory.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
module es {
|
||||
/**
|
||||
* 创建这个字典的原因只有一个:
|
||||
* 我需要一个能让我直接以数组的形式对值进行迭代的字典,而不需要生成一个数组或使用迭代器。
|
||||
* 对于这个目标是比标准字典快N倍。
|
||||
* Faster dictionary在大部分操作上也比标准字典快,但差别可以忽略不计。
|
||||
* 唯一较慢的操作是在添加时调整内存大小,因为与标准数组相比,这个实现需要使用两个单独的数组。
|
||||
*/
|
||||
export class FasterDictionary<TKey, TValue> {
|
||||
public _values: TValue[];
|
||||
public _valuesInfo: FastNode[];
|
||||
public _buckets: number[];
|
||||
public _freeValueCellIndex: number = 0;
|
||||
public _collisions: number = 0;
|
||||
|
||||
constructor(size: number = 1) {
|
||||
this._valuesInfo = new Array(size);
|
||||
this._values = new Array(size);
|
||||
this._buckets = new Array(HashHelpers.getPrime(size));
|
||||
}
|
||||
|
||||
public getValuesArray(count: {value: number}): TValue[] {
|
||||
count.value = this._freeValueCellIndex;
|
||||
|
||||
return this._values;
|
||||
}
|
||||
|
||||
public get valuesArray(): TValue[] {
|
||||
return this._values;
|
||||
}
|
||||
|
||||
public get count(): number {
|
||||
return this._freeValueCellIndex;
|
||||
}
|
||||
|
||||
public add(key: TKey, value: TValue) {
|
||||
if (!this.addValue(key, value, {value: 0}))
|
||||
throw new Error("key 已经存在")
|
||||
}
|
||||
|
||||
public addValue(key: TKey, value: TValue, indexSet: {value: number}) {
|
||||
let hash = HashHelpers.getHashCode(key);
|
||||
let bucketIndex = FasterDictionary.reduce(hash, this._buckets.length);
|
||||
|
||||
if (this._freeValueCellIndex == this._values.length) {
|
||||
let expandPrime = HashHelpers.expandPrime(this._freeValueCellIndex);
|
||||
|
||||
this._values.length = expandPrime;
|
||||
this._valuesInfo.length = expandPrime;
|
||||
}
|
||||
|
||||
// buckets值-1表示它是空的
|
||||
let valueIndex = NumberExtension.toNumber(this._buckets[bucketIndex]) - 1;
|
||||
|
||||
if (valueIndex == -1) {
|
||||
// 在最后一个位置创建信息节点,并填入相关信息
|
||||
this._valuesInfo[this._freeValueCellIndex] = new FastNode(key, hash);
|
||||
} else {
|
||||
{
|
||||
let currentValueIndex = valueIndex;
|
||||
do {
|
||||
// 必须检查键是否已经存在于字典中
|
||||
if (this._valuesInfo[currentValueIndex].hashcode == hash &&
|
||||
this._valuesInfo[currentValueIndex].key == key) {
|
||||
// 键已经存在,只需将其值替换掉即可
|
||||
this._values[currentValueIndex] = value;
|
||||
indexSet.value = currentValueIndex;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentValueIndex = this._valuesInfo[currentValueIndex].previous;
|
||||
}
|
||||
while (currentValueIndex != -1); // -1表示没有更多的值与相同的哈希值的键
|
||||
}
|
||||
|
||||
this._collisions++;
|
||||
// 创建一个新的节点,该节点之前的索引指向当前指向桶的节点
|
||||
this._valuesInfo[this._freeValueCellIndex] = new FastNode(key, hash, valueIndex);
|
||||
// 更新现有单元格的下一个单元格指向新的单元格,旧的单元格 -> 新的单元格 -> 旧的单元格 <- 下一个单元格
|
||||
this._valuesInfo[valueIndex].next = this._freeValueCellIndex;
|
||||
}
|
||||
// 重要的是:新的节点总是被桶单元格指向的那个节点,所以我可以假设被桶指向的那个节点总是最后添加的值(next = -1)
|
||||
// item与这个bucketIndex将指向最后创建的值
|
||||
// TODO: 如果相反,我假设原来的那个是bucket中的那个,我就不需要在这里更新bucket了
|
||||
this._buckets[bucketIndex] = (this._freeValueCellIndex + 1);
|
||||
|
||||
this._values[this._freeValueCellIndex] = value;
|
||||
indexSet.value = this._freeValueCellIndex;
|
||||
|
||||
this._freeValueCellIndex++;
|
||||
|
||||
if (this._collisions > this._buckets.length) {
|
||||
// 我们需要更多的空间和更少的碰撞
|
||||
this._buckets = new Array(HashHelpers.expandPrime(this._collisions));
|
||||
this._collisions = 0;
|
||||
// 我们需要得到目前存储的所有值的哈希码,并将它们分布在新的桶长上
|
||||
for (let newValueIndex = 0; newValueIndex < this._freeValueCellIndex; newValueIndex++) {
|
||||
// 获取原始哈希码,并根据新的长度找到新的bucketIndex
|
||||
bucketIndex = FasterDictionary.reduce(this._valuesInfo[newValueIndex].hashcode, this._buckets.length);
|
||||
// bucketsIndex可以是-1或下一个值。
|
||||
// 如果是-1意味着没有碰撞。
|
||||
// 如果有碰撞,我们创建一个新节点,它的上一个指向旧节点。
|
||||
// 旧节点指向新节点,新节点指向旧节点,旧节点指向新节点,现在bucket指向新节点,这样我们就可以重建linkedlist.
|
||||
// 获取当前值Index,如果没有碰撞,则为-1。
|
||||
let existingValueIndex = NumberExtension.toNumber(this._buckets[bucketIndex]) - 1;
|
||||
// 将bucket索引更新为共享bucketIndex的当前项目的索引(最后找到的总是bucket中的那个)
|
||||
this._buckets[bucketIndex] = newValueIndex + 1;
|
||||
if (existingValueIndex != -1) {
|
||||
// 这个单元格已经指向了新的bucket list中的一个值,这意味着有一个碰撞,出了问题
|
||||
this._collisions++;
|
||||
// bucket将指向这个值,所以新的值将使用以前的索引
|
||||
this._valuesInfo[newValueIndex].previous = existingValueIndex;
|
||||
this._valuesInfo[newValueIndex].next = -1;
|
||||
// 并将之前的下一个索引更新为新的索引
|
||||
this._valuesInfo[existingValueIndex].next = newValueIndex;
|
||||
} else {
|
||||
// 什么都没有被索引,桶是空的。我们需要更新之前的 next 和 previous 的值。
|
||||
this._valuesInfo[newValueIndex].next = -1;
|
||||
this._valuesInfo[newValueIndex].previous = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public remove(key: TKey): boolean {
|
||||
let hash = FasterDictionary.hash(key);
|
||||
let bucketIndex = FasterDictionary.reduce(hash, this._buckets.length);
|
||||
|
||||
// 找桶
|
||||
let indexToValueToRemove = NumberExtension.toNumber(this._buckets[bucketIndex]) - 1;
|
||||
|
||||
// 第一部分:在bucket list中寻找实际的键,如果找到了,我就更新bucket list,使它不再指向要删除的单元格。
|
||||
while (indexToValueToRemove != -1) {
|
||||
if (this._valuesInfo[indexToValueToRemove].hashcode == hash &&
|
||||
this._valuesInfo[indexToValueToRemove].key == key) {
|
||||
// 如果找到了密钥,并且桶直接指向了要删除的节点
|
||||
if (this._buckets[bucketIndex] - 1 == indexToValueToRemove){
|
||||
if (this._valuesInfo[indexToValueToRemove].next != -1)
|
||||
throw new Error("如果 bucket 指向单元格,那么 next 必须不存在。");
|
||||
|
||||
// 如果前一个单元格存在,它的下一个指针必须被更新!
|
||||
//<---迭代顺序
|
||||
// B(ucket总是指向最后一个)
|
||||
// ------- ------- -------
|
||||
// 1 | | | | 2 | | | 3 | //bucket不能有下一个,只能有上一个。
|
||||
// ------- ------- -------
|
||||
//--> 插入
|
||||
let value = this._valuesInfo[indexToValueToRemove].previous;
|
||||
this._buckets[bucketIndex] = value + 1;
|
||||
}else{
|
||||
if (this._valuesInfo[indexToValueToRemove].next == -1)
|
||||
throw new Error("如果 bucket 指向另一个单元格,则 NEXT 必须存在");
|
||||
}
|
||||
|
||||
FasterDictionary.updateLinkedList(indexToValueToRemove, this._valuesInfo);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
indexToValueToRemove = this._valuesInfo[indexToValueToRemove].previous;
|
||||
}
|
||||
|
||||
if (indexToValueToRemove == -1)
|
||||
return false; // 未找到
|
||||
|
||||
this._freeValueCellIndex --; // 少了一个需要反复计算的值
|
||||
|
||||
// 第二部分
|
||||
// 这时节点指针和水桶会被更新,但_values数组会被更新仍然有要删除的值
|
||||
// 这个字典的目标是能够做到像数组一样对数值进行迭代,所以数值数组必须始终是最新的
|
||||
|
||||
// 如果要删除的单元格是列表中的最后一个,我们可以执行较少的操作(不需要交换),否则我们要将最后一个值的单元格移到要删除的值上。
|
||||
if (indexToValueToRemove != this._freeValueCellIndex){
|
||||
// 我们可以将两个数组的最后一个值移到要删除的数组中。
|
||||
// 为了做到这一点,我们需要确保 bucket 指针已经更新了
|
||||
// 首先我们在桶列表中找到指向要移动的单元格的指针的索引
|
||||
let movingBucketIndex = FasterDictionary.reduce(this._valuesInfo[this._freeValueCellIndex].hashcode, this._buckets.length);
|
||||
|
||||
// 如果找到了键,并且桶直接指向要删除的节点,现在必须指向要移动的单元格。
|
||||
if (this._buckets[movingBucketIndex] - 1 == this._freeValueCellIndex)
|
||||
this._buckets[movingBucketIndex] = (indexToValueToRemove + 1);
|
||||
|
||||
// 否则意味着有多个键具有相同的哈希值(碰撞),所以我们需要更新链接列表和它的指针
|
||||
let next = this._valuesInfo[this._freeValueCellIndex].next;
|
||||
let previous = this._valuesInfo[this._freeValueCellIndex].previous;
|
||||
|
||||
// 现在它们指向最后一个值被移入的单元格
|
||||
if (next != -1)
|
||||
this._valuesInfo[next].previous = indexToValueToRemove;
|
||||
if (previous != -1)
|
||||
this._valuesInfo[previous].next = indexToValueToRemove;
|
||||
|
||||
// 最后,实际上是移动值
|
||||
this._valuesInfo[indexToValueToRemove] = this._valuesInfo[this._freeValueCellIndex];
|
||||
this._values[indexToValueToRemove] = this._values[this._freeValueCellIndex];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public trim(){
|
||||
let expandPrime = HashHelpers.expandPrime(this._freeValueCellIndex);
|
||||
|
||||
if (expandPrime < this._valuesInfo.length){
|
||||
this._values.length = expandPrime;
|
||||
this._valuesInfo.length = expandPrime;
|
||||
}
|
||||
}
|
||||
|
||||
public clear(){
|
||||
if (this._freeValueCellIndex == 0) return;
|
||||
|
||||
this._freeValueCellIndex = 0;
|
||||
this._buckets.length = 0;
|
||||
this._values.length = 0;
|
||||
this._valuesInfo.length = 0;
|
||||
}
|
||||
|
||||
public fastClear(){
|
||||
if (this._freeValueCellIndex == 0) return;
|
||||
|
||||
this._freeValueCellIndex = 0;
|
||||
|
||||
this._buckets.length = 0;
|
||||
this._valuesInfo.length = 0;
|
||||
}
|
||||
|
||||
public containsKey(key: TKey){
|
||||
if (this.tryFindIndex(key, {value: 0})){
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public tryGetValue(key: TKey): TValue {
|
||||
let findIndex = {value: 0};
|
||||
if (this.tryFindIndex(key, findIndex)){
|
||||
return this._values[findIndex.value];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public tryFindIndex(key: TKey, findIndex: {value: number}){
|
||||
// 我把所有的索引都用偏移量+1来存储,这样在bucket list中0就意味着实际上不存在
|
||||
// 当读取时,偏移量必须再偏移-1才是真实的
|
||||
// 这样我就避免了将数组初始化为-1
|
||||
let hash = FasterDictionary.hash(key);
|
||||
let bucketIndex = FasterDictionary.reduce(hash, this._buckets.length);
|
||||
|
||||
let valueIndex = NumberExtension.toNumber(this._buckets[bucketIndex]) - 1;
|
||||
|
||||
// 即使我们找到了一个现有的值,我们也需要确定它是我们所要求的值
|
||||
while (valueIndex != -1){
|
||||
if (this._valuesInfo[valueIndex].hashcode == hash && this._valuesInfo[valueIndex].key == key){
|
||||
findIndex.value = valueIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
valueIndex = this._valuesInfo[valueIndex].previous;
|
||||
}
|
||||
|
||||
findIndex.value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public getDirectValue(index: number): TValue {
|
||||
return this._values[index];
|
||||
}
|
||||
|
||||
public getIndex(key: TKey): number {
|
||||
let findIndex = {value: 0};
|
||||
if (this.tryFindIndex(key, findIndex))
|
||||
return findIndex.value;
|
||||
|
||||
throw new Error("未找到key");
|
||||
}
|
||||
|
||||
public static updateLinkedList(index: number, valuesInfo: FastNode[]){
|
||||
let next = valuesInfo[index].next;
|
||||
let previous = valuesInfo[index].previous;
|
||||
|
||||
if (next != -1)
|
||||
valuesInfo[next].previous = previous;
|
||||
if (previous != -1)
|
||||
valuesInfo[previous].next = next;
|
||||
}
|
||||
|
||||
public static hash(key) {
|
||||
return HashHelpers.getHashCode(key);
|
||||
}
|
||||
|
||||
public static reduce(x: number, n: number) {
|
||||
if (x >= n)
|
||||
return x % n;
|
||||
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
export class FastNode {
|
||||
readonly key;
|
||||
readonly hashcode: number;
|
||||
previous: number;
|
||||
next: number;
|
||||
|
||||
constructor(key, hash: number, previousNode: number = -1) {
|
||||
this.key = key;
|
||||
this.hashcode = hash;
|
||||
this.previous = previousNode;
|
||||
this.next = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
source/src/ECS/Utils/HashHelper.ts
Normal file
90
source/src/ECS/Utils/HashHelper.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
module es {
|
||||
export class HashHelpers {
|
||||
public static readonly hashCollisionThreshold: number = 100;
|
||||
public static readonly hashPrime: number = 101;
|
||||
|
||||
/**
|
||||
* 用来作为哈希表大小的质数表。
|
||||
* 一个典型的调整大小的算法会在这个数组中选取比之前容量大两倍的最小质数。
|
||||
* 假设我们的Hashtable当前的容量为x,并且添加了足够多的元素,因此需要进行大小调整。
|
||||
* 调整大小首先计算2x,然后在表中找到第一个大于2x的质数,即如果质数的顺序是p_1,p_2,...,p_i,...,则找到p_n,使p_n-1 < 2x < p_n。
|
||||
* 双倍对于保持哈希特操作的渐近复杂度是很重要的,比如添加。
|
||||
* 拥有一个质数可以保证双倍哈希不会导致无限循环。 IE,你的哈希函数将是h1(key)+i*h2(key),0 <= i < size.h2和size必须是相对质数。
|
||||
*/
|
||||
public static readonly primes = [3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
|
||||
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
|
||||
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
|
||||
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
|
||||
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369];
|
||||
|
||||
/**
|
||||
* 这是比Array.MaxArrayLength小的最大质数
|
||||
*/
|
||||
public static readonly maxPrimeArrayLength = 0x7FEFFFFD;
|
||||
|
||||
public static isPrime(candidate: number): boolean {
|
||||
if ((candidate & 1) != 0){
|
||||
let limit = Math.sqrt(candidate);
|
||||
for (let divisor = 3; divisor <= limit; divisor += 2){
|
||||
if ((candidate & divisor) == 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return (candidate == 2);
|
||||
}
|
||||
|
||||
public static getPrime(min: number): number{
|
||||
if (min < 0)
|
||||
throw new Error("参数错误 min不能小于0");
|
||||
|
||||
for (let i = 0; i < this.primes.length; i ++){
|
||||
let prime = this.primes[i];
|
||||
if (prime >= min) return prime;
|
||||
}
|
||||
|
||||
// 在我们预定义的表之外,计算的方式稍复杂。
|
||||
for (let i = (min | 1); i < Number.MAX_VALUE; i += 2){
|
||||
if (this.isPrime(i) && ((i - 1) % this.hashPrime != 0))
|
||||
return i;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param oldSize
|
||||
* @returns 返回要增长的哈希特表的大小
|
||||
*/
|
||||
public static expandPrime(oldSize: number): number {
|
||||
let newSize = 2 * oldSize;
|
||||
|
||||
// 在遇到容量溢出之前,允许哈希特表增长到最大可能的大小
|
||||
// 请注意,即使当_items.Length溢出时,这项检查也会起作用
|
||||
if (newSize > this.maxPrimeArrayLength && this.maxPrimeArrayLength > oldSize){
|
||||
return this.maxPrimeArrayLength;
|
||||
}
|
||||
|
||||
return this.getPrime(newSize);
|
||||
}
|
||||
|
||||
public static getHashCode(str){
|
||||
let s;
|
||||
if (typeof str == 'object'){
|
||||
s = JSON.stringify(str);
|
||||
} else {
|
||||
s = str.toString();
|
||||
}
|
||||
|
||||
let hash = 0;
|
||||
if (s.length == 0) return hash;
|
||||
for (let i = 0; i < s.length; i ++){
|
||||
let char = s.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
147
source/src/Graphics/Textures/GaussianBlur.ts
Normal file
147
source/src/Graphics/Textures/GaussianBlur.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
module es {
|
||||
export class GaussianBlur {
|
||||
/**
|
||||
* 创建一个新的纹理,它是原始纹理的高斯模糊版本
|
||||
* @param image
|
||||
* @param deviation 偏差
|
||||
* @returns 模糊的纹理
|
||||
*/
|
||||
public static createBlurredTexture(image: egret.Texture, deviation: number = 1) {
|
||||
let pixelData = image.getPixels(0, 0, image.textureWidth, image.textureHeight);
|
||||
let srcData: Color[] = new Array(image.textureWidth * image.textureHeight);
|
||||
for (let i = 0; i < image.textureWidth; i++) {
|
||||
for (let j = 0; j < image.textureHeight; j++) {
|
||||
let width = image.textureWidth;
|
||||
let r = pixelData[i * 4 + j * width];
|
||||
let g = pixelData[i * 4 + j * width + 1];
|
||||
let b = pixelData[i * 4 + j * width + 2];
|
||||
let a = pixelData[i * 4 + j * width + 3];
|
||||
srcData[i + j * width] = new Color(r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 计算方式有问题 后面再研究研究
|
||||
let destData = this.createBlurredTextureData(srcData, image.textureWidth, image.textureHeight, deviation);
|
||||
let arrayBuffer = new ArrayBuffer(destData.length);
|
||||
destData.forEach((value, index) => {
|
||||
arrayBuffer[index] = value.packedValue;
|
||||
});
|
||||
egret.BitmapData.create("arraybuffer", arrayBuffer, (bitmapData) => {
|
||||
// TODO: 生成bitmapdata
|
||||
});
|
||||
}
|
||||
|
||||
public static createBlurredTextureData(srcData: Color[], width: number, height: number, deviation: number = 1) {
|
||||
let matrixR = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
let matrixG = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
let matrixB = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
let matrixA = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
|
||||
let destData: Color[] = new Array(width * height);
|
||||
|
||||
// 首先,我们计算出灰度,并将其存储在矩阵中
|
||||
for (let i = 0; i < width; i++) {
|
||||
for (let j = 0; j < height; j++) {
|
||||
matrixR.add({ x: i, y: j }, srcData[i + j * width].r);
|
||||
matrixG.add({ x: i, y: j }, srcData[i + j * width].g);
|
||||
matrixB.add({ x: i, y: j }, srcData[i + j * width].b);
|
||||
matrixA.add({ x: i, y: j }, srcData[i + j * width].a);
|
||||
}
|
||||
}
|
||||
|
||||
matrixR = this.gaussianConvolution(matrixR, deviation);
|
||||
matrixG = this.gaussianConvolution(matrixG, deviation);
|
||||
matrixB = this.gaussianConvolution(matrixB, deviation);
|
||||
matrixA = this.gaussianConvolution(matrixA, deviation);
|
||||
|
||||
for (let i = 0; i < width; i++) {
|
||||
for (let j = 0; j < height; j++) {
|
||||
let r = Math.min(255, matrixR.tryGetValue({x: i, y: j}));
|
||||
let g = Math.min(255, matrixG.tryGetValue({x: i, y: j}));
|
||||
let b = Math.min(255, matrixB.tryGetValue({x: i, y: j}));
|
||||
let a = Math.min(255, matrixA.tryGetValue({x: i, y: j}));
|
||||
destData[i + j * width] = new Color(r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
return destData;
|
||||
}
|
||||
|
||||
public static gaussianConvolution(matrix: FasterDictionary<{ x: number, y: number }, number>, deviation: number) {
|
||||
let kernel = this.calculateNormalized1DSampleKernel(deviation);
|
||||
let res1 = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
let res2 = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
|
||||
for (let i = 0; i < matrix._valuesInfo.length; i++) {
|
||||
for (let j = 0; j < matrix.valuesArray.length; j++)
|
||||
res1.add({ x: i, y: j }, this.processPoint(matrix, i, j, kernel, 0));
|
||||
}
|
||||
|
||||
for (let i = 0; i < matrix._valuesInfo.length; i++) {
|
||||
for (let j = 0; j < matrix.valuesArray.length; j++)
|
||||
res2.add({ x: i, y: j }, this.processPoint(res1, i, j, kernel, 1));
|
||||
}
|
||||
|
||||
return res2;
|
||||
}
|
||||
|
||||
public static processPoint(matrix: FasterDictionary<{ x: number, y: number }, number>,
|
||||
x: number,
|
||||
y: number,
|
||||
kernel: FasterDictionary<{ x: number, y: number }, number>,
|
||||
direction: number) {
|
||||
let res = 0;
|
||||
let half = kernel._valuesInfo.length / 2;
|
||||
for (let i = 0; i < kernel._valuesInfo.length; i++) {
|
||||
let cox = direction == 0 ? x + i - half : x;
|
||||
let coy = direction == 1 ? y + i - half : y;
|
||||
if (cox >= 0 && cox < matrix._valuesInfo.length && coy >= 0 && coy < matrix.valuesArray.length)
|
||||
res += matrix.tryGetValue({ x: cox, y: coy }) * kernel.tryGetValue({x: i, y: 0});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public static calculate1DSampleKernel(deviation: number) {
|
||||
let size = Math.ceil(deviation * 3) * 3 + 1;
|
||||
return this.calculate1DSampleKernelOfSize(deviation, size);
|
||||
}
|
||||
|
||||
public static calculate1DSampleKernelOfSize(deviation: number, size: number) {
|
||||
let ret = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
// let sum = 0;
|
||||
|
||||
let half = (size - 1) / 2;
|
||||
for (let i = 0; i < size; i++) {
|
||||
ret.add({x: i, y: 0}, 1 / (Math.sqrt(2 * Math.PI) * deviation) * Math.exp(-(i - half) * (i - half) / (2 * deviation * deviation)));
|
||||
// sum += ret.tryGetValue({x: i, y: 0});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static calculateNormalized1DSampleKernel(deviation: number) {
|
||||
return this.normalizeMatrix(this.calculate1DSampleKernel(deviation));
|
||||
}
|
||||
|
||||
public static normalizeMatrix(matrix: FasterDictionary<{ x: number, y: number }, number>) {
|
||||
let ret = new FasterDictionary<{ x: number, y: number }, number>();
|
||||
let sum = 0;
|
||||
for (let i = 0; i < ret._valuesInfo.length; i++) {
|
||||
for (let j = 0; j < ret.valuesArray.length; j++) {
|
||||
sum += matrix.tryGetValue({x: i, y: j});
|
||||
}
|
||||
}
|
||||
|
||||
if (sum != 0) {
|
||||
for (let i = 0; i < ret._valuesInfo.length; i++) {
|
||||
for (let j = 0; j < ret.valuesArray.length; j++) {
|
||||
ret.add({x: i, y: j}, matrix.tryGetValue({x: i, y: j}) / sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
source/src/Utils/NumberExtension.ts
Normal file
9
source/src/Utils/NumberExtension.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
module es {
|
||||
export class NumberExtension {
|
||||
public static toNumber(value){
|
||||
if (value == undefined) return 0;
|
||||
|
||||
return Number(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user