引力效果[1] - 分组计算引力

说明

这里用了120颗粒子来实现,如果每个粒子之间都计算一次引力的话,得进行 n(n-1)次计算。120颗粒子计算一次引力,就是14280次计算,再加上页面是每秒30帧进行渲染,那就是120*119*30=428400次计算。1s之内如果做这么多次运算,最终的效果可想而知。因此用了一个分格的一个想法,如果每次只检查相邻的格子里的粒子,数量会少很多。

640320 的舞台,每2020作为一格的话,就是32*16=512格,如果每次只检查相邻8个方向的格子,一共才8格。
而我一共120个粒子,平均每个粒子,需要与多少粒子参与引力计算呢: 120*8/512=1.875
只与1.875个粒子参与计算,那把所有粒子算一遍,只需要120*1.875=225次计算,14280次与225次,性能差别很明显哦。当然,如果后续有更优秀的计算方法,我会继续更新的。

本篇先实现了计算性能的处理,后续继续完善效果

1
2
3
<div id="preview-box" style="text-align:center"></div>
<button id="start">start</button>
<button id="stop">stop</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// 简单的数组随机取值函数
function random(arr) {
return arr[Math.floor(Math.random() * arr.length)]
}

document.getElementById('start').onclick = () => PIXI.Ticker.shared.start()
document.getElementById('stop').onclick = () => PIXI.Ticker.shared.stop()

let game = new PIXI.Application({
width: 640,
height: 320,
backgroundColor: 0x333333
})
document.getElementById('preview-box').appendChild(game.view)
game.view.style.width = "480px"
game.view.style.maxWidth = "100%"

let width = game.renderer.width
let height = game.renderer.height

// 舞台分割粒度
let cellSize = 20
// 二维数组 y做为第1维度,x作为第2维度
let team = []
// 列数
let colNum = Math.ceil(width / cellSize)
// 行数
let rowNum = Math.ceil(height / cellSize)
// 格子数
let boxNum = 0
// 总粒子数
let pNum = 120
// 总运行帧数 可以在达到一定值之后置0
let frame = 0
// 引力影响格子数 R=1 则影响包括自身格子的 9格 (1 + 2R)^2
// 也就是说 认为这些距离的粒子才会对当前粒子产生引力效果 虽然不严谨 但是考虑到计算性能 只能折中考虑
let R = 1

let container = new PIXI.ParticleContainer(pNum)
game.stage.addChild(container)

// 预先准备格子
for (let x = 0; x < colNum; x++) {
let col = []
for (let y = 0; y < rowNum; y++) {
let box = {}
boxNum++
col.push(box)
}
team.push(col)
}

// 创建粒子
for (let n = 0; n < pNum; n++) {
let px = Math.random() * width
let py = Math.random() * height
let col = ~~(px / cellSize)
let row = ~~(py / cellSize)

// 粒子基本属性
let p = new PIXI.Sprite(PIXI.Texture.WHITE)
p.name = n;
p.x = px;
p.y = py;
p.anchor.set(0.5)
p.rotation = Math.PI/4
p.tint = 0x91ff00
p.alpha = .45
// 附加属性 可以考虑用WeakMap存储 到时候做个对比,看性能差异
// 行列数
p.col = col;
p.row = row;
// 质量
p.m = 2
p.scale.set(.3)
// 速度
p.vx = (Math.random() - 0.5)
p.vy = (Math.random() - 0.5)
// 加速度
p.ax = 0
p.ay = 0

team[col][row][n] = p;
container.addChild(p)
}

// 计算行动一次的时间
let time = 1;
// 定时器
// 舞台等分为 多少格,然后粒子每次运动结束,记录当前属于第几个格子,移动到对应的数组存储
PIXI.Ticker.shared.add(() => {
if (frame % 1 === 0) {
container.children.forEach(p => {
gravityCalc(p, R)

let fcol = ~~(p.x / cellSize)
let frow = ~~(p.y / cellSize)

let tx = p.x + p.vx
let ty = p.y + p.vy

// 计算碰撞反弹 模拟摩擦力为0 能量无损耗的理想环境
if (tx < p.width / 2 || tx > width - p.width / 2) {
p.vx *= -1
p.x = tx < p.width / 2 ? p.width / 2 : width - p.width / 2
} else {
p.x += p.vx
}

if (ty < p.height / 2 || ty > height - p.height / 2) {
p.vy *= -1
p.y = ty < p.height / 2 ? p.height / 2 : height - p.height / 2
} else {
p.y += p.vy
}

// 移动完毕 更新所在的格子
let tcol = ~~(p.x / cellSize)
let trow = ~~(p.y / cellSize)
if (fcol !== tcol || frow !== trow) {
p.col = tcol;
p.row = trow;
delete team[fcol][frow][p.name]
team[tcol][trow][p.name] = p;
}
});
}
frame++;
})

// 每个粒子检查周边 (1+2R)^2个 格子
function gravityCalc(p, range) {
let { col: centerCol, row: centerRow } = p;
let g = 0.9;

for (let x = 0; x < range * 2 + 1; x++) {
for (let y = 0; y < range * 2 + 1; y++) {
let
testCol = centerCol - range + x,
testRow = centerRow - range + y;

// 非法行列数 目标盒子不存在
if (testCol < 0 || testCol >= colNum || testRow < 0 || testRow >= rowNum) continue;
let testBox = team[testCol][testRow];

// 目标盒子没有内容
if (Object.keys(testBox).length <= 0) continue;

// 目标盒子是当前盒子 只有自己
if (testCol === centerCol && testRow === centerRow && Object.keys(testBox).length <= 1) continue;

// 剩余情况 目标盒子与自己产生引力效应,影响速度
for (let name in testBox) {
if (testBox.hasOwnProperty(name)) {

// 引力点
let tp = testBox[name];

// 求出引力点,相对于当前点p 的位置 弧度 及距离
let rad = Math.atan2((tp.x - p.x), (tp.y - p.y))
let dis = Math.pow(Math.pow(tp.x - p.x, 2) + Math.pow(tp.y - p.y, 2), .5)

// 引力公式 f = g(M*m/r^2)
let f = g * (tp.m * p.m / Math.pow(dis, 2))
f = f > 2 ? 2 : f

// xy轴的引力向量
let fx = Math.sin(rad) * f
let fy = Math.cos(rad) * f

// 加速度 收到影响
p.ax += fx / p.m
p.ay += fy / p.m
}
}
}
}
}
  1. 说明