序
この記事では、次のような一つの簡単なProcessingスケッチについて、様々なスタイルでコードを書いて比較してみます。
float[][][]d=new float[32][2][2];
— FAL @STG制作とプログラミングお絵かき (@falworks_ja) August 14, 2019
float f(float[]x){return x[0]+=((frameCount%30==0?x[1]=random(100):x[1])-x[0])/4;}
void setup(){size(800,800);fill(252,32);}
void draw(){
square(0,0,800); scale(8);
for(float[][]e:d) line(e[0][0],e[1][0],f(e[0]),f(e[1]));
}#つぶやきProcessing pic.twitter.com/055DFzHmwF
環境
- Processing 3.5.3 (Java mode)
- 文法は昔のJavaです1
- Processing 3.5.3 (Python mode)
- p5.js 0.9.0 (w/ JavaScript ES2015)
注
- パラダイムと題するには軸が不揃いである気がしたので、タイトルでは「スタイル」と表記しました。パラダイムの分類と網羅的な紹介を試みるものではございません。
- 「スタイル」というと、今度はコーディング規約などを想像されるかもしれませんが、より曖昧かつ広い意味でご了解ください。
共通仕様
まずは冒頭のスケッチをご覧ください。
黒い粒が一定周期で勢いよく動くというものです。
もう少し具体的に書くと、次のようなことを実装します。
- 32個(固定)の粒子が存在する。
各粒子について、「現在位置」と「目標位置」、またはそれらに相当する情報を記憶する仕組みがある。 - 毎フレーム、各粒子について、以下が実行される。
- 目標位置を目指して、「現在位置から目標位置までの距離」の4分の1だけ移動する
(簡易的なイージング2処理)。 - 移動前後の位置を結ぶ線を
line()
関数で表示することによって、移動の軌跡を描画する。
- 目標位置を目指して、「現在位置から目標位置までの距離」の4分の1だけ移動する
- 毎フレーム、画面全体を覆う透明度の高い四角形を描画することで、過去の描画結果を徐々に薄め、残像効果を得る。
- 30フレーム3ごとに、各粒子の目標位置を、画面内のランダムな位置に変更する。
以上は、これから書くすべてのコードに共通します。
なお、コードはGitHubにも置いています(ただしこちらはコメントなし)。
素朴型
素朴というと曖昧ですが、クラスも関数も新たに作らない方向で、Processingのサンプルコードとしてよくありそうな形を考えてみることとします。
現在位置をx
とy
、目標位置をtargetX
とtargetY
として、それらを粒子の数だけ管理できるよう、それぞれ配列にします。
float[] x = new float[32];
float[] y = new float[32];
float[] targetX = new float[32];
float[] targetY = new float[32];
void setup() {
size(800, 800);
fill(252, 32); // 下記の rect() で使われる色
strokeWeight(8); // 下記の line() で使われる線の太さ
}
void draw() {
rect(0, 0, width, height); // 残像効果のため、画面全体を塗る
// 粒子の数だけループ
for (int i = 0; i < 32; i++) {
// 30フレームに1回、目標位置をランダムに変更
if (frameCount % 30 == 0) {
targetX[i] = random(width);
targetY[i] = random(height);
}
// 移動前の位置(=現在位置)
float previousX = x[i];
float previousY = y[i];
// 移動後の位置(=現在位置と目標位置から計算)
float nextX = x[i] + (targetX[i] - x[i]) / 4;
float nextY = y[i] + (targetY[i] - y[i]) / 4;
// 移動の軌跡について線を描画
line(previousX, previousY, nextX, nextY);
// 現在位置を移動後の値で更新
x[i] = nextX;
y[i] = nextY;
}
}
素朴と書いたのはなにも悪い意味ではなく、むしろProcessingはこういうものだという感すらあります。
スクリプト型(Python)
Pythonは近年の代表的なスクリプト言語であり、読みやすく書きやすい文法を目指して作られていますが、様々な高度な用途にも使われていることはご存知の通りです。
ProcessingにもPythonモードがあり、Pythonでスケッチを書くことができます。
前項のコードをベースにしつつ、ベタ書きは先ほどやったので、簡単ですが関数も2つほど作ってみます。
また、速度や保守性はひとまずあまり気にせず、気楽に使える辞書型で粒子に位置を持たせてみます。
def change_target_position(particle):
""" 粒子の目標位置をランダムに変更 """
particle["target_x"] = random(width)
particle["target_y"] = random(height)
def update_draw_particle(particle):
""" 粒子の移動と描画 """
prev_x = particle["x"]
prev_y = particle["y"]
next_x = prev_x + (particle["target_x"] - prev_x) / 4
next_y = prev_y + (particle["target_y"] - prev_y) / 4
line(prev_x, prev_y, next_x, next_y)
particle["x"] = next_x
particle["y"] = next_y
def setup():
size(800, 800)
fill(252, 32)
strokeWeight(8)
# 上の方に書かなくてもグローバル変数を宣言できる
global particles
# 内包表記で配列を簡単に初期化できる
particles = [{"x": 0, "y": 0, "target_x": 0, "target_y": 0}
for i in range(32)]
def draw():
square(0, 0, 800)
for particle in particles:
if frameCount % 30 == 0:
change_target_position(particle)
update_draw_particle(particle)
(Python初めてなのですが果たしてこれはPython的に自然なのでしょうか)
インデントが特徴的。
そしてこれは確かに楽で良いなと感じます。
短さ重視型
冒頭のツイートのコードで、この記事を書くきっかけにもなったものです。
#つぶやきProcessing というハッシュタグで、Twitterで1ツイートに収まるソースコードでスケッチを描いてみようという遊びが流行っているのですが、短くするために頭をひねるのが思ったより面白かったので、ついでにいろいろスタイルを見比べてみようと思うに至ったのでした。
float[][][]d=new float[32][2][2];
float f(float[]x){return x[0]+=((frameCount%30==0?x[1]=random(100):x[1])-x[0])/4;}
void setup(){size(800,800);fill(252,32);}
void draw(){
square(0,0,800);scale(8);
for(float[][]e:d)line(e[0][0],e[1][0],f(e[0]),f(e[1]));
}
1ツイートというのはやってみると本当に短く、この例では次のようなことをやってみました。
- 可読性目的の空白文字をできるだけ削る
(かろうじて読めるよう、上記では少し改行とインデントを残しています) - 変数宣言を短くするため、「粒子32個」×「x座標とy座標」×「現在値と目標値」と考えて、32x2x2の多次元配列とする(ちなみに
d
がdataでe
がentityのつもり、x
はx座標y座標を問わない) -
strokeWeight()
が長いのでscale()
で代用 - inline assignment で値の取得と更新を同時に行う
(a = b += 1
とすると、b += 1
を実行してその結果をa
にも代入できる、といったものです)
通常のプログラミングでこういうことは本当にやめた方がいいのですが、言語の知識が問われることもあり、良い練習にもなるしこういったものも面白いのではないかと思います。
邪悪なコーディング作法に手を染めてしもうた……
— FAL @STG制作とプログラミングお絵かき (@falworks_ja) August 14, 2019
と一瞬思いましたが、ある文脈では邪悪とされる所作が別の文脈では美しいと言われることすらある、これを我々人類は忘れてはならないのであった
クラスベース型
簡単で短いコードは最初は楽なのですが、少し複雑なプログラムであったり、後で追加修正が予想されるようなプログラムだと、書いていくうちにどこがどうなってるのか分からなくなり、いろいろな問題が発生します。
そこで、一定の考え方のもとでプログラムを細かい部品に分け、それをうまく組み合わせながら大きなプログラムを作っていこうという方向性があります。その代表の一つが「クラス」を基盤としてこれを行おうとするスタイルです。
Javaなどはまさにそうであり、よってJavaベースであるProcessingでもクラスを使う場面が多々あります。4
部分的ながら、その考え方を今回の例に適用してみます。とりあえず以下のように考えました。
- 主役は粒子なので
Particle
クラスを作る。
粒子にまつわる諸々、つまり以下のことを、ひとまとめにしてこのクラスに任せたい。
- データ: 位置に関するもの
- 振る舞い: 移動・描画の処理、目標位置の変更処理 - 「移動(=位置の更新)」と「描画」は意味的にだいぶ異なるので、別々の機能として分けて実装すべきであろう。
- すると、移動処理を行ったあと、描画処理で使うための「移動前後の位置」を両方記憶しておく必要がある。これはデータとしては始点と終点を持つ線分に他ならない。よってこれを表す
LineSegment
クラスを作り、Particle
クラスに持たせよう。
- すると、移動処理を行ったあと、描画処理で使うための「移動前後の位置」を両方記憶しておく必要がある。これはデータとしては始点と終点を持つ線分に他ならない。よってこれを表す
- つまり
Particle
のデータは次の2つである。- 直近の移動の軌跡を表す線分(
LineSegment
クラス) - 目標位置(Processing組み込みの
PVector
クラス)
- 直近の移動の軌跡を表す線分(
- 線の描画は
LineSegment
が持つ情報だけで完結するので、実際の描画はLineSegment
に任せ、Particle
はただそれを呼び出せばよいのではないか。 - さらに、すべての粒子をグループとして管理することを任せたいので、
ParticleGroup
クラスを作る。このクラスの仕事は以下である。- 全粒子の移動
- 全粒子の描画
- 全粒子の目標位置の一斉変更(シャッフルと呼ぼう)
何やら大げさですが、結果こうなりました(ファイル分けました)。
/**
* このファイルには、粒子、およびそれに関連するクラスを集めています。
*/
import java.util.List;
import java.util.Arrays;
/** 線分。始点と終点を持ち、それらを結ぶ線を描画できる。 */
class LineSegment {
PVector startPoint;
PVector endPoint;
LineSegment() {
startPoint = new PVector();
endPoint = new PVector();
}
void draw() {
line(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
}
}
/** Particle.update() で使う定数 */
float EASING_FACTOR = 0.25;
/** 粒子。上述の通り */
class Particle {
LineSegment lineSegment; // 何か意味のある名前を付けるべきかも
PVector targetPoint;
Particle() {
lineSegment = new LineSegment();
targetPoint = new PVector();
}
/** 位置の更新処理。実装としては移動前後の位置を表す線分を更新 */
void update() {
lineSegment.startPoint.set(lineSegment.endPoint);
// 移動量を表すベクトルを計算。
// このように繋げて書くのをメソッドチェーンなどと呼ぶ
PVector displacement = targetPoint
.copy()
.sub(lineSegment.endPoint)
.mult(EASING_FACTOR);
lineSegment.endPoint.add(displacement);
}
void setTargetPosition(float x, float y) {
targetPoint.set(x, y);
}
void draw() {
lineSegment.draw();
}
}
/** 粒子グループ。全ての粒子をまとめて管理・操作する。 */
class ParticleGroup {
List<Particle> list; // いろいろな理由で、このスタイルでは配列を避けるのが一般的
ParticleGroup(int particleCount) {
final Particle[] array = new Particle[particleCount];
for (int i = 0; i < particleCount; i++)
array[i] = new Particle();
list = Arrays.asList(array);
}
void update() {
for (Particle each : list) each.update();
}
/** シャッフル。全粒子について、目標位置をランダムに再設定 */
void shuffle(float maxX, float maxY) {
for (Particle each : list)
each.setTargetPosition(random(maxX), random(maxY));
}
void draw() {
for (Particle each : list) each.draw();
}
}
/**
* メインのソースファイルです。
*/
ParticleGroup particles = new ParticleGroup(32);
void setup(){
size(800, 800);
fill(252, 32);
strokeWeight(8);
}
void draw(){
rect(0, 0, width, height);
if (frameCount % 30 == 0) particles.shuffle(width, height);
particles.update();
particles.draw();
}
恐らくこのスタイルに慣れた方からはいろいろな改善点が見えることと思いますので、もし何かあれば。
一方で、そもそも将来拡張する予定が特にないので、設計にこだわろうにも判断基準が少ないという側面もありますが……。
なお、これは一般に想像されるオブジェクト指向とも言えるものですが、カプセル化も継承もポリモーフィズムもやっていないので今回はっきりそう言っていいものか、ということでクラスベースと書きました。5
ここから、例えば粒子の見た目や動きにバリエーションを持たせたくなったとき、該当部分を別のクラスに切り出して移譲するなどして、将来の機能追加・変更に備えることができます(コンポーネント指向)。
クラスベース・不変オブジェクト利用型
前項の派生形です。
前項ではオブジェクトの中身のデータを更新することに躊躇がありませんでしたが(実際そうせざるを得ない場面も多いと思いますが)、プログラムが複雑になると、状態の変更というのはとにかくバグの温床になります。
そこで、一部のクラスについては、作ったら最後、中身を絶対に変更できないような作りにしておく(=不変オブジェクト)。値を更新したければその都度あらたなオブジェクトを生成する、というアプローチがあります。
今回の例で考えると、
- 位置を表す2次元ベクトル & その組である線分
- 本質的には
int
などと同じ、単なる値として考えるべきであり、中身をいじくって変更するようなことはできないほうが良いだろう。
- 本質的には
- 粒子 & 粒子グループ
- クラスの意味を考えると状態の変更が起こるのは自然であるから、このままでいいだろう。
という風に考えられたので、LineSegment
クラスは不変にして、可変だったPVector
クラスについても、新たに不変なImmutableVector
クラスを作って置き換えます。
また、この方針はドメイン駆動設計という考え方の中で言われている「値オブジェクト」とも共通するものです6。これは大雑把には、int
やfloat
といったプリミティブ型やその他組み込みのクラスを直接使うことは避け、用途に応じていろいろな値オブジェクトを定義して使った方が、意味的な対応が取れるし、(今回やりませんが)値のチェックや制約も任せられるので良い、というものです。
この考え方を(部分的ではありますが)ついでに適用して、メインではないもののちょこちょこ存在していたint
やfloat
を置き換えてみます。7
不変にするにあたっては、Javaの場合、変数の再代入やメソッドのオーバーライドやクラスの継承を禁止するfinal
を使いまくることになります。8
長いです。(3ファイル)
/**
* このファイルには不変クラスを集めています。
*/
/** 比率。ベクトルへの乗算に使用 */
final class Ratio {
final float value;
Ratio(float value) {
this.value = value;
}
}
/** 不変な2次元ベクトル。PVectorの代わり */
final class ImmutableVector {
final float x;
final float y;
ImmutableVector(float x, float y) {
this.x = x;
this.y = y;
}
// どんな操作をするにしても、必ず新たなオブジェクトを生成する
ImmutableVector add(ImmutableVector other) {
return new ImmutableVector(
this.x + other.x,
this.y + other.y
);
}
ImmutableVector sub(ImmutableVector other) {
return new ImmutableVector(
this.x - other.x,
this.y - other.y
);
}
ImmutableVector mult(Ratio multiplier) {
return new ImmutableVector(
multiplier.value * this.x,
multiplier.value * this.y
);
}
}
/** 前出の線分。不変バージョン */
final class LineSegment {
final ImmutableVector startPoint;
final ImmutableVector endPoint;
LineSegment(
ImmutableVector startPoint,
ImmutableVector endPoint
) {
this.startPoint = startPoint;
this.endPoint = endPoint;
}
void draw() {
line(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
}
}
/** 四角形の大きさ、すなわち幅と高さ。 */
final class RectangleSize {
final float width;
final float height;
RectangleSize(float w, float h) {
this.width = w;
this.height = h;
}
}
/** 四角形の位置&大きさ。描画機能も。rect()はここで呼ぶ */
final class Rectangle {
final ImmutableVector position;
final RectangleSize size;
Rectangle(ImmutableVector position, RectangleSize size) {
this.position = position;
this.size = size;
}
void draw() {
rect(position.x, position.y, size.width, size.height);
}
}
/** 一定周期でゼロになるカウント。frameCountの代わり */
final class CyclicCount {
final int value;
final int maxValue;
CyclicCount(int value, int maxValue) {
this.value = value;
this.maxValue = maxValue;
}
CyclicCount increment() {
return new CyclicCount((value + 1) % maxValue, maxValue);
}
boolean isZero() {
return this.value == 0;
}
}
/**
* このファイルには可変クラス(粒子と粒子グループ)を集めています。
*/
import java.util.List;
import java.util.Arrays;
class Particle {
LineSegment lineSegment;
ImmutableVector targetPoint;
Particle() {
lineSegment = ZERO_LINE_SEGMENT;
targetPoint = ZERO_VECTOR;
}
void update() {
ImmutableVector displacement = targetPoint
.sub(lineSegment.endPoint)
.mult(EASING_FACTOR);
// オブジェクトを作り直すことで値を更新
lineSegment = new LineSegment(
lineSegment.endPoint,
lineSegment.endPoint.add(displacement)
);
}
void setTargetPosition(float x, float y) {
// オブジェクトを作り直すことで値を更新
targetPoint = new ImmutableVector(x, y);
}
void draw() {
lineSegment.draw();
}
}
class ParticleGroup {
final List<Particle> list;
ParticleGroup(int particleCount) {
final Particle[] array = new Particle[particleCount];
for (int i = 0; i < particleCount; i++)
array[i] = new Particle();
list = Arrays.asList(array);
}
void update() {
for (Particle each : list) each.update();
}
void shuffle(float maxX, float maxY) {
for (Particle each : list)
each.setTargetPosition(random(maxX), random(maxY));
}
void draw() {
for (Particle each : list) each.draw();
}
}
/**
* メインのソースファイルです。
*/
// 定数扱いの値オブジェクト。Particleクラスで使う
final ImmutableVector ZERO_VECTOR = new ImmutableVector(0, 0);
final LineSegment ZERO_LINE_SEGMENT = new LineSegment(ZERO_VECTOR, ZERO_VECTOR);
final Ratio EASING_FACTOR = new Ratio(0.25);
// draw()で使う変数
final ParticleGroup particles = new ParticleGroup(32);
Rectangle canvasRectangle;
CyclicCount count = new CyclicCount(0, 30);
void setup(){
size(800, 800);
fill(252, 32);
strokeWeight(8);
canvasRectangle = new Rectangle(
new ImmutableVector(0, 0),
new RectangleSize(width, height)
);
}
void draw(){
canvasRectangle.draw();
if (count.isZero()) particles.shuffle(width, height);
particles.update();
particles.draw();
count = count.increment(); // increment()は新たなオブジェクトを返す
}
まことに仰々しくなってまいりました。
かなり前ですがこのスタイルでゲームっぽいものを作ろうとしたとき、定義は確かに大変ですが、ほんとうにバグが激減したので驚いた記憶があります。
他にネックになりえるのはオブジェクトの生成破棄のコストです。「ProcessingのJavaモードで書いてPCで動かす」という条件であればほぼ心配いらないというのが筆者の感覚ですが、他の言語であったり環境がスマホであったりすると場合によっては厳しいかもしれません。
また、大量のデータを一度に処理するような場合もこのままでは無駄が多く、Flyweightパターンなどの出番になると思われます。
関数型(ただしJavaScript)
関数型! と言ってしまうといろいろな議論がありそうですが、ここでは単に、
- プログラムを部品化するメインの手段として関数を用いる
- 関数を繋ぎ合わせてデータを変換していく
- できるだけ副作用(関数の外の何かへの影響。値の更新など)を避ける
- できるだけ参照透過(同じ入力なら常に結果も同じ)な関数にする
といったことを、手段は最善でなくとも良いのでゆるく志していくスタイルのこととします。
Processingでは少々大変に思えるので、ここでは JavaScript (ES2015) + p5.js を用います。9
ベストプラクティスがよく分かっておらず不安ですが、ひとまず次のように書きました。10
// 新たに生成する系(初期化用)
const createLineSegment = () => {
return { start: createVector(), end: createVector() };
};
const createParticle = () => {
return { lineSegment: createLineSegment(), targetPoint: createVector() };
};
const createParticles = count => Array.from(Array(count), createParticle);
// 粒子の移動処理に使う部品系
const distance = (current, target) => p5.Vector.sub(target, current);
const easingFactor = 0.25;
const displacement = (current, target) => p5.Vector.mult(distance(current, target), easingFactor);
const nextPoint = (current, target) => p5.Vector.add(current, displacement(current, target));
const nextLineSegment = (currentPoint, targetPoint) => {
return { start: currentPoint, end: nextPoint(currentPoint, targetPoint) };
};
// 粒子を受け取って新たな粒子を返す系
const changeTargetPoint = particle => newTarget => {
return { lineSegment: particle.lineSegment, targetPoint: newTarget };
};
const updateParticle = particle => {
return {
lineSegment: nextLineSegment(
particle.lineSegment.end,
particle.targetPoint
),
targetPoint: particle.targetPoint
};
};
// ---- 副作用なし・参照透過 ここまで -----------------------------
// ランダムな位置を返す。参照透過でない
const randomPoint = () => createVector(random(width), random(height));
// 線分を描画。画面への出力は一種の副作用である
const drawLineSegment = seg => line(seg.start.x, seg.start.y, seg.end.x, seg.end.y);
// 粒子の配列。毎フレーム新たなグループにすげ変わる。古いのは破棄
let particles;
function setup() {
createCanvas(800, 800);
fill(252, 32);
strokeWeight(8);
particles = createParticles(32);
};
function draw() {
rect(0, 0, width, height);
// 全粒子の集合を、目標位置ランダム変更後の粒子の集合に置き換える(30フレーム毎)
if (frameCount % 30 === 0)
particles = particles
.map(changeTargetPoint)
.map(f => f(randomPoint()));
// 全粒子の集合を、位置更新後の粒子の集合に置き換える
particles = particles.map(updateParticle);
// 全粒子の集合を線分の集合に変換し、その全てを描画する
particles
.map(particle => particle.lineSegment)
.forEach(drawLineSegment);
}
(高階関数などをもっと使う余地があるかもしれません。JSで可能な、よりクールな書き方がありましたら教えてください)
副作用を避けると、変数への代入すら難しくなりますが、これは前項の不変オブジェクトをさらに推し進めた形となりそうです。
前項ではParticle
やParticleGroup
クラスは可変のままとしましたが、それすらも不変と考え、毎フレーム、すべてのオブジェクトをごっそり作り変えるようにしました。
「今の世界」を入力として関数にぶち込み、「1フレーム未来の世界」を出力として受け取っている、とでも言えばよいでしょうか。
非効率にも見えますが、JavaScriptはともかくとして言語自体が関数型指向であれば、いろいろなことを高効率で行える仕組みがある……らしい……と聞きます。
なおOOP的なプログラムで副作用に拘らない場合でも、要所要所で関数をうまく使うことでクラスを複雑化させずに済むことがよくあると感じていて、個人的にもさらに学ぶ価値がありそうだと思っています。
データ指向型
最後ですが、処理速度の要求がシビアな場合を考えます(主にゲーム開発を考えています)。
このとき、オブジェクトをたくさん作って使うプログラムだと、以下のような点で効率が落ちてしまいます。
- オブジェクトの生成およびGCによる破棄のコスト11
- メモリ上のデータ配置が分散することによるキャッシュミスのコスト
- 参照値から実データを見に行く際のコスト
そこで、メモリ上にデータを綺麗に並べ、高速にアクセスしやすい状態にすることを重視するアプローチがあり、データ指向設計などと呼ばれるようです。
参考: データ指向設計 (または何故OOPで自爆してしまうのか)
参考: 「プログラミングの常識」を時々見直す必要性について
ところでこの観点からすると、一番最初に書いた素朴型が実はかなり優秀であったことが分かります。
しかし、なにかもう少し構造を整理する方法はないか。
まだ筆者にはちゃんとしたデータ指向設計の知識がないのですが、少なくともforループくらいは隠蔽したいし機能の追加も少しは容易にしてみたい。
ということで、単純な配列だけという構造は残しつつ、以下のようなことをしてみます。
- データの配列を一ヶ所に集めた
ParticleSystem
クラスを作る -
Process
という型を用意、データの操作をそれで作るようにして、任意の操作をParticleSystem
に投げるだけで実行してもらえるようにする
なお、真面目に処理効率を上げたいときには*絶対*にプロファイリングをしなければなりません。が、今回はなんとなくやってみた程度のものであり、すみませんが効率が悪化していない保証はありません。
/**
* このファイルでは、基盤となるクラスとインタフェースを定義しています。
*/
/**
* 粒子に対する処理。
* Javaで関数オブジェクト的なものを作りたいときは、このようにinterfaceを用意しておく。
* これを実装するオブジェクトなら何でも、ParticleSystem.runProcess()に渡すことが可能
*/
interface Process {
void run(
int i,
float[] x,
float[] y,
float[] targetX,
float[] targetY
);
}
/** 粒子のデータを管理し、任意の処理を受け付けるクラス */
class ParticleSystem {
static final int PARTICLE_COUNT = 32;
final float[] x = new float[PARTICLE_COUNT];
final float[] y = new float[PARTICLE_COUNT];
final float[] targetX = new float[PARTICLE_COUNT];
final float[] targetY = new float[PARTICLE_COUNT];
/** Process型を投げ入れるとそれを各粒子について実行してくれる */
final void runProcess(Process process) {
final float[] x = this.x;
final float[] y = this.y;
final float[] targetX = this.targetX;
final float[] targetY = this.targetY;
for (int i = 0; i < PARTICLE_COUNT; i++)
process.run(i, x, y, targetX, targetY);
}
}
/**
* このファイルには、粒子に対して行う処理の具体的な実装、
* すなわちProcess型のインスタンスを集めています。
*/
/** 単なる簡易イージング用関数 */
final float ease(float currentValue, float targetValue, float easingFactor) {
return currentValue + easingFactor * (targetValue - currentValue);
}
final float EASING_FACTOR = 0.25;
// 粒子の更新・描画。匿名クラスという宣言方法。run()の中身を好きに決められる
final Process updateAndDraw = new Process() {
final void run(int i, float[] x, float[] y, float[] targetX, float[] targetY) {
line(
x[i],
y[i],
x[i] = ease(x[i], targetX[i], EASING_FACTOR),
y[i] = ease(y[i], targetY[i], EASING_FACTOR)
);
}
};
// 同じく匿名クラス。上の更新・描画処理に、目標位置のランダム変更を追加した版
final Process shuffleAndDraw = new Process() {
final void run(int i, float[] x, float[] y, float[] targetX, float[] targetY) {
targetX[i] = random(width);
targetY[i] = random(height);
updateAndDraw.run(i, x, y, targetX, targetY);
}
};
/**
* メインのソースファイルです。
*/
/** フレーム数に応じて、いま実行すべきProcessを返す */
final Process selectProcess() {
return frameCount % 30 == 0 ? shuffleAndDraw : updateAndDraw;
}
final ParticleSystem system = new ParticleSystem();
final void setup() {
size(800, 800);
fill(252, 32);
strokeWeight(8);
}
final void draw(){
square(0, 0, 800);
system.runProcess(selectProcess());
}
このようにすると、処理の種類が増えた時にもProcess型のオブジェクトを増やすだけで良いので追加がしやすいのではないかという目論見でした。
ただし、この例ではデータ自体の種類が増えた時の変更が極めて面倒という欠点があり、課題は多そうです。
また、要素数が膨大になった時にこの構造は本当にキャッシュ効率がいいのかというと恐らくそうでもなく、効率よく処理される確率を最大化するように、データの区切り方(Array of Struct、Struct of Array、そのハイブリッドなど)を工夫する余地があると思われます。
ここからさらに、データの種類も処理の種類も柔軟に増やせるような仕組みを作ると(もはやメタプログラミングというべき状態な気がしますが)、ECS(Entity Component System)と呼ばれる仕組みに近いものになる、といったような理解をしています。
参考: 【Unity】Entity Component System入門(その1)【2018.2】
終わりに
いかがd...
他にも、イベント駆動型、省メモリ型、並列性重視型、スタック指向型、いろいろ考えられそうですね。
それぞれ何かしら説明を加えたいという思いから、いろいろ知ったかぶったことを書いてしまったので、訂正・補足などありましたら大変ありがたく存じます。
-
ラムダ式や型推論は使用できません。また、定義したクラスは全て、コンパイル時にある一つのクラスの内部クラスになるため、staticやアクセス修飾子などのあたりが異なってきます(外に出す方法もありますが本記事対象外)。 ↩
-
加減速を調整して動きを滑らかにすること。 ↩
-
フレームレートはデフォルトの60FPS。 ↩
-
ただし、Processingはあくまで初心者向けであることを重視しており、多数のクラスを複雑に組み合わせるようなプログラムはもともとあまり想定されていません。この記事全体に言えることですが、そもそもの技術選択については少し横に置いてお読みください。 ↩
-
「オブジェクト指向」にそもそも2種類(もしくはそれ以上)の系譜があるという話もあり、いずれにしても使い方の難しい言葉です。 ↩
-
業務ロジックとの関係とか他の側面もあるので、同じというと語弊がありそうです。 ↩
-
ここで作っている不変クラスは用途があまり限定されていないので、値オブジェクトの考え方からすると、(どんなモデルを考えるか次第でもありますが)おそらくもっと個別に意味を持たせたクラスを作るべきなのかもしれません。この例では、ひとまず不変ならまあいいかというレベルで留めています。 ↩
-
あるクラスが不変に見えても、それを継承してメソッドをオーバーライドすると可変なクラスに変えることができてしまいます。今回の例では手っ取り早く、クラス自体をfinalにして継承不可にしてしまっています。変数については、再代入できないことと中身を更新できないこととは別の話なので、finalさえ付けていれば良いわけではないことに注意。 ↩
-
現状、Processing本家のエディターのp5.jsモードでは、ライブラリが古いせいか動きません。自分で用意するか、私のGitHubリポジトリのものを使うか、もしくはOpenProcessingなどの環境を使うかすれば動きます。 ↩
-
前半部分は副作用のない純粋関数群のつもりですが、JavaScriptなので抜け道があり、どんな場合でも絶対にそうだというわけではありません。 ↩
-
GCについては、バグ発生率増加と引き換えにオブジェクトプールなどによって回避できますが、それをやらずに済むならそうしたいし、いずれにしろ他の問題点が残ります。 ↩