├── .gitignore ├── LICENSE.md ├── README.md ├── default_env.tres ├── icon.png ├── icon.png.import ├── main.tscn ├── project.godot ├── screenshot.png └── water.gd /.gitignore: -------------------------------------------------------------------------------- 1 | .import/ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Fluid 2D demo 2 | ------------------ 3 | 4 | Copyright (c) Marc Gilleron 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2D fluid cellular automata 2 | ============================= 3 | 4 | Simple demo showcasing a 2D liquid model, a bit like one you can see in Terraria or Starbound. 5 | Made with Godot engine. 6 | 7 | ![Screenshot](screenshot.png) 8 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 ) 5 | sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 ) 6 | sky_curve = 0.25 7 | ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 ) 8 | ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 ) 9 | ground_curve = 0.01 10 | sun_energy = 16.0 11 | 12 | [resource] 13 | background_mode = 2 14 | background_sky = SubResource( 1 ) 15 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zylann/fluid2d_demo/c3deb1de7855d4a12ff4c3165a7b543d2073580a/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://water.gd" type="Script" id=1] 4 | 5 | [node name="Node" type="Node"] 6 | 7 | [node name="Node2D" type="Node2D" parent="."] 8 | script = ExtResource( 1 ) 9 | 10 | [node name="Label" type="Label" parent="."] 11 | margin_left = 5.0 12 | margin_top = 7.0 13 | margin_right = 208.0 14 | margin_bottom = 89.0 15 | text = "[LMB]: place water 16 | [RMB]: place block 17 | [RMB] + Ctrl: remove block 18 | " 19 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="New Game Project" 19 | run/main_scene="res://main.tscn" 20 | config/icon="res://icon.png" 21 | 22 | [display] 23 | 24 | window/size/width=480 25 | window/size/height=480 26 | 27 | [rendering] 28 | 29 | environment/default_environment="res://default_env.tres" 30 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zylann/fluid2d_demo/c3deb1de7855d4a12ff4c3165a7b543d2073580a/screenshot.png -------------------------------------------------------------------------------- /water.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | const BLOCKING_CELL = -1 4 | 5 | var _grid_width = 30 6 | var _grid_height = 30 7 | var _cell_size = 16 8 | # That grid contains the water levels of each cell. 9 | # -1 means water can't penetrate. 10 | var _grid = [] 11 | # Deferred results of one simulation (see tick() function) 12 | var _actions = [] 13 | # how much liquid a cell can normally contain. 14 | # It can be more but different rules may apply for the cell to reach back to a normal value. 15 | var _cell_capacity = 8 16 | 17 | 18 | func _ready(): 19 | # Create grid 20 | _grid = [] 21 | _grid.resize(_grid_height) 22 | for y in len(_grid): 23 | var row = [] 24 | row.resize(_grid_width) 25 | for x in len(row): 26 | row[x] = 0 27 | _grid[y] = row 28 | 29 | 30 | func get_cell(x, y): 31 | if x < 0 or y < 0 or x >= _grid_width or y >= _grid_height: 32 | return BLOCKING_CELL 33 | return _grid[y][x] 34 | 35 | 36 | const _spread_dirs0 = [ 37 | [0, 1], 38 | [-1, 0], 39 | [1, 0] 40 | ] 41 | const _spread_dirs1 = [ 42 | [0, 1], 43 | [1, 0], 44 | [-1, 0] 45 | ] 46 | 47 | 48 | func _process(delta): 49 | if Input.is_mouse_button_pressed(BUTTON_LEFT): 50 | var pos = _world_to_grid(get_global_mouse_position()) 51 | if _is_valid_pos(pos.x, pos.y): 52 | _grid[pos.y][pos.x] = _cell_capacity 53 | 54 | for i in 1: 55 | tick() 56 | 57 | 58 | func tick(): 59 | # Run the simulation and defer the results in a list of actions. 60 | # We do it that way because it allows each cell to be simulated from the same time frame. 61 | # If we did it immediately it would alter the results as we calculate them. 62 | for y in len(_grid): 63 | var row = _grid[y] 64 | for x in len(row): 65 | var cell = row[x] 66 | if cell > 0: 67 | 68 | # if cell == 1 and randi() % 50 == 0: 69 | # # Evaporate 70 | # _actions.append([x, y, -1]) 71 | # continue 72 | 73 | var ncell_down = get_cell(x, y + 1) 74 | if ncell_down >= 0 and ncell_down < _cell_capacity: 75 | _actions.append([x, y, -1]) 76 | _actions.append([x, y + 1, 1]) 77 | cell -= 1 78 | continue 79 | 80 | var ncell_left = get_cell(x - 1, y) 81 | var ncell_right = get_cell(x + 1, y) 82 | 83 | if ncell_left == -1 and ncell_right == -1: 84 | continue 85 | # if ncell_left >= cell and ncell_right >= cell: 86 | # continue 87 | 88 | var could_evaporate = false 89 | if ncell_left == BLOCKING_CELL: 90 | if cell - ncell_right == 1: 91 | could_evaporate = true 92 | elif ncell_right == BLOCKING_CELL: 93 | if cell - ncell_left == 1: 94 | could_evaporate = true 95 | else: 96 | if cell - ncell_left == 1 or cell - ncell_right == 1: 97 | could_evaporate = true 98 | if could_evaporate and randi() % 30 == 0: 99 | _actions.append([x, y, BLOCKING_CELL]) 100 | continue 101 | 102 | var dx = null 103 | if ncell_left < 0: 104 | dx = 1 105 | elif ncell_right < 0: 106 | dx = -1 107 | elif ncell_left == ncell_right: 108 | if randi() % 2 == 0: 109 | dx = 1 110 | else: 111 | dx = -1 112 | elif ncell_left > ncell_right: 113 | dx = 1 114 | else: 115 | dx = -1 116 | 117 | var ncell = get_cell(x + dx, y) 118 | if ncell >= cell or ncell == BLOCKING_CELL: 119 | continue 120 | 121 | _actions.append([x, y, -1]) 122 | _actions.append([x + dx, y, 1]) 123 | cell -= 1 124 | if cell <= 0: 125 | continue 126 | 127 | # Apply actions 128 | for a in _actions: 129 | var x = a[0] 130 | var y = a[1] 131 | var d = a[2] 132 | var cell = _grid[y][x] 133 | cell += d 134 | _grid[y][x] = cell 135 | 136 | _actions.clear() 137 | 138 | # Trigger a redraw 139 | update() 140 | 141 | 142 | func _world_to_grid(pos): 143 | return pos / _cell_size 144 | 145 | 146 | func _is_valid_pos(x, y): 147 | return x >= 0 and y >= 0 and x < _grid_width and y < _grid_height 148 | 149 | 150 | func _input(event): 151 | if event is InputEventMouseButton: 152 | if event.pressed: 153 | var pos = _world_to_grid(event.position) 154 | if not _is_valid_pos(pos.x, pos.y): 155 | return 156 | elif event.button_index == BUTTON_RIGHT: 157 | var v = BLOCKING_CELL 158 | if event.control: 159 | v = 0 160 | _grid[pos.y][pos.x] = v 161 | update() 162 | 163 | elif event is InputEventKey: 164 | if event.pressed: 165 | tick() 166 | 167 | 168 | func _draw(): 169 | for y in len(_grid): 170 | var row = _grid[y] 171 | for x in len(row): 172 | var cell = row[x] 173 | if cell == -1: 174 | draw_rect(Rect2(x * _cell_size, y * _cell_size, _cell_size, _cell_size), Color(0.5, 0.5, 0)) 175 | elif cell > 0: 176 | var f = float(cell) / _cell_capacity 177 | var col = Color(0.5, 0.5, 1.0) 178 | if f > 1.0: 179 | col.r += f - 1.0 180 | f = clamp(f, 0.0, 1.0) 181 | if get_cell(x, y - 1) > 0: 182 | f = 1.0 183 | var r = Rect2(x * _cell_size, (y + 1.0 - f) * _cell_size, _cell_size, _cell_size * f) 184 | draw_rect(r, col) 185 | 186 | 187 | --------------------------------------------------------------------------------