Skip to content

Instantly share code, notes, and snippets.

@tos-kamiya
Created February 26, 2025 08:27
Show Gist options
  • Save tos-kamiya/e19a9ba06ad16607ff6b90a908123627 to your computer and use it in GitHub Desktop.
Save tos-kamiya/e19a9ba06ad16607ff6b90a908123627 to your computer and use it in GitHub Desktop.
Fish-school simulation inspired by an X post: https://x.com/snakajima/status/1894382189720092770
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Fish School Simulation</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
}
.controls {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(255, 255, 255, 0.8);
padding: 5px;
border-radius: 4px;
z-index: 100;
}
input {
margin-right: 5px;
}
</style>
</head>
<body>
<div class="controls">
<label for="numFish">魚の数:</label>
<input type="number" id="numFish" value="300" min="1" max="1000">
<button onclick="updateNumFish()">更新</button>
</div>
<script>
let fish = [];
let numFish = 300;
const cellSize = 50; // セルサイズ
let grid;
function setup() {
createCanvas(windowWidth, windowHeight);
updateGrid();
for (let i = 0; i < numFish; i++) {
fish.push(new Fish());
}
}
function draw() {
background(64, 164, 255); // 水の色を設定
for (let f of fish) {
f.flock(fish);
f.update();
f.edges(); // 境界にぶつからないように修正
f.show();
}
}
function updateGrid() {
const cols = Math.ceil(width / cellSize);
const rows = Math.ceil(height / cellSize);
grid = new Array(cols).fill().map(() => new Array(rows).fill().map(() => []));
for (let f of fish) {
let col = Math.floor(f.position.x / cellSize);
let row = Math.floor(f.position.y / cellSize);
if (col >= 0 && col < cols && row >= 0 && row < rows) {
grid[col][row].push(f);
}
}
}
class Fish {
constructor() {
this.position = createVector(random(width), random(height));
this.velocity = p5.Vector.random2D();
this.acceleration = createVector(0, 0);
this.r = 3;
this.maxSpeed = 4;
this.maxForce = 0.1;
}
applyForce(force) {
this.acceleration.add(force);
}
update() {
this.velocity.add(this.acceleration);
this.velocity.limit(this.maxSpeed);
this.position.add(this.velocity);
this.acceleration.mult(0); // 加速度をリセット
}
show() {
push();
translate(this.position.x, this.position.y);
let angle = this.velocity.heading() + PI / 2; // 頭が左90度に向くように調整
rotate(angle);
fill(255);
stroke(200);
beginShape();
vertex(0, -this.r * 2);
vertex(-this.r, this.r * 2);
vertex(this.r, this.r * 2);
endShape(CLOSE);
pop();
}
edges() {
const edgeBuffer = 50; // 境界からのバッファ距離
if (this.position.x < edgeBuffer) {
let steerForce = createVector(this.maxSpeed, this.velocity.y);
steerForce.sub(this.velocity);
steerForce.limit(this.maxForce);
this.applyForce(steerForce);
}
if (this.position.x > width - edgeBuffer) {
let steerForce = createVector(-this.maxSpeed, this.velocity.y);
steerForce.sub(this.velocity);
steerForce.limit(this.maxForce);
this.applyForce(steerForce);
}
if (this.position.y < edgeBuffer) {
let steerForce = createVector(this.velocity.x, this.maxSpeed);
steerForce.sub(this.velocity);
steerForce.limit(this.maxForce);
this.applyForce(steerForce);
}
if (this.position.y > height - edgeBuffer) {
let steerForce = createVector(this.velocity.x, -this.maxSpeed);
steerForce.sub(this.velocity);
steerForce.limit(this.maxForce);
this.applyForce(steerForce);
}
}
flock(boids) {
updateGrid(); // グリッドを更新して近くの魚を取得
let perceptionRadius = 50;
let cols = Math.ceil(width / cellSize);
let rows = Math.ceil(height / cellSize);
let col = Math.floor(this.position.x / cellSize);
let row = Math.floor(this.position.y / cellSize);
// 関連するセルを取得
let neighbors = [];
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
let xCell = col + i;
let yCell = row + j;
if (xCell >= 0 && xCell < cols && yCell >= 0 && yCell < rows) {
neighbors = neighbors.concat(grid[xCell][yCell]);
}
}
}
let sep = this.separation(neighbors);
let ali = this.align(neighbors);
let coh = this.cohesion(neighbors);
sep.mult(1.5); // より多くのスペースを保つために重要
ali.mult(1.0);
coh.mult(1.0);
this.applyForce(sep);
this.applyForce(ali);
this.applyForce(coh);
}
align(boids) {
let steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
if (other !== this && p5.Vector.dist(this.position, other.position) < 50) { // 排他的な距離チェック
steering.add(other.velocity);
total++;
}
}
if (total > 0) {
steering.div(total);
steering.setMag(this.maxSpeed);
steering.sub(this.velocity);
steering.limit(this.maxForce);
}
return steering;
}
cohesion(boids) {
let steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
if (other !== this && p5.Vector.dist(this.position, other.position) < 50) { // 排他的な距離チェック
steering.add(other.position);
total++;
}
}
if (total > 0) {
steering.div(total);
steering.sub(this.position);
steering.setMag(this.maxSpeed);
steering.sub(this.velocity);
steering.limit(this.maxForce);
}
return steering;
}
separation(boids) {
let steer = createVector(0, 0);
let total = 0;
for (let other of boids) {
if (other !== this && p5.Vector.dist(this.position, other.position) < 25) { // 排他的な距離チェック
let diff = p5.Vector.sub(this.position, other.position);
diff.div(p5.Vector.dist(this.position, other.position)); // 強さは距離で減少
steer.add(diff);
total++;
}
}
if (total > 0) {
steer.div(total);
}
if (steer.mag() > 0) {
steer.setMag(this.maxSpeed);
steer.sub(this.velocity);
steer.limit(this.maxForce);
}
return steer;
}
}
function updateNumFish() {
let input = document.getElementById('numFish');
numFish = parseInt(input.value);
// 魚の数を更新
fish = [];
for (let i = 0; i < numFish; i++) {
fish.push(new Fish());
}
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment