├── LICENSE.md ├── README.md ├── gif.gif ├── grass.py ├── grass ├── grass_0.png ├── grass_1.png ├── grass_2.png ├── grass_3.png ├── grass_4.png └── grass_5.png └── grass_demo.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 DaFluffyPotato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### An efficient pure Python/Pygame grass module. Feel free to use however you'd like. 2 | 3 | ![](/gif.gif) 4 | 5 | Please see grass_demo.py for an example of how to use GrassManager. 6 | 7 | Important functions and objects: 8 | 9 | ### `grass.GrassManager(grass_path, tile_size=15, shade_amount=100, stiffness=360, max_unique=10, place_range=[1, 1], padding=13)` 10 | 11 | Initialize a grass manager object. 12 | 13 | ### `grass.GrassManager.enable_ground_shadows(shadow_strength=40, shadow_radius=2, shadow_color=(0, 0, 1), shadow_shift=(0, 0))` 14 | 15 | Enables shadows for individual blades (or disables if shadow_strength is set to 0). shadow_radius determines the radius of the 16 | shadow circle, shadow_color determines the base color of the shadow, and shadow_shift is the offset of the shadow relative to 17 | the base of the blade. 18 | 19 | ### `grass.GrassManager.place_tile(location, density, grass_options)` 20 | 21 | Adds new grass. location specifies which "tile" the grass should be placed at, so the pixel-position of the tile will depend 22 | on the GrassManager's tile size. density specifies the number of blades the tile should have and grass_options is a list of blade 23 | image IDs that can be used to form the grass tile. The blade image IDs are the alphabetical index of the image in the asset folder 24 | provided for the blades. Please note that you can specify the same ID multiple times in the grass options to make it more likely 25 | to appear. 26 | 27 | ### `grass.GrassManager.apply_force(location, radius, dropoff)` 28 | 29 | Applies a physical force to the grass at the given location. The radius is the range at which the grass should be fully bent over at. 30 | The dropoff is the distance past the end of the "radius" that it should take for the force to be eased into nothing. 31 | 32 | ### `grass.GrassManager.update_render(surf, dt, offset=(0, 0), rot_function=None)` 33 | 34 | Renders the grass onto a surface and applies updates. surf is the surface rendered onto, dt is the amount of seconds passed since the 35 | last update, offset is the camera's offset, and the rot_function is for custom rotational modifiers. The rot_function passed as an 36 | argument should take an X and Y value while returning a rotation value. Take a look at grass_demo.py to how you can create a wind 37 | effect with this. 38 | 39 | Notes about configuration of the GrassManager: 40 | 41 | `` 42 | 43 | The only required argument. It points to a folder with all of the blade images. The names of the images don't matter. When creating 44 | tiles, you provide a list of IDs, which are the indexes of the blade images that can be used. The indexes are based on alphabetical 45 | order, so if be careful with numbers like img_2.png and img_10.png because img_10.png will come first. It's recommended that you do 46 | img_02.png and img_10.png if you need double digits. 47 | 48 | `` 49 | 50 | This is used to define the "tile size" for the grass. If your game is tile based, your actual tile size should be some multiple of the 51 | number given here. This affects a couple things. First, it defines the smallest section of grass that can be individually affected by 52 | efficient rotation modifications (such as wind). Second, it affects performance. If the size is too large, an unnecessary amount of 53 | calculations will be made for applied forces. If the size is too small, there will be too many images render, which will also reduce 54 | performance. It's good to play around with this number for proper optimization. 55 | 56 | `` 57 | 58 | The shade amount determines the maximum amount of transparency that can be applied to a blade as it tilts away from its base angle. 59 | This should be a value from 0 to 255. 60 | 61 | `` 62 | 63 | This determines how fast the blades of grass bounce back into place after being rotated by an applied force. 64 | 65 | `` 66 | 67 | This determines the maximum amount of variants that can be used for a specific tile configuration (a configuration is the combination 68 | of the amount of blades of grass and the possible set of blade images that can be used for a tile). If the number is too high, the 69 | application will use a large amount of RAM to store all of the cached tile images. If the number is too low, you'll start to see 70 | consistent patterns appear in the layout of your grass tiles. 71 | 72 | `` 73 | 74 | This determines the vertical range that the base of the blades can be placed at. The range should be any range in the range of 0 to 1. 75 | Use [1, 1] when you want the base of the blades to be placed at the bottom of the tile (useful for platformers) or [0, 1] if you want 76 | the blades to be placed anywhere in the tile (useful for top-down games). 77 | 78 | `` 79 | 80 | This is the amount of spacial padding the tile images have to fit the blades spilling outside the bounds of the tile. This should 81 | probably be set to the height of your tallest blade of grass. 82 | 83 | `` 84 | 85 | This is the amount of precision the angles can have. The lower precision, the choppier the motion will appear. However, using a high 86 | precision will use a large amount of RAM. The integer given is the amount of distinct angles allowed in a 90 degree range. 87 | -------------------------------------------------------------------------------- /gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaFluffyPotato/pygame-grass/e30460b715982341fa2011a02af1bc0bef9ded14/gif.gif -------------------------------------------------------------------------------- /grass.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Version 1.0 3 | 4 | An efficient pure Python/Pygame grass module written by DaFluffyPotato. Feel free to use however you'd like. 5 | 6 | Please see grass_demo.py for an example of how to use GrassManager. 7 | 8 | Important functions and objects: 9 | 10 | -> grass.GrassManager(grass_path, tile_size=15, shade_amount=100, stiffness=360, max_unique=10, place_range=[1, 1], padding=13) 11 | Initialize a grass manager object. 12 | 13 | -> grass.GrassManager.enable_ground_shadows(shadow_strength=40, shadow_radius=2, shadow_color=(0, 0, 1), shadow_shift=(0, 0)) 14 | Enables shadows for individual blades (or disables if shadow_strength is set to 0). shadow_radius determines the radius of the 15 | shadow circle, shadow_color determines the base color of the shadow, and shadow_shift is the offset of the shadow relative to 16 | the base of the blade. 17 | 18 | -> grass.GrassManager.place_tile(location, density, grass_options) 19 | Adds new grass. location specifies which "tile" the grass should be placed at, so the pixel-position of the tile will depend 20 | on the GrassManager's tile size. density specifies the number of blades the tile should have and grass_options is a list of blade 21 | image IDs that can be used to form the grass tile. The blade image IDs are the alphabetical index of the image in the asset folder 22 | provided for the blades. Please note that you can specify the same ID multiple times in the grass options to make it more likely 23 | to appear. 24 | 25 | -> grass.GrassManager.apply_force(location, radius, dropoff) 26 | Applies a physical force to the grass at the given location. The radius is the range at which the grass should be fully bent over at. 27 | The dropoff is the distance past the end of the "radius" that it should take for the force to be eased into nothing. 28 | 29 | -> grass.GrassManager.update_render(surf, dt, offset=(0, 0), rot_function=None) 30 | Renders the grass onto a surface and applies updates. surf is the surface rendered onto, dt is the amount of seconds passed since the 31 | last update, offset is the camera's offset, and the rot_function is for custom rotational modifiers. The rot_function passed as an 32 | argument should take an X and Y value while returning a rotation value. Take a look at grass_demo.py to how you can create a wind 33 | effect with this. 34 | 35 | Notes about configuration of the GrassManager: 36 | 37 | 38 | The only required argument. It points to a folder with all of the blade images. The names of the images don't matter. When creating 39 | tiles, you provide a list of IDs, which are the indexes of the blade images that can be used. The indexes are based on alphabetical 40 | order, so if be careful with numbers like img_2.png and img_10.png because img_10.png will come first. It's recommended that you do 41 | img_02.png and img_10.png if you need double digits. 42 | 43 | 44 | This is used to define the "tile size" for the grass. If your game is tile based, your actual tile size should be some multiple of the 45 | number given here. This affects a couple things. First, it defines the smallest section of grass that can be individually affected by 46 | efficient rotation modifications (such as wind). Second, it affects performance. If the size is too large, an unnecessary amount of 47 | calculations will be made for applied forces. If the size is too small, there will be too many images render, which will also reduce 48 | performance. It's good to play around with this number for proper optimization. 49 | 50 | 51 | The shade amount determines the maximum amount of transparency that can be applied to a blade as it tilts away from its base angle. 52 | This should be a value from 0 to 255. 53 | 54 | 55 | This determines how fast the blades of grass bounce back into place after being rotated by an applied force. 56 | 57 | 58 | This determines the maximum amount of variants that can be used for a specific tile configuration (a configuration is the combination 59 | of the amount of blades of grass and the possible set of blade images that can be used for a tile). If the number is too high, the 60 | application will use a large amount of RAM to store all of the cached tile images. If the number is too low, you'll start to see 61 | consistent patterns appear in the layout of your grass tiles. 62 | 63 | 64 | This determines the vertical range that the base of the blades can be placed at. The range should be any range in the range of 0 to 1. 65 | Use [1, 1] when you want the base of the blades to be placed at the bottom of the tile (useful for platformers) or [0, 1] if you want 66 | the blades to be placed anywhere in the tile (useful for top-down games). 67 | 68 | 69 | This is the amount of spacial padding the tile images have to fit the blades spilling outside the bounds of the tile. This should 70 | probably be set to the height of your tallest blade of grass. 71 | ''' 72 | 73 | import os 74 | import random 75 | import math 76 | from copy import deepcopy 77 | 78 | import pygame 79 | 80 | def normalize(val, amt, target): 81 | if val > target + amt: 82 | val -= amt 83 | elif val < target - amt: 84 | val += amt 85 | else: 86 | val = target 87 | return val 88 | 89 | # the main object that manages the grass system 90 | class GrassManager: 91 | def __init__(self, grass_path, tile_size=15, shade_amount=100, stiffness=360, max_unique=10, place_range=[1, 1], padding=13): 92 | # asset manager 93 | self.ga = GrassAssets(grass_path, self) 94 | 95 | # caching variables 96 | self.grass_id = 0 97 | self.grass_cache = {} 98 | self.shadow_cache = {} 99 | self.formats = {} 100 | 101 | # tile data 102 | self.grass_tiles = {} 103 | 104 | # config 105 | self.tile_size = tile_size 106 | self.shade_amount = shade_amount 107 | self.stiffness = stiffness 108 | self.max_unique = max_unique 109 | self.vertical_place_range = place_range 110 | self.ground_shadow = [0, (0, 0, 0), 100, (0, 0)] 111 | self.padding = padding 112 | 113 | # enables circular shadows that appear below each blade of grass 114 | def enable_ground_shadows(self, shadow_strength=40, shadow_radius=2, shadow_color=(0, 0, 1), shadow_shift=(0, 0)): 115 | # don't interfere with colorkey 116 | if shadow_color == (0, 0, 0): 117 | shadow_color = (0, 0, 1) 118 | 119 | self.ground_shadow = [shadow_radius, shadow_color, shadow_strength, shadow_shift] 120 | 121 | # either creates a new grass tile layout or returns an existing one if the cap has been hit 122 | def get_format(self, format_id, data, tile_id): 123 | if format_id not in self.formats: 124 | self.formats[format_id] = {'count': 1, 'data': [(tile_id, data)]} 125 | elif self.formats[format_id]['count'] >= self.max_unique: 126 | return deepcopy(random.choice(self.formats[format_id]['data'])) 127 | else: 128 | self.formats[format_id]['count'] += 1 129 | self.formats[format_id]['data'].append((tile_id, data)) 130 | 131 | # attempt to place a new grass tile 132 | def place_tile(self, location, density, grass_options): 133 | # ignore if a tile was already placed in this location 134 | if tuple(location) not in self.grass_tiles: 135 | self.grass_tiles[tuple(location)] = GrassTile(self.tile_size, (location[0] * self.tile_size, location[1] * self.tile_size), density, grass_options, self.ga, self) 136 | 137 | # apply a force to the grass that causes the grass to bend away 138 | def apply_force(self, location, radius, dropoff): 139 | location = (int(location[0]), int(location[1])) 140 | grid_pos = (int(location[0] // self.tile_size), int(location[1] // self.tile_size)) 141 | tile_range = math.ceil((radius + dropoff) / self.tile_size) 142 | for y in range(tile_range * 2 + 1): 143 | y = y - tile_range 144 | for x in range(tile_range * 2 + 1): 145 | x = x - tile_range 146 | pos = (grid_pos[0] + x, grid_pos[1] + y) 147 | if pos in self.grass_tiles: 148 | self.grass_tiles[pos].apply_force(location, radius, dropoff) 149 | 150 | # an update and render combination function 151 | def update_render(self, surf, dt, offset=(0, 0), rot_function=None): 152 | visible_tile_range = (int(surf.get_width() // self.tile_size) + 1, int(surf.get_height() // self.tile_size) + 1) 153 | base_pos = (int(offset[0] // self.tile_size), int(offset[1] // self.tile_size)) 154 | 155 | # get list of grass tiles to render based on visible area 156 | render_list = [] 157 | for y in range(visible_tile_range[1]): 158 | for x in range(visible_tile_range[0]): 159 | pos = (base_pos[0] + x, base_pos[1] + y) 160 | if pos in self.grass_tiles: 161 | render_list.append(pos) 162 | 163 | # render shadow if applicable 164 | if self.ground_shadow[0]: 165 | for pos in render_list: 166 | self.grass_tiles[pos].render_shadow(surf, offset=(offset[0] - self.ground_shadow[3][0], offset[1] - self.ground_shadow[3][1])) 167 | 168 | # render the grass tiles 169 | for pos in render_list: 170 | tile = self.grass_tiles[pos] 171 | tile.render(surf, dt, offset=offset) 172 | if rot_function: 173 | tile.set_rotation(rot_function(tile.loc[0], tile.loc[1])) 174 | 175 | # an asset manager that contains functionality for rendering blades of grass 176 | class GrassAssets: 177 | def __init__(self, path, gm): 178 | self.gm = gm 179 | self.blades = [] 180 | 181 | # load in blade images 182 | for blade in sorted(os.listdir(path)): 183 | img = pygame.image.load(path + '/' + blade).convert() 184 | img.set_colorkey((0, 0, 0)) 185 | self.blades.append(img) 186 | 187 | def render_blade(self, surf, blade_id, location, rotation): 188 | # rotate the blade 189 | rot_img = pygame.transform.rotate(self.blades[blade_id], rotation) 190 | 191 | # shade the blade of grass based on its rotation 192 | shade = pygame.Surface(rot_img.get_size()) 193 | shade_amt = self.gm.shade_amount * (abs(rotation) / 90) 194 | shade.set_alpha(shade_amt) 195 | rot_img.blit(shade, (0, 0)) 196 | 197 | # render the blade 198 | surf.blit(rot_img, (location[0] - rot_img.get_width() // 2, location[1] - rot_img.get_height() // 2)) 199 | 200 | # the grass tile object that contains data for the blades 201 | class GrassTile: 202 | def __init__(self, tile_size, location, amt, config, ga, gm): 203 | self.ga = ga 204 | self.gm = gm 205 | self.loc = location 206 | self.size = tile_size 207 | self.blades = [] 208 | self.master_rotation = 0 209 | self.precision = 30 210 | self.padding = self.gm.padding 211 | self.inc = 90 / self.precision 212 | 213 | # generate blade data 214 | y_range = self.gm.vertical_place_range[1] - self.gm.vertical_place_range[0] 215 | for i in range(amt): 216 | new_blade = random.choice(config) 217 | 218 | y_pos = self.gm.vertical_place_range[0] 219 | if y_range: 220 | y_pos = random.random() * y_range + self.gm.vertical_place_range[0] 221 | 222 | self.blades.append([(random.random() * self.size, y_pos * self.size), new_blade, random.random() * 30 - 15]) 223 | 224 | # layer back to front 225 | self.blades.sort(key=lambda x: x[1]) 226 | 227 | # get next ID 228 | self.base_id = self.gm.grass_id 229 | self.gm.grass_id += 1 230 | 231 | # check if the blade data needs to be overwritten with a previous layout to save RAM usage 232 | format_id = (amt, tuple(config)) 233 | overwrite = self.gm.get_format(format_id, self.blades, self.base_id) 234 | if overwrite: 235 | self.blades = overwrite[1] 236 | self.base_id = overwrite[0] 237 | 238 | # custom_blade_data is used when the blade's current state should not be cached. all grass tiles will try to return to a cached state 239 | self.custom_blade_data = None 240 | 241 | self.update_render_data() 242 | 243 | # apply a force that affects each blade individually based on distance instead of the rotation of the entire tile 244 | def apply_force(self, force_point, force_radius, force_dropoff): 245 | if not self.custom_blade_data: 246 | self.custom_blade_data = [None] * len(self.blades) 247 | 248 | for i, blade in enumerate(self.blades): 249 | orig_data = self.custom_blade_data[i] 250 | dis = math.sqrt((self.loc[0] + blade[0][0] - force_point[0]) ** 2 + (self.loc[1] + blade[0][1] - force_point[1]) ** 2) 251 | max_force = False 252 | if dis < force_radius: 253 | force = 2 254 | else: 255 | dis = max(0, dis - force_radius) 256 | force = 1 - min(dis / force_dropoff, 1) 257 | dir = 1 if force_point[0] > (self.loc[0] + blade[0][0]) else -1 258 | # don't update unless force is greater 259 | if not self.custom_blade_data[i] or abs(self.custom_blade_data[i][2] - self.blades[i][2]) <= abs(force) * 90: 260 | self.custom_blade_data[i] = [blade[0], blade[1], blade[2] + dir * force * 90] 261 | 262 | # update the identifier used to find a valid cached image 263 | def update_render_data(self): 264 | self.render_data = (self.base_id, self.master_rotation) 265 | self.true_rotation = self.inc * self.master_rotation 266 | 267 | # set new master tile rotation 268 | def set_rotation(self, rotation): 269 | self.master_rotation = rotation 270 | self.update_render_data() 271 | 272 | # render the tile's image based on its current state and return the data 273 | def render_tile(self, render_shadow=False): 274 | # make a new padded surface (to fit blades spilling out of the tile) 275 | surf = pygame.Surface((self.size + self.padding * 2, self.size + self.padding * 2)) 276 | surf.set_colorkey((0, 0, 0)) 277 | 278 | # use custom_blade_data if it's active (uncached). otherwise use the base data (cached). 279 | if self.custom_blade_data: 280 | blades = self.custom_blade_data 281 | else: 282 | blades = self.blades 283 | 284 | # render the shadows of each blade if applicable 285 | if render_shadow: 286 | shadow_surf = pygame.Surface(surf.get_size()) 287 | shadow_surf.set_colorkey((0, 0, 0)) 288 | for blade in self.blades: 289 | pygame.draw.circle(shadow_surf, self.gm.ground_shadow[1], (blade[0][0] + self.padding, blade[0][1] + self.padding), self.gm.ground_shadow[0]) 290 | shadow_surf.set_alpha(self.gm.ground_shadow[2]) 291 | 292 | # render each blade using the asset manager 293 | for blade in blades: 294 | self.ga.render_blade(surf, blade[1], (blade[0][0] + self.padding, blade[0][1] + self.padding), max(-90, min(90, blade[2] + self.true_rotation))) 295 | 296 | # return surf and shadow_surf if applicable 297 | if render_shadow: 298 | return surf, shadow_surf 299 | else: 300 | return surf 301 | 302 | # draw the shadow image for the tile 303 | def render_shadow(self, surf, offset=(0, 0)): 304 | if self.gm.ground_shadow[0] and (self.base_id in self.gm.shadow_cache): 305 | surf.blit(self.gm.shadow_cache[self.base_id], (self.loc[0] - offset[0] - self.padding, self.loc[1] - offset[1] - self.padding)) 306 | 307 | # draw the grass itself 308 | def render(self, surf, dt, offset=(0, 0)): 309 | # render a new grass tile image if using custom uncached data otherwise use cached data if possible 310 | if self.custom_blade_data: 311 | surf.blit(self.render_tile(), (self.loc[0] - offset[0] - self.padding, self.loc[1] - offset[1] - self.padding)) 312 | 313 | else: 314 | # check if a new cached image needs to be generated and use the cached data if not (also cache shadow if necessary) 315 | if (self.render_data not in self.gm.grass_cache) and (self.gm.ground_shadow[0] and (self.base_id not in self.gm.shadow_cache)): 316 | grass_img, shadow_img = self.render_tile(render_shadow=True) 317 | self.gm.grass_cache[self.render_data] = grass_img 318 | self.gm.shadow_cache[self.base_id] = shadow_img 319 | elif self.render_data not in self.gm.grass_cache: 320 | self.gm.grass_cache[self.render_data] = self.render_tile() 321 | 322 | # render image from the cache 323 | surf.blit(self.gm.grass_cache[self.render_data], (self.loc[0] - offset[0] - self.padding, self.loc[1] - offset[1] - self.padding)) 324 | 325 | # attempt to move blades back to their base position 326 | if self.custom_blade_data: 327 | matching = True 328 | for i, blade in enumerate(self.custom_blade_data): 329 | blade[2] = normalize(blade[2], self.gm.stiffness * dt, self.blades[i][2]) 330 | if blade[2] != self.blades[i][2]: 331 | matching = False 332 | # mark the data as non-custom once in base position so the cache can be used 333 | if matching: 334 | self.custom_blade_data = None 335 | -------------------------------------------------------------------------------- /grass/grass_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaFluffyPotato/pygame-grass/e30460b715982341fa2011a02af1bc0bef9ded14/grass/grass_0.png -------------------------------------------------------------------------------- /grass/grass_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaFluffyPotato/pygame-grass/e30460b715982341fa2011a02af1bc0bef9ded14/grass/grass_1.png -------------------------------------------------------------------------------- /grass/grass_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaFluffyPotato/pygame-grass/e30460b715982341fa2011a02af1bc0bef9ded14/grass/grass_2.png -------------------------------------------------------------------------------- /grass/grass_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaFluffyPotato/pygame-grass/e30460b715982341fa2011a02af1bc0bef9ded14/grass/grass_3.png -------------------------------------------------------------------------------- /grass/grass_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaFluffyPotato/pygame-grass/e30460b715982341fa2011a02af1bc0bef9ded14/grass/grass_4.png -------------------------------------------------------------------------------- /grass/grass_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaFluffyPotato/pygame-grass/e30460b715982341fa2011a02af1bc0bef9ded14/grass/grass_5.png -------------------------------------------------------------------------------- /grass_demo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import math 4 | import random 5 | 6 | import pygame 7 | from pygame.locals import * 8 | 9 | import grass 10 | 11 | # set up pygame 12 | pygame.init() 13 | pygame.display.set_caption('grass demo') 14 | 15 | screen = pygame.display.set_mode((600, 600), 0, 32) 16 | display = pygame.Surface((300, 300)) 17 | 18 | clock = pygame.time.Clock() 19 | 20 | # set up the grass manager and enable shadows 21 | gm = grass.GrassManager('grass', tile_size=10, stiffness=600, max_unique=5, place_range=[0, 1]) 22 | gm.enable_ground_shadows(shadow_radius=4, shadow_color=(0, 0, 1), shadow_shift=(1, 2)) 23 | 24 | # fill in the base square 25 | for y in range(20): 26 | y += 5 27 | for x in range(20): 28 | x += 5 29 | v = random.random() 30 | if v > 0.1: 31 | gm.place_tile((x, y), int(v * 12), [0, 1, 2, 3, 4]) 32 | 33 | # general variables 34 | t = 0 35 | start = time.time() 36 | 37 | scroll = [0, 0] 38 | camera_speed = 170 39 | clicking = False 40 | brush_size = 1 41 | 42 | # demo loop 43 | while True: 44 | # calc dt 45 | dt = time.time() - start 46 | start = time.time() 47 | 48 | # fill background 49 | display.fill((27, 66, 52)) 50 | 51 | # calculate mouse position in pixels 52 | mx, my = pygame.mouse.get_pos() 53 | mx /= 2 54 | my /= 2 55 | 56 | # move camera based on mouse position 57 | if mx / display.get_width() < 0.2: 58 | scroll[0] -= camera_speed * dt 59 | if mx / display.get_width() > 0.8: 60 | scroll[0] += camera_speed * dt 61 | if my / display.get_height() < 0.2: 62 | scroll[1] -= camera_speed * dt 63 | if my / display.get_height() > 0.8: 64 | scroll[1] += camera_speed * dt 65 | 66 | # apply a force from the mouse's position relative to the brush size 67 | gm.apply_force((mx + scroll[0], my + scroll[1]), 10 * brush_size, 25 * brush_size) 68 | 69 | # create an anonymous function that will apply a wind pattern to the grass when passed to the grass manager's update render function 70 | # this function uses the X offset of the grass, the master time of the application, and a sine function to create the pattern. 71 | rot_function = lambda x, y: int(math.sin(t / 60 + x / 100) * 15) 72 | 73 | # run the update/render for the grass 74 | gm.update_render(display, dt, offset=scroll, rot_function=rot_function) 75 | 76 | # draw the circles for the mouse 77 | pygame.draw.circle(display, (255, 255, 255), (mx, my), 10 * brush_size - int(clicking) * 2, 2 if not clicking else 0) 78 | if clicking: 79 | pygame.draw.circle(display, (255, 255, 255), (mx, my), 10 * brush_size, 1) 80 | 81 | # increment master time 82 | t += dt * 100 83 | 84 | # place new tiles if clicking 85 | if clicking: 86 | gm.place_tile((int((mx + scroll[0]) // gm.tile_size), int((my + scroll[1]) // gm.tile_size)), int(random.random() * 12 * brush_size + 1), [0, 1, 2, 3, 5]) 87 | # place a 3x3 pattern of tiles if the brush is full size 88 | if brush_size == 1: 89 | offsets = [(-1, 0), (-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1)] 90 | for offset in offsets: 91 | gm.place_tile((int((mx + scroll[0]) // gm.tile_size) + offset[0], int((my + scroll[1]) // gm.tile_size) + offset[1]), int(random.random() * 14 + 3), [0, 1, 2, 3, 5]) 92 | 93 | # handle events 94 | for event in pygame.event.get(): 95 | if event.type == QUIT: 96 | pygame.quit() 97 | sys.exit() 98 | if event.type == KEYDOWN: 99 | if event.key == K_ESCAPE: 100 | pygame.quit() 101 | sys.exit() 102 | if event.key == K_e: 103 | print(clock.get_fps()) 104 | 105 | if event.type == MOUSEBUTTONDOWN: 106 | if event.button == 4: 107 | brush_size = min(1, brush_size + 0.1) 108 | if event.button == 5: 109 | brush_size = max(0.1, brush_size - 0.1) 110 | if event.button == 1: 111 | clicking = True 112 | if event.type == MOUSEBUTTONUP: 113 | if event.button == 1: 114 | clicking = False 115 | 116 | # render 117 | screen.blit(pygame.transform.scale(display, screen.get_size()), (0, 0)) 118 | pygame.display.update() 119 | clock.tick(1000) 120 | --------------------------------------------------------------------------------