A 2D tile-based sandbox game.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

WorldPlane.cc 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. #include "WorldPlane.h"
  2. #include <math.h>
  3. #include <iostream>
  4. #include <algorithm>
  5. #include "log.h"
  6. #include "World.h"
  7. #include "Game.h"
  8. #include "Clock.h"
  9. namespace Swan {
  10. static ChunkPos chunkPos(TilePos pos) {
  11. // This might look weird, but it reduces an otherwise complex series of operations
  12. // including conditional branches into like four x64 instructions.
  13. // Basically, the problem is that we want 'floor(pos.x / CHUNK_WIDTH)', but
  14. // integer division rounds towards zero, it doesn't round down.
  15. // Solution: Move the position far to the right in the number line, do the math there
  16. // with integer division which always rounds down (because the numbers are always >0),
  17. // then move the result back to something hovering around 0 again.
  18. return ChunkPos(
  19. ((long long)pos.x + (LLONG_MAX / 2) + 1) / CHUNK_WIDTH - ((LLONG_MAX / 2) / CHUNK_WIDTH) - 1,
  20. ((long long)pos.y + (LLONG_MAX / 2) + 1) / CHUNK_HEIGHT - ((LLONG_MAX / 2) / CHUNK_HEIGHT) - 1);
  21. }
  22. static Chunk::RelPos relPos(TilePos pos) {
  23. // This uses a similar trick to chunkPos to turn a mess of conditional moves
  24. // and math instructions into literally one movabs and one 'and'
  25. return Chunk::RelPos(
  26. (pos.x + (long long)CHUNK_WIDTH * ((LLONG_MAX / 2) / CHUNK_WIDTH)) % CHUNK_WIDTH,
  27. (pos.y + (long long)CHUNK_HEIGHT * ((LLONG_MAX / 2) / CHUNK_HEIGHT)) % CHUNK_HEIGHT);
  28. }
  29. Context WorldPlane::getContext() {
  30. return {
  31. .game = *world_->game_,
  32. .world = *world_,
  33. .plane = *this,
  34. };
  35. }
  36. WorldPlane::WorldPlane(
  37. ID id, World *world, std::unique_ptr<WorldGen> gen,
  38. std::vector<std::unique_ptr<EntityCollection>> &&colls):
  39. id_(id), world_(world), gen_(std::move(gen)),
  40. entColls_(std::move(colls)),
  41. lighting_(std::make_unique<LightServer>(*this)) {
  42. for (auto &coll: entColls_) {
  43. entCollsByType_[coll->type()] = coll.get();
  44. entCollsByName_[coll->name()] = coll.get();
  45. }
  46. }
  47. EntityRef WorldPlane::spawnEntity(const std::string &name, const Entity::PackObject &obj) {
  48. return entCollsByName_.at(name)->spawn(getContext(), obj);
  49. }
  50. bool WorldPlane::hasChunk(ChunkPos pos) {
  51. return chunks_.find(pos) != chunks_.end();
  52. }
  53. // This function will be a bit weird because it's a really fucking hot function.
  54. Chunk &WorldPlane::getChunk(ChunkPos pos) {
  55. // First, look through all chunks which have been in use this tick
  56. for (auto [chpos, chunk]: tickChunks_) {
  57. if (chpos == pos)
  58. return *chunk;
  59. }
  60. Chunk &chunk = slowGetChunk(pos);
  61. tickChunks_.push_back({pos, &chunk});
  62. return chunk;
  63. }
  64. Chunk &WorldPlane::slowGetChunk(ChunkPos pos) {
  65. ZoneScopedN("WorldPlane slowGetChunk");
  66. auto iter = chunks_.find(pos);
  67. // Create chunk if that turns out to be necessary
  68. if (iter == chunks_.end()) {
  69. iter = chunks_.emplace(pos, Chunk(pos)).first;
  70. Chunk &chunk = iter->second;
  71. gen_->genChunk(*this, chunk);
  72. activeChunks_.push_back(&chunk);
  73. chunkInitList_.push_back(&chunk);
  74. // Need to tell the light engine too
  75. NewLightChunk lc;
  76. for (int y = 0; y < CHUNK_HEIGHT; ++y) {
  77. for (int x = 0; x < CHUNK_WIDTH; ++x) {
  78. Tile::ID id = chunk.getTileID({ x, y });
  79. Tile &tile = world_->getTileByID(id);
  80. if (tile.isOpaque) {
  81. lc.blocks[y * CHUNK_HEIGHT + x] = true;
  82. }
  83. if (tile.lightLevel > 0) {
  84. lc.lightSources[{x, y}] = tile.lightLevel;
  85. }
  86. }
  87. }
  88. lighting_->onChunkAdded(pos, std::move(lc));
  89. // Otherwise, it might not be active, so let's activate it
  90. } else if (!iter->second.isActive()) {
  91. iter->second.keepActive();
  92. activeChunks_.push_back(&iter->second);
  93. chunkInitList_.push_back(&iter->second);
  94. }
  95. return iter->second;
  96. }
  97. void WorldPlane::setTileID(TilePos pos, Tile::ID id) {
  98. Chunk &chunk = getChunk(chunkPos(pos));
  99. Chunk::RelPos rp = relPos(pos);
  100. Tile::ID old = chunk.getTileID(rp);
  101. if (id != old) {
  102. Tile &newTile = world_->getTileByID(id);
  103. Tile &oldTile = world_->getTileByID(old);
  104. chunk.setTileID(rp, id);
  105. if (!oldTile.isOpaque && newTile.isOpaque) {
  106. lighting_->onSolidBlockAdded(pos);
  107. } else if (oldTile.isOpaque && !newTile.isOpaque) {
  108. lighting_->onSolidBlockRemoved(pos);
  109. }
  110. if (newTile.lightLevel != oldTile.lightLevel) {
  111. if (oldTile.lightLevel > 0) {
  112. removeLight(pos, oldTile.lightLevel);
  113. }
  114. if (newTile.lightLevel > 0) {
  115. addLight(pos, newTile.lightLevel);
  116. }
  117. }
  118. if (newTile.onSpawn) {
  119. newTile.onSpawn(getContext(), pos);
  120. }
  121. }
  122. }
  123. void WorldPlane::setTile(TilePos pos, const std::string &name) {
  124. setTileID(pos, world_->getTileID(name));
  125. }
  126. Tile::ID WorldPlane::getTileID(TilePos pos) {
  127. return getChunk(chunkPos(pos)).getTileID(relPos(pos));
  128. }
  129. Tile &WorldPlane::getTile(TilePos pos) {
  130. return world_->getTileByID(getTileID(pos));
  131. }
  132. Iter<Entity *> WorldPlane::getEntsInArea(Vec2 center, float radius) {
  133. return Iter<Entity *>([] { return std::nullopt; });
  134. // TODO: this
  135. /*
  136. return mapFilter(entities_.begin(), entities_.end(), [=](std::unique_ptr<Entity> &ent)
  137. -> std::optional<Entity *> {
  138. // Filter out things which don't have bodies
  139. auto *hasBody = dynamic_cast<BodyTrait::HasBody *>(ent.get());
  140. if (hasBody == nullptr)
  141. return std::nullopt;
  142. // Filter out things which are too far away from 'center'
  143. auto &body = hasBody->getBody();
  144. auto bounds = body.getBounds();
  145. Vec2 entcenter = bounds.pos + (bounds.size / 2);
  146. auto dist = (entcenter - center).length();
  147. if (dist > radius)
  148. return std::nullopt;
  149. return ent.get();
  150. });
  151. */
  152. }
  153. EntityRef WorldPlane::spawnPlayer() {
  154. return gen_->spawnPlayer(getContext());
  155. }
  156. void WorldPlane::breakTile(TilePos pos) {
  157. // If the block is already air, do nothing
  158. Tile::ID id = getTileID(pos);
  159. Tile::ID air = world_->getTileID("@::air");
  160. if (id == air)
  161. return;
  162. // Change tile to air and emit event
  163. setTileID(pos, air);
  164. world_->evtTileBreak_.emit(getContext(), pos, world_->getTileByID(id));
  165. }
  166. Cygnet::Color WorldPlane::backgroundColor() {
  167. return gen_->backgroundColor(world_->player_->pos);
  168. }
  169. void WorldPlane::draw(Cygnet::Renderer &rnd) {
  170. ZoneScopedN("WorldPlane draw");
  171. std::lock_guard<std::mutex> lock(mut_);
  172. auto ctx = getContext();
  173. auto &pbody = *(world_->player_);
  174. gen_->drawBackground(ctx, rnd, pbody.pos);
  175. ChunkPos pcpos = ChunkPos(
  176. (int)floor(pbody.pos.x / CHUNK_WIDTH),
  177. (int)floor(pbody.pos.y / CHUNK_HEIGHT));
  178. for (int x = -1; x <= 1; ++x) {
  179. for (int y = -1; y <= 1; ++y) {
  180. auto iter = chunks_.find(pcpos + ChunkPos(x, y));
  181. if (iter != chunks_.end())
  182. iter->second.draw(ctx, rnd);
  183. }
  184. }
  185. for (auto &coll: entColls_) {
  186. coll->draw(ctx, rnd);
  187. }
  188. lighting_->flip();
  189. /*
  190. if (debugBoxes_.size() > 0) {
  191. for (auto &pos: debugBoxes_) {
  192. rnd.drawRect(pos, Vec2(1, 1));
  193. }
  194. }
  195. TODO */
  196. }
  197. void WorldPlane::update(float dt) {
  198. ZoneScopedN("WorldPlane update");
  199. std::lock_guard<std::mutex> lock(mut_);
  200. auto ctx = getContext();
  201. debugBoxes_.clear();
  202. // Just init one chunk per frame
  203. if (chunkInitList_.size() > 0) {
  204. Chunk *chunk = chunkInitList_.front();
  205. chunkInitList_.pop_front();
  206. TilePos base = chunk->pos_ * Vec2i{CHUNK_WIDTH, CHUNK_HEIGHT};
  207. for (int y = 0; y < CHUNK_HEIGHT; ++y) {
  208. for (int x = 0; x < CHUNK_WIDTH; ++x) {
  209. Tile::ID id = chunk->getTileID({x, y});
  210. Tile &tile = world_->getTileByID(id);
  211. if (tile.onSpawn) {
  212. tile.onSpawn(getContext(), base + Vec2i{x, y});
  213. }
  214. }
  215. }
  216. }
  217. for (auto &coll: entColls_)
  218. coll->update(ctx, dt);
  219. }
  220. void WorldPlane::tick(float dt) {
  221. ZoneScopedN("WorldPlane tick");
  222. std::lock_guard<std::mutex> lock(mut_);
  223. auto ctx = getContext();
  224. // Any chunk which has been in use since last tick should be kept alive
  225. for (std::pair<ChunkPos, Chunk *> &ch: tickChunks_)
  226. ch.second->keepActive();
  227. tickChunks_.clear();
  228. for (auto &coll: entColls_)
  229. coll->tick(ctx, dt);
  230. // Tick all chunks, figure out if any of them should be deleted or compressed
  231. auto iter = activeChunks_.begin();
  232. auto last = activeChunks_.end();
  233. while (iter != last) {
  234. auto &chunk = *iter;
  235. auto action = chunk->tick(dt);
  236. switch (action) {
  237. case Chunk::TickAction::DEACTIVATE:
  238. info << "Compressing inactive modified chunk " << chunk->pos_;
  239. chunk->compress(world_->game_->renderer_);
  240. iter = activeChunks_.erase(iter);
  241. last = activeChunks_.end();
  242. break;
  243. case Chunk::TickAction::DELETE:
  244. info << "Deleting inactive unmodified chunk " << chunk->pos_;
  245. chunk->destroy(world_->game_->renderer_);
  246. chunks_.erase(chunk->pos_);
  247. iter = activeChunks_.erase(iter);
  248. last = activeChunks_.end();
  249. break;
  250. case Chunk::TickAction::NOTHING:
  251. ++iter;
  252. break;
  253. }
  254. }
  255. }
  256. void WorldPlane::debugBox(TilePos pos) {
  257. debugBoxes_.push_back(pos);
  258. }
  259. void WorldPlane::addLight(TilePos pos, float level) {
  260. getChunk(chunkPos(pos));
  261. lighting_->onLightAdded(pos, level);
  262. }
  263. void WorldPlane::removeLight(TilePos pos, float level) {
  264. getChunk(chunkPos(pos));
  265. lighting_->onLightRemoved(pos, level);
  266. }
  267. void WorldPlane::onLightChunkUpdated(const LightChunk &chunk, ChunkPos pos) {
  268. std::lock_guard<std::mutex> lock(mut_);
  269. Chunk &realChunk = getChunk(pos);
  270. realChunk.setLightData(chunk.lightLevels);
  271. }
  272. }