REBOL

 

Boids - Source Code

Craig Reynolds' Boid flocking simulation
Author: Nathan Brown
File size: 7K
Return to index

 

REBOL [
    Title:   "Boids"
    File:    %boids.r
    Author:  "Nathan Brown"
    Home:    http://www.croftware.com
    Date:    29-Mar-2004
    Purpose: {
        Bird flocking simulation based on Craig Reynolds' Boids.  The boids
        are controlled purely by local interactions with no explicit global
        control.  A flocking dynamic emerges over a short period of time.
    }
]

boid-num:    20   max-boid:    199
radius:      50   max-radius:  199
crowded:     10   max-crowded:  39
change-rate: 30   max-change:  180
species:      1
boids:       make block! 0

random/seed now/time

movement: [[5.0 0.0] [4.8 1.3] [4.3 2.5] [3.5 3.5] [2.5 4.3] [1.3 4.8] [0.0 5.0]
    [-1.3 4.8] [-2.5 4.3] [-3.5 3.5] [-4.3 2.5] [-4.8 1.3] [-5.0 0.0]
    [-4.8 -1.3] [-4.3 -2.5] [-3.5 -3.5] [-2.5 -4.3] [-1.3 -4.8] [0.0 -5.0]
    [1.3 -4.8] [2.5 -4.3] [3.5 -3.5] [4.3 -2.5] [4.8 -1.3] [5.0 0.0]
]

boid-face: make face [
    id: 0
    theta: 0
    act-loc: 0x0  ; last updated co-ords
    co-ords: none ; boid co-ords at next time-step
    aim: 0x0
    too-close: false
    feathers: red
    edge: make edge [size: 0x0]
    size: 10x10
    ;image: load %sphere.png
    effect: [key 0.0.0 fit]

    refresh: does [
        act-loc/x: co-ords/x
        act-loc/y: co-ords/y
        offset/x: co-ords/x - (size/x / 2)
        offset/y: co-ords/y - (size/y / 2)
    ]

    move: func [
        {Moves the boid one time-step in direction adjusted for its neighbouring
            flock-mates}
        heading [integer!] "General heading of this boid's neighbouring flock"
        /local ch diff
    ][
        ch: 0
        diff: theta - heading
        either too-close [0] [
            ch: ch + either theta > heading [
                either (abs diff) < (abs (diff + 360)) [- change-rate] [change-rate]
            ][
                either (abs diff) < (abs (diff + 360)) [change-rate] [- change-rate]
            ]
        ]

        theta: theta + ch + 360 // 360

        co-ords/x: co-ords/x + to-integer (first (pick movement (theta / 15 + 1)))
        co-ords/y: co-ords/y + - to-integer (second (pick movement (theta / 15 + 1)))

        co-ords/x: co-ords/x + world/size/x // world/size/x
        co-ords/y: co-ords/y + world/size/y // world/size/y
        
        aim/x: co-ords/x + to-integer (20 * cosine/radians (theta * (pi / 180)))
        aim/y: co-ords/y + to-integer (-20 * sine/radians (theta * (pi / 180)))
        
        too-close: false
    ]

    distance: func [
        {Euclidean distance between this boids and the specified co-ordinates}
        loc [pair! object!] "co-ordinates"
        /local x y
    ][
        x: loc/x - co-ords/x
        y: loc/y - co-ords/y
        return to-integer square-root (x * x + (y * y))
    ]

    general-heading: func [
        {Calculates the general heading of this boid's neighbouring flock-mates
            including itself}
        boids [block!] "Boids flock block"
        /local heading num goal-theta
    ][
        heading: make object! [x: 0.0 y: 0.0]
        num: 0

        foreach boid boids [
            either all [(distance boid/act-loc) < crowded not id = boid/id] [
                too-close: true
            ][
                if (distance boid/act-loc) < radius [
                    either feathers = boid/feathers [
                        heading/x: heading/x + boid/aim/x
                        heading/y: heading/y + boid/aim/y
                    ][
                        heading/x: either aim/x < boid/aim/x [0] [1000]
                        heading/y: either aim/y < boid/aim/y [0] [1000]
                        num: 0
                        break
                    ]
                    num: num + 1
                ]
            ]
        ]

        if not num = 0 [
            heading/x: heading/x / num
            heading/y: heading/y / num
        ]

        goal-theta: either (heading/x - co-ords/x) = 0 [0] [
            (co-ords/y - heading/y) / (heading/x - co-ords/x)
        ]
        goal-theta: to-integer ((arctangent/radians goal-theta) * 180 / pi)

        if heading/x < co-ords/x [goal-theta: goal-theta + 180]
        if goal-theta < 0 [goal-theta: goal-theta + 360]

        return to-integer (goal-theta / 15) * 15
    ]
]

update-flock: func [
    {Updates the entire flock by first finding out where each boid is going to
        move to, then moving the flock en masse.}
    boids [block!] "boid flock block"
][
    foreach boid boids [boid/move boid/general-heading boids]
    foreach boid boids [boid/refresh]
    show world
]

create-boids: func [
    {Given a boids flock block, the function appends or removes boids until the
        number of boids is equal to num}
    boids [block!] "Global boids flock block"
    num [integer!] "New number of boids in flock block"
    range [object!] "Permitted co-ordinates range"
    /init "Initialises the Boids flock block"
    /local clr
][
    if init [remove/part boids (length? boids)]
    either num > (length? boids) [
        for i ((length? boids) + 1) num 1 [
            clr: pick [255.0.0 0.255.0] (random species)
            append boids make boid-face compose/deep [
                id: i
                co-ords: make object! [x: (random range/x) y: (random range/y)]
                theta: (random 24) * 15
                aim: make object! [x: co-ords/x y: co-ords/y]
                feathers: clr
                either pos: find effect 'colorize [
                    change next pos clr
                ][
                    append effect [colorize (clr)]
                ]
                
            ]
        ]
    ] [remove/part boids ((length? boids) - num)]

    foreach boid boids [boid/refresh]
    world/pane: boids
    show world
]

view-boids: func [
    /local world-pair world-size boids-layout
][
    world-pair: 400x300
    world-size: make object! [x: world-pair/x y: world-pair/y]

    boids-layout: layout [
        ;backdrop effect compose [gradient 1x1 (aqua) (sky)]
        across origin 4x4 space 4x4
        style lbl label 100 right
        style sld slider 200x20
        style btn btn 90
        style chc choice 200x20
        style rot rotary 200x20
        
        world: box world-pair black
        rate 0 feel [engage: func [face act evt] [update-flock boids]]
        return

        across lbl "boids:" num-sld: sld [
            boid-num: num-lbl/text: to-integer (num-sld/data * max-boid + 1)
            show num-lbl
            create-boids boids boid-num world-size
        ] num-lbl: label 36 center to-string boid-num return

        lbl "radius:" rds-sld: sld [
            radius: rds-lbl/text: to-integer (rds-sld/data * max-radius + 1)
            show rds-lbl
        ] rds-lbl: label 36 center to-string radius return

        lbl "crowded:" crd-sld: sld [
            crowded: crd-lbl/text: to-integer (crd-sld/data * max-crowded + 1)
            show crd-lbl
        ] crd-lbl: label 36 center to-string crowded return

        lbl "change:" chg-sld: sld [
            change-rate: chg-lbl/text: to-integer (chg-sld/data * max-change)
            show chg-lbl
        ] chg-lbl: label 36 center to-string change-rate return

        lbl "species:"
        spc-rot: rot "one" "two" [
            species: switch (first spc-rot/data) ["one" [1] "two" [2]]
            create-boids/init boids boid-num world-size
        ] return

        below pad 20 across
        pad 310
        btn #"r" "randomise" [create-boids/init boids boid-num world-size]
    ]
    create-boids boids boid-num world-size
    
    num-sld/data: boid-num / max-boid
    rds-sld/data: radius / max-radius
    crd-sld/data: crowded / max-crowded
    chg-sld/data: change-rate / max-change

    view center-face boids-layout
]

view-boids