forked from: Ants
(追記というか書きなおし。ついでにコードも微修正)
* アリのエサ運びをシミュレーションしてみました
* アリは普段ランダムに動いていますが、
* エサを見つけると地面にフェロモンを残しながら巣まで戻り、
* そのフェロモンを発見した他のアリがそれを辿ってエサの在り処を見つけるという流れです。
*
* フェロモン濃度だけで判断するとなかなかうまく辿れなかったので
* 濃度と一緒に進む方向も記録するようにしています。
* (濃度と向きはBitmapDataのピクセルに色情報としてまとめて記録してます)
* フェロモンが蒸発して消えないかぎり複雑な道のりも辿れるんですが、
* 複雑すぎると巣に帰るプログラムかけなくなりそうだったのでやめました。。。
*
* (操作説明)
* 土をクリックするとお菓子が置けて、左下のボタンでアリが増やせます(1000匹まで)
* ※増やしすぎ注意(負荷的にも見た目的にも・・・)
/**
* Copyright soundkitchen ( http://wonderfl.net/user/soundkitchen )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/3d1n
*/
// forked from tencho's Ants
/**
* (追記というか書きなおし。ついでにコードも微修正)
* アリのエサ運びをシミュレーションしてみました
* アリは普段ランダムに動いていますが、
* エサを見つけると地面にフェロモンを残しながら巣まで戻り、
* そのフェロモンを発見した他のアリがそれを辿ってエサの在り処を見つけるという流れです。
*
* フェロモン濃度だけで判断するとなかなかうまく辿れなかったので
* 濃度と一緒に進む方向も記録するようにしています。
* (濃度と向きはBitmapDataのピクセルに色情報としてまとめて記録してます)
* フェロモンが蒸発して消えないかぎり複雑な道のりも辿れるんですが、
* 複雑すぎると巣に帰るプログラムかけなくなりそうだったのでやめました。。。
*
* (操作説明)
* 土をクリックするとお菓子が置けて、左下のボタンでアリが増やせます(1000匹まで)
* ※増やしすぎ注意(負荷的にも見た目的にも・・・)
*/
package {
import com.bit101.components.Label;
import com.bit101.components.PushButton;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.display.Sprite;
import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filters.DropShadowFilter;
import flash.geom.ColorTransform;
import flash.geom.Point;
import flash.geom.Rectangle;
public class AntsTest extends Sprite {
private const DISPLAY:Rectangle = new Rectangle(0, 0, 465, 465);
private const ANTSNUM:int = 1000;
private const ANTSMAX:int = 10000;
private const MATERIAL_URL:String = "http://assets.wonderfl.net/images/related_images/4/40/40c1/40c16871d560d547ebb1fc1725db47922ef9f261";
private var world:World;
private var bg:Sprite;
private var containerAnts:Sprite;
private var containerFoods:Sprite;
private var pheromoneBmp:Bitmap;
private var canvas:BitmapData;
private var loader:ImageLoader;
private var info:Label;
private var whiteColor:ColorTransform;
//コンストラクタ
public function AntsTest() {
Wonderfl.capture_delay(25);
whiteColor = new ColorTransform();
whiteColor.color = 0xFFFFFF;
stage.frameRate = 30;
world = new World();
world.home.setPosition(110, 350);
world.obstacles.push(new Obstacle(280, 225, 90));
world.obstacles.push(new Obstacle(0, -150, 280));
bg = addChild(new Sprite()) as Sprite;
bg.graphics.beginFill(0x444444, 1);
bg.graphics.drawRect(0, 0, DISPLAY.width, DISPLAY.height);
bg.graphics.endFill();
addChild(world.home);
//外部画像読み込み開始
loader = new ImageLoader();
loader.load(MATERIAL_URL, onLoadImage, onErrorImage);
}
//画像読み込み失敗
private function onErrorImage(e:ErrorEvent):void {
var msg:Label = new Label(this, 5, 5, e.text);
msg.transform.colorTransform = whiteColor;
}
//画像読み込み完了
private function onLoadImage():void {
var ground:Bitmap = bg.addChild(loader.bg) as Bitmap;
ground.width = DISPLAY.width;
ground.height = DISPLAY.height;
//メイン処理開始
world.init(DISPLAY.width, DISPLAY.height);
setLayout();
init(ANTSNUM);
bg.addEventListener(MouseEvent.MOUSE_DOWN, onClickStage);
addEventListener(Event.ENTER_FRAME, onEnter);
}
//画面にボタン等を配置する
private function setLayout():void {
pheromoneBmp = addChild(new Bitmap(world.pheromone.map)) as Bitmap;
pheromoneBmp.visible = false;
pheromoneBmp.blendMode = BlendMode.LIGHTEN;
//障害物領域をマウスクリックできなくさせる
for each(var obs:Obstacle in world.obstacles) {
var sp:Sprite = addChild(new Sprite()) as Sprite;
sp.graphics.beginFill(0x444444, 0);
sp.graphics.drawCircle(obs.center.x, obs.center.y, obs.radius);
sp.graphics.endFill();
}
canvas = new BitmapData(DISPLAY.width, DISPLAY.height, true, 0x00FFFFFF);
addChild(new Bitmap(canvas)).filters = [new DropShadowFilter(3, 45, 0x222222, 0.8, 3, 3, 1, 1)];
containerAnts = new Sprite();
containerFoods = addChild(new Sprite()) as Sprite;
containerFoods.mouseChildren = false;
containerFoods.mouseEnabled = false;
new Label(this, 5, 3, "CLICK TO FEED").transform.colorTransform = whiteColor;
//画面下のバー
var panel:Sprite = addChild(new Sprite()) as Sprite;
var blackBox:Sprite = panel.addChild(new Sprite()) as Sprite;
blackBox.graphics.beginFill(0x000000, 0.5);
blackBox.graphics.drawRect(0, 0, DISPLAY.width, 25);
blackBox.graphics.endFill();
panel.y = DISPLAY.height - panel.height;
new SwitchButton(panel, DISPLAY.width - 160, 5, ["PHEROMONE: OFF", "PHEROMONE: ON"], onClickPheromon);
new PushButton(panel, DISPLAY.width - 55, 5, "RESET", onClickClear).setSize(50, 16);
new PushButton(panel, 5, 5, "+1000 ANTS", onClickAdd).setSize(70, 16);
info = new Label(panel, 85, 3, "");
info.transform.colorTransform = whiteColor;
}
private function refreshInfo():void {
info.text = "ANTS: " + world.ants.length;
}
//蟻の数を指定して初期化
private function init(num:int):void {
world.clear();
var wait:Number = 10;
for (var i:int = 0; i < num; i++) {
wait += Math.max(0.05, 15 / (i * i / 100 + 1));
containerAnts.addChild(world.addAnt(wait).body);
}
refreshInfo();
}
private function onClickClear(...arg):void{
init(ANTSNUM);
}
private function onClickAdd(...arg):void {
var num:int = Math.min(ANTSNUM, ANTSMAX - world.ants.length);
for (var i:int = 0; i < num; i++) {
containerAnts.addChild(world.addAnt(i/2).body);
}
refreshInfo();
}
private function onClickPheromon(mode:int):void{
pheromoneBmp.visible = (mode == 1);
}
private function onClickStage(e:MouseEvent):void {
var img:ImageData = loader.feeds[Math.round(Math.random() * (loader.feeds.length - 1))];
containerFoods.addChild(world.addFood(stage.mouseX, stage.mouseY, img).sprite);
}
//毎フレーム処理
private function onEnter(e:Event):void {
world.pheromone.map.lock();
for each(var a:Ant in world.ants) a.action(world);
world.pheromone.blur();
world.pheromone.map.unlock();
canvas.fillRect(DISPLAY, 0x00000000);
canvas.draw(containerAnts);
}
}
}
import com.bit101.components.PushButton;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.display.DisplayObjectContainer;
import flash.display.Loader;
import flash.display.LoaderInfo;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.filters.DropShadowFilter;
import flash.geom.ColorTransform;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.net.URLRequest;
import flash.system.LoaderContext;
//角度変換用
class Angle {
static public const ALL_RADIAN:Number = Math.PI * 2;
static public const TO_RADIAN:Number = Math.PI / 180;
static public const TO_ROTATION:Number = 180 / Math.PI;
//角度を合成する
static public function between(a1:Number, a2:Number, per:Number):Number {
var minus:Number = a1 - a2;
var r180:Number = (minus % Angle.ALL_RADIAN + Angle.ALL_RADIAN) % Angle.ALL_RADIAN;
if (r180 > Math.PI) r180 -= Angle.ALL_RADIAN;
var a0:Number = r180 + a2;
return a0 * (1 - per) + a2 * (per);
}
}
//
class World {
public var area:Rectangle; //ワールドサイズ
public var ants:Vector.<Ant>; //全てのアリ
public var foods:Vector.<Food>; //全てのエサ
public var obstacles:Vector.<Obstacle>; //全ての障害物
public var home:AntsHill; //アリ塚
public var pheromone:Pheromone; //フェロモン
public function World() {
ants = new Vector.<Ant>();
foods = new Vector.<Food>();
obstacles = new Vector.<Obstacle>();
home = new AntsHill();
home.visible = false;
}
//サイズを指定して初期化
public function init(width:Number, height:Number):void {
area = new Rectangle(0, 0, width, height);
pheromone = new Pheromone(width, height);
home.visible = true;
}
//エサを削って無くなったら削除
public function cutFood(f:Food):void {
if (f.cut()) {
f.remove();
foods.splice(foods.indexOf(f), 1);
}
}
//エサを追加
public function addFood(x:int, y:int, img:ImageData):Food {
var f:Food = new Food(x, y, 50, 200, img);
foods.push(f);
return f;
}
//アリを追加
public function addAnt(wait:int):Ant {
var a:Ant = new Ant(home.position.x, home.position.y);
a.thinkTime = wait;
ants.push(a);
return a;
}
//色々リセット
public function clear():void {
for each(var f:Food in foods) f.remove();
for each(var a:Ant in ants) a.remove();
ants.length = 0;
foods.length = 0;
pheromone.clear();
}
}
//エサの画像
class ImageData {
public var colors:Array;
public var bmp:BitmapData;
public function ImageData(bmp:BitmapData) {
this.bmp = bmp;
//画像のピクセルカラーを調べる(透明領域は無視)
colors = new Array();
for (var px:int = 0; px < bmp.width; px += 8) {
for (var py:int = 0; py < bmp.width; py += 8) {
var rgba:uint = bmp.getPixel32(px, py);
if (rgba >>> 24 == 255) colors.push(rgba & 0xFFFFFF);
}
}
}
//画像の色をランダムに取得
public function getRandomColor():uint {
return colors[Math.round(Math.random() * (colors.length - 1))];
}
}
//画像をロードして分割(複数画像読み込みが面倒くさかったので・・・)
class ImageLoader {
public var bg:Bitmap;
public var feeds:Vector.<ImageData>;
private var loader:Loader;
private const FEED_NUM:int = 5;
public function ImageLoader() {
loader = new Loader();
}
public function load(src:String, loadFunc:Function, errorFunc:Function):void{
loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, errorFunc);
loader.contentLoaderInfo.addEventListener(SecurityErrorEvent.SECURITY_ERROR, errorFunc);
loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
function(...arg):void {
onLoadImage.apply(null, arg);
loadFunc.apply();
});
loader.load(new URLRequest(src), new LoaderContext(true));
}
private function onLoadImage(e:Event):void {
var info:LoaderInfo = e.currentTarget as LoaderInfo;
var bmp:BitmapData = Bitmap(info.content).bitmapData;
feeds = new Vector.<ImageData>();
for (var i:int = 0; i < FEED_NUM; i++) {
var bmp2:BitmapData = new BitmapData(64, 64, true, 0x00FFFFFF);
bmp2.copyPixels(bmp, new Rectangle(64 * i, 0, 64, 64), new Point(0, 0));
feeds.push(new ImageData(bmp2));
}
bg = new Bitmap(new BitmapData(bmp.width, bmp.height-64, false));
bg.bitmapData.copyPixels(bmp, new Rectangle(0, 64, bg.width, bg.height), new Point());
bg.smoothing = true;
}
}
//切り替えボタン
class SwitchButton extends PushButton {
private var mode:int = 0;
private var modeNum:int = 1;
private var labels:Array;
private var func:Function;
public function SwitchButton(parent:DisplayObjectContainer, xpos:Number, ypos:Number, labels:Array = null, func:Function = null) {
if(labels == null) labels = [""];
this.labels = labels;
this.func = func;
modeNum = labels.length;
super(parent, xpos, ypos, labels[0], onClick);
height = 16;
}
private function onClick(...arg):void {
mode = (mode + 1) % modeNum;
label = labels[mode];
if(func != null) func.apply(null, [mode]);
}
}
//アリ塚
class AntsHill extends Sprite {
public var position:Point;
public function AntsHill() {
graphics.beginFill(0x000000, 0.3);
graphics.drawCircle(0, 0, 9);
graphics.beginFill(0x000000, 1);
graphics.drawCircle(0, 0, 7);
graphics.endFill();
position = new Point();
}
//位置変更
public function setPosition(x:Number, y:Number):void {
this.x = position.x = x;
this.y = position.y = y;
}
}
//フェロモン
class Pheromone {
public var map:BitmapData;
private var ct:ColorTransform;
private var timeCount:int = 0;
public function Pheromone(width:Number, height:Number) {
ct = new ColorTransform(1, 0.99, 1, 1, 0, 0, 0, 0);
map = new BitmapData(width, height, true, 0x00000000);
}
//指定座標のフェロモンから進む角度を調べる
public function getGuidepost(x:int, y:int):Number {
//周囲のフェロモン濃度を調べて濃い方向を調べる
var isNone:Boolean = true;
var totalX:Number = 0;
var totalY:Number = 0;
for (var px:int = -2; px <= 2; px++) {
for (var py:int = -2; py <= 2; py++) {
if (px != 0 || py != 0) {
var per:Number = (map.getPixel32(x + px * 4, y + py * 4) >> 8 & 0xFF) / 255;
totalX += per * px;
totalY += per * py;
if (per) isNone = false;
}
}
}
var res:Number;
if ((!totalY && !totalX) || isNone) {
res = NaN;
} else {
//足元のフェロモンから進む角度を調べる
var per2:Number = (map.getPixel32(x, y) & 0xFF) / 255;
var xx:Number, yy:Number;
if (per2 == 0) {
//フェロモンが無ければ濃い方へ
xx = totalX;
yy = totalY;
} else {
//フェロモンがあれば先に進む(周囲濃度で若干角度補正)
var radian:Number = per2 * Angle.ALL_RADIAN + Math.PI;
xx = Math.cos(radian) * 40 + totalX;
yy = Math.sin(radian) * 40 + totalY;
}
res = Math.atan2(yy, xx);
}
return res;
}
//フェロモンをつける
public function putPheromone(x:int, y:int, radianPer:Number):void {
var rgb:uint = 0xF0 << 24 | 0xFF << 16 | 0xFF << 8 | uint(0xFF * radianPer);
map.fillRect(new Rectangle(x-5, y-5, 11, 11), rgb);
}
//フェロモン拡散
public function blur():void {
if (!(++timeCount % 2)) map.colorTransform(map.rect, ct);
}
//フェロモンリセット
public function clear():void {
map.fillRect(map.rect, 0x00000000);
}
}
//アリのエサ
class Food {
public var sprite:Sprite; //グラフィック
public var position:Point; //位置
public var size:Number; //半径
public var num:int; //残りの量
private var numMax:int;
public var image:ImageData;
private var maskImage:BitmapData;
private var noiseImage:BitmapData;
public function Food(x:Number = 0, y:Number = 0, size:Number = 10, num:int = 10, img:ImageData = null) {
position = new Point(x, y);
this.size = size;
this.num = num;
numMax = num;
sprite = new Sprite();
sprite.x = x;
sprite.y = y;
sprite.scaleX = sprite.scaleY = 0.7;
sprite.filters = [new DropShadowFilter(5, 45, 0x111111, 0.7, 8, 8, 1, 1)];
image = img;
var foodBmp:Bitmap = sprite.addChild(new Bitmap(image.bmp)) as Bitmap;
foodBmp.smoothing = true;
//徐々に削られるエフェクト用
maskImage = new BitmapData(image.bmp.width, image.bmp.height, true);
maskImage.fillRect(maskImage.rect, 0x00888888);
noiseImage = new BitmapData(image.bmp.width, image.bmp.height, false);
noiseImage.perlinNoise(20, 20, 3, int(Math.random() * 100), false, true, 1, true);
var maskBmp:Bitmap = sprite.addChild(new Bitmap(maskImage)) as Bitmap;
maskBmp.blendMode = BlendMode.ERASE;
foodBmp.x = maskBmp.x = -foodBmp.width / 2;
foodBmp.y = maskBmp.y = -foodBmp.height / 2;
}
//削る
public function cut():Boolean {
num--;
var per:Number = num / numMax * 0.4 + 0.4;
maskImage.fillRect(maskImage.rect, 0xFF888888);
maskImage.threshold(noiseImage, noiseImage.rect, new Point(), "<", per*255, 0x00000000, 255, false);
return !num;
}
//削除
public function remove():void {
maskImage.dispose();
noiseImage.dispose();
if (sprite.parent != null) sprite.parent.removeChild(sprite);
}
}
//円形障害物
class Obstacle {
public var center:Point;
public var radius:Number;
public function Obstacle(x:Number = 0, y:Number = 0, radius:Number = 50) {
center = new Point(x, y);
this.radius = radius;
}
}
//アリ
class Ant {
public var body:Sprite; //グラフィック
public var position:Point; //位置
public var thinkTime:int = 0; //停止時間
private var locus:Vector.<Point>; //数フレーム前までの位置リスト
private var radian:Number = 0; //角度
private var speed:Number = 2; //速度
private var status:int = 0; //状況 0:エサ探し 1:巣に帰る
private var freeTime:int = 0; //フェロモン無効時間
private var view:Number = 20; //視界範囲
private var wanderTime:int = 1;
private var food:Sprite;
private var randomRad:Number;
public function Ant(x:Number = 0, y:Number = 0) {
body = new Sprite();
body.graphics.beginFill(0x000000, 1);
body.graphics.drawRect(-4, -1, 4, 2);
body.graphics.drawRect(1, -1, 1, 2);
body.graphics.drawRect(3, -1, 2, 2);
body.graphics.endFill();
food = body.addChild(new Sprite()) as Sprite;
food.graphics.beginFill(0xFFFFFF, 1);
food.graphics.drawRect(-2, -2, 4, 4);
food.graphics.endFill();
food.x = 6;
food.visible = false;
position = new Point(x, y);
locus = new Vector.<Point>();
radian = Math.random() * Angle.ALL_RADIAN;
randomRad = (Math.random() * 10 - 5) * Angle.TO_RADIAN;
}
//行動
public function action(w:World):void {
if (thinkTime) {
if (--thinkTime == 0) {
if (status == 0) startSearch();
if (status == 1) toFace(w.home.position);
}
return;
} else {
randomThink(0.015, Math.random() * 10 + 15);
}
//エサ探しモード
if (status == 0) {
var targetFood:Food = null;
var minDistance:Number = Number.MAX_VALUE;
for each(var f:Food in w.foods) {
var distance:Number = position.subtract(f.position).length;
//エサに接触したら持ち帰り始める
if (distance <= f.size/2 + 1) {
thinkTime = Math.random() * 50 + 30;
getFood(f.image.getRandomColor());
w.cutFood(f);
return;
}
//エサを見つけた
var d:Number = distance - f.size/2;
if (d <= view && d < minDistance) {
minDistance = d;
targetFood = f;
}
}
//エサを見つけているか
if (targetFood) {
toFace(targetFood.position);
} else {
if (freeTime > 0) freeTime--;
//フェロモンが近くにあるかチェック
var rad:Number = (freeTime > 0)? NaN : w.pheromone.getGuidepost(position.x, position.y);
if (isNaN(rad)) {
wander(w.obstacles);
} else {
radian = Angle.between(getAdjustRadian(w.obstacles, rad), radian, 0.5) + Angle.TO_RADIAN + randomRad;
checkStay();
}
}
}
//エサ持ち帰りモード
if (status == 1) {
var per:Number = radian / Angle.ALL_RADIAN;
w.pheromone.putPheromone(position.x, position.y, per);
goto(w.obstacles, w.home.position);
if (w.home.position.subtract(position).length < 5) backHome();
}
walk(radian);
adjustPosition(w.area);
refresh();
}
//障害物を避ける角度を求める
private function getAdjustRadian(obsts:Vector.<Obstacle>, rad:Number):Number {
var plus:Point = new Point(Math.cos(rad) * speed, Math.sin(rad) * speed);
var nextPos:Point = position.add(plus);
for each(var obs:Obstacle in obsts){
var distance:Number = Point.distance(nextPos, obs.center);
if (distance < obs.radius) {
var radius:Point = nextPos.subtract(obs.center);
if (radius.length < obs.radius) {
radius.normalize(obs.radius);
var fixPos:Point = obs.center.add(radius);
rad = Math.atan2(fixPos.y - position.y, fixPos.x - position.x);
}
break;
}
}
return rad
}
//ランダム回転
private function wander(obsts:Vector.<Obstacle>):void {
if (wanderTime && !--wanderTime) {
wanderTime = 4;
radian += (Math.random() * 60 - 30) * Angle.TO_RADIAN;
}
radian = Angle.between(getAdjustRadian(obsts, radian), radian, 0.85);
}
//指定の座標を向く
private function toFace(target:Point):void {
radian = Math.atan2(target.y - position.y, target.x - position.x);
}
//指定座標に向かいながらランダム回転
private function goto(obsts:Vector.<Obstacle>, target:Point):void {
if (wanderTime && !--wanderTime) {
wanderTime = 3;
var rad:Number = Math.atan2(target.y - position.y, target.x - position.x);
var per:Number = target.subtract(position).length/100
if (per > 1) per = 1;
rad = getAdjustRadian(obsts, rad);
radian = Angle.between(rad, radian, 0.75 * per) + (Math.random() * 30 - 15) * Angle.TO_RADIAN;
}
}
//一定時間その場でうろついていたらフェロモン無効に
private function checkStay():void {
locus.unshift(position.clone());
locus.length = 5;
if (locus[4] && locus[0].subtract(locus[3]).length <= speed * 2) freeTime = 60;
}
//ラジアン角指定で移動
private function walk(rad:Number):void {
radian = rad;
position.x += Math.cos(rad) * speed;
position.y += Math.sin(rad) * speed;
}
//ワールドエリアに収まるように位置調整
private function adjustPosition(worldRect:Rectangle):void {
var adjustRad:Number = NaN;
var padding:int = 5;
if (position.x < worldRect.left + padding) {
position.x = worldRect.left + padding;
adjustRad = 0;
}
if (position.x > worldRect.right - padding) {
position.x = worldRect.right - padding;
adjustRad = Math.PI;
}
if (position.y > worldRect.bottom - padding) {
position.y = worldRect.bottom - padding;
adjustRad = Math.PI * 1.5;
}
if (position.y < worldRect.top + padding) {
position.y = worldRect.top + padding;
adjustRad = Math.PI * 0.5;
}
if (!isNaN(adjustRad)) radian = Angle.between(adjustRad, radian, 0.9);
}
//一定確率で立ち止まる
private function randomThink(per:Number, time:int):void {
if (Math.random() <= per) thinkTime = time;
}
//エサを探し始める
private function startSearch():void {
body.visible = true;
status = 0;
}
//巣に入れる
private function backHome():void {
status = 0;
food.visible = false;
body.visible = false;
thinkTime = Math.random() * 100 + 100;
}
//エサを取得
private function getFood(color:uint = 0xFFFFFF):void {
status = 1;
food.visible = true;
var ct:ColorTransform = new ColorTransform();
ct.color = color;
food.transform.colorTransform = ct;
}
//表示更新
public function refresh():void {
body.x = position.x;
body.y = position.y;
body.rotation = radian * Angle.TO_ROTATION;
}
//親から削除
public function remove():void {
if (body.parent != null) body.parent.removeChild(body);
}
}