/agdg/ - Amateur Game Development General

Just Like Make Game

SAVE THIS FILE: Anon.cafe Fallback File v1.1 (updated 2021-12-13)

/agdg/ - Build a Platformer (First AGDG Game Jam), June 6 to July 7

Want your event posted here? Requests accepted in this /meta/ thread.

Max message length: 20000

Drag files to upload or
click here to select them

Maximum 5 files / Maximum size: 20.00 MB

Board Rules
More

(used to delete files and postings)


Open file (3.20 KB 920x512 raylib.png)
Open file (126.94 KB 800x600 losdiabolol2.jpg)
Open file (117.51 KB 1000x750 sd2.jpg)
Open file (97.06 KB 768x576 ff12.jpeg)
ARPG project with Raylib yesdev 03/30/2023 (Thu) 03:39:45 No.486
Greetings and salutations, faggots. I'm currently working on a 2D, isometric-perspective action RPG. The goal is a Diablo-like, minus the Skinner-box loot pinata bullshit and adding a party system with simple but configurable AI party members - think Seiken Densetsu 2, 3 and Final Fantasy XII. I'm using C++ with Raylib as my primary graphics/input/output library, no "game engine" to speak of as yet. As I'm going through a bit of a refactor, I thought I would re-organize the project from scratch and document it here in the form of a tutorial slash dev blog. I'd like to use it to dump progress updates and code snippets, as well as for it to be a place to discuss Raylib and general RPG development. If you follow along, you should be able to build a similar game without too much difficulty. Disclaimer though - I am not a pro (game developer, I do a different kind of engineering for my day job), and am new to C++, so don't expect perfect code, best design practices, or anything like that. Stay tuned for an intro to Raylib, my project/build system, and design document.
Open file (70.77 KB 909x892 rl_example.png)
First, some background information. >What is Raylib? ht tps://www.raylib.com/index.html (No, I'm not going to directly link anything.) Raylib is a Games Programming Library - It is not a game engine per-se. It can be compared to other games libraries like SDL2, SFML, XNA (deprecated), and Monogame. These sorts of libraries allow you to open a window, draw stuff in it, get keyboard and mouse input, play sounds, and so forth. Do not try to compare it to Unity or Godot or Unreal Engine or other ENGINES - apples and oranges. You will still need to code all of your objects, your systems, your containers, and so forth. THIS IS NOT A BAD THING. It gives you flexibility and freedom, and helps you keep your core game (the simulation, state, etc.) separate from its on-screen representation and player input. It does include, however, a veritable shit-ton of useful functions for you to build on: window management functions, graphics primitives drawing and texture handling, asset loading and file i/o, and much more. See cheat sheet at ht tps://www.raylib.com/cheatsheet/cheatsheet.html for the full list of functions available to you just be including raylib.h in your project. >Why use Raylib? • It kicks ass. • It's free. Free as in yours to use for anything and everything. • Only need to '#include "raylib.h"' and link with '-lraylib -lGL -lm -lpthread -ldl -lrt -lX11' and you're good to go. No need to install an IDE, learn a new build system, or click "Start a New Raylib Project" or any of that shit. • Lightweight. I did a test at 1080p, 4xMSAA (It helps with font drawing at the very least) with a screen full of tiles, a player controller, and a bunch of objects and was pulling 3000+ FPS on a toaster. • Tutorials for all the basics to get you up and running, with source code. Some small games made with Raylib are also listed, also with full source code. See website tabs "examples" and "games". • Strikes an excellent balance between "Here's a window and OpenGL context. Good luck." and "Engine::Core::Math::Addition::AdDtWoInTeGeRs(Engine::Core::Math::SnowFlakeInt FirstNumber, Engine::Core::Math::SnowFlakeInt seconDNumber)". >Shill Fuck you, it's free. I'm just spreading the good word.
Open file (147.18 KB 960x720 d1npc.jpeg)
Open file (739.98 KB 1027x720 mw_inv1.png)
Open file (74.92 KB 675x380 d1selected.jpeg)
Open file (22.33 KB 516x389 pixel_selected.jpeg)
The Design Document. What I am setting out to create is the framework for a game, a playable, vertical slice of a game. It will not be full-length, full-featured, or available to purchase on Steam. It will become a test-bed for ideas as well as a visualization for game assets (maps, models, etc.). The scope of the project is limited. I can say certain that it will not contain the following, although some of them may be future goals: ❌ Procedural/random generation ❌ Multiple player classes ❌ Skill trees or "builds" ❌ Multiplayer ❌ Code optimizations (data-oriented design, entity-component system, raw OpenGL/shaders, etc.) The current project scope does include: ✓ 2D (sprites) isometric maps authored in Tiled - both interior and exterior • Loaded at runtime from Tiled .tmx and .tsx format files (XML) • Includes doors - click to open and close ✓ Two areas - a starting town/camp with basic services (buying/selling, see image) and an area to fight MOBs in ✓ Player inventory - items can be picked up, dropped, bought, and sold • "Paper doll" equipment - drag and drop items to equip in slots. Weapon, armor, etc. • Morrowind-style inventory (see image) - all items occupy one slot in inventory and you are limited by weight, not inventory grid size (Diablo and friends) • Weapons - a few of both melee (sword, axe, spear, etc.) and ranged (bow, wand, etc.) • Potions - one for health and one for mana • Coin - to be dropped by intelligent creatures (no rats dying and leaving a pile of gold) and looted from containers ✓ Containers - chests, barrels, and/or crates. Objects can be retrieved as well as placed - Think Morrowind, not Diablo ✓ Spells and/or abilities - click to use, with cool down ✓ MOBs: one type - goblin, with three classes: basic fighter, spear-chucker, and shaman (melee, ranged, and spells) • MOBs may spawn with weapons and their attacks should reflect that • MOBs may drop items on death - need to "loot" corpse, items will not just fly out like a pinata • Random selection from loot tables ✓ On-screen, MUD-like text input and output - have a text box to display NPC text, combat text "YOU slash a Goblin Peon for 8 points of damage!" and more. "You sold a Rusty Short Sword to Blacksmith Ted for 3 gold pieces." • Stretch goals: Type to talk with NPCs, using keywords and crude Natural Language Processing, plus console commands "/additem 14" -> Sword of Kick Ass added to inventory. ✓ Player state saving and loading - if not world state saving and loading ✓ "Pixel-perfect" object selection - clicking on or hovering over an in-world object should select it (if interactive) and show a one-pixel colored border around it (see image) ✓ Extremely basic, free music and sound effects ✓ Limited sprite atlases and animation ✓ Tile-based lighting - centered around player a la Souls-series or Diablo - may include item to increase range/brighten lighting • Line of sight/fog of war - areas not yet explored/obscured by doors/walls should be black (if unknown) or greyed out (if currently out of sight) ✓ "Style" will consist of 3D renders in Blender and mock-ups/place holders done in GIMP You may have noticed that the above list (subject to changed, btw) does not include a player-controllable party. I figure that that is a large enough of an undertaking as to warrant being its own milestone, and that the above should all be in place before attempting to implement it.
Edited last time by nviridescens on 03/30/2023 (Thu) 05:57:58.
Open file (587.34 KB 1920x1080 flare.jpeg)
Open file (76.63 KB 758x426 fftactics.jpeg)
Open file (68.64 KB 700x470 dg.jpeg)
Open file (238.67 KB 1280x720 dIOtZUhnXPg.jpg)
>>488 A Couple of additions to design: ❌ Multi-platform - current build is on linux, for linux • I would, however, like it if someone could test and post a working Windows Raylib install and build guide for it ✓ Experience points and levels - at least 1-5 ✓ Combat formula taking into consideration: weapon damage, weapon speed, PC/MOB level, and/or stats Hopefully I don't end up updating the design much more. While I'm sure I'm still forgetting a thing or two, I'd like to keep the scope reasonably constrained so that I can release something working and not be stuck in permanent dev hell. Before I get into the code, I'd like to take some time to talk about so-called "isometric games", what the isometric perspective entails, as well as the pros and cons as a way of representing a game world. I say "representing" because "isometric" is simply one way to visually represent a game world, which is more often than not just a regular 2-dimensional X, Y plane. The guy who made Flare ht tps://flarerpg.org/ sums it up better than I can here ht tps://clintbellanger.net/articles/ particularly the first two articles. Key points in typical isometric games: • The world is not actually a grid of horizontal diamonds but your regular old 2D plane - though "Z-levels" may exist, see Final Fantasy Tactics Advance or the Disgaea series • Isometric games can be either fully 2D, 3D, or a mixture of both - it's just an angle from which the world is viewed • Isometric worlds can be composed of tiles on a regular grid or as a single, high-resolution image (for an example of the first - see Diablo I/II, for the second - FF7 or Planescape: Torment) • 2D Isometric tiles can be hand-drawn or 3D rendered - you see more of the first in Japanese titles and games for low resolution screens, more of the latter in western titles and PC games • Isometric tiles, representing a grid square, are twice as wide as they are tall (in the most common isometric projection) - common sizes being 64x32, 128x64, and so on ↓ More
Open file (1.00 MB 2048x1155 roller.png)
Open file (1.71 MB 1280x720 oLQPpv5.png)
Open file (567.27 KB 705x373 J60hBQA.png)
Open file (47.17 KB 640x360 ur.jpeg)
>>489 Isometric pros and cons The Good: • You can get a decent pseudo-3D look with only 2D assets • Code is as easy as top-down 2D, outside of a few screen-space to world-space and vice versa functions • 3D rendering assets can let you utilize all the fancy PBR materials and ambient occlusion, volumetric lighting, and whatever the fuck that's out there now for your 3D suite of choice, without having to add anything to your rendering pipeline or any runtime overhead • 3D rendering assets can also let you ignore a lot of mesh/model optimizations needed for real-time rendering like proper topology, normal mapping, texture size, contiguousness? and more, making asset creation easier • Should also be easier to mod and make assets for as a non-professional than a realtime 3D game The not-so-good: • View is often limited to a single camera angle (some games like Roller Coaster Tycoon will let you rotate through 4 viewing angles) • Field of view is limited because of the top-down camera angle - only way to see "further" is to zoom out or scroll • Need to deal with view being obscured by walls and objects in front of the player (southeast, southwest, see pics) • Sprites typically drawn from 8 different directions depending on angle - easy to do when you're rendering, a lot of work when you're drawing • Eight-direction only sprites can look jerky as they pop between angles when moving • Lighting and effects are limited if your implementation isn't full 3D
Sounds interesting anon, good luck and looking forward to future updates.
Based and Raylibbed. It's a nice library. My only two wishlist features for it are resizing with UI layout and lower level access to tracker music so you could do Deus Ex/Unreal style dynamic music, but those would introduce a lot of complexity.
>>492 >so you could do Deus Ex/Unreal style dynamic music That's just changing the current pattern isn't it? If it already plays modules that wouldn't be hard to add.
Open file (1.24 MB 270x300 attack anim.gif)
I basically doing exactly the same thing as you, but in SDL2, and I just found out it doesn't support shaders, which means, most likely, I have to do it in opengl. >• Field of view is limited because of the top-down camera angle - only way to see "further" is to zoom out or scroll Brigador solves it by centering view between cursor and player mech. >• Eight-direction only sprites can look jerky as they pop between angles when moving Diablo 2 uses 16 directions for players, and 8 for mobs. And small tiles hide it anyway. Another big problem of 2d is big mobs, like dragons. They often look janky or require extreme amount of space to store all tiles, and its more difficult to make their animations, since its much more noticeable when they dont fit together.
>>494 Hey fellow isometric dev, thanks for dropping by. >SDL2 ... it doesn't support shaders You mentioned this in the other thread too but can you elaborate on why you need shaders? What sort of effect are you trying to do with them? I understand you are also doing 2D tiles/sprites based on 3D renders. >Brigador I knew of the game but never played it. I'll have to check that out. >Diablo 2 Also didn't know that, guess I gotta reinstall it. >big mobs >extreme amount of space to store all tiles I always thought larger enemies simply had larger sprites. In my code, I get sprite width and height and use that to offset draw position (Raylib draws sprites from the upper-left corner) in order to align the sprite being drawn with it's world x/y coordinates. The only issue I can see with that method is sprites nearer to the camera but still "within" the size of the larger sprite could be drawn on top of the larger, when they should be drawn under it, but I figure that can be solved by using collision to ensure things are not inside one another.
Open file (8.92 KB 434x244 folder.png)
>>490 Fuck it, let's get to the code. I'll cover other issues as I come to them. Let's get a game up and running first. For that, we're gonna need a project folder. Now, project organization can be just as personal and contentious as placing braces around a code block but, here's mine for reference. You do whatever works for you, just understand that you'll need to tweak the Makefile to match. └── top-level directory ├── Makefile ├── src/ │ ├── main.cpp │ ├── main.h │ ├── Class0.cpp │ ├── Class0.h │ ├── Class1.cpp │ ├── Class0.h │ ├── Lib.cpp │ ├── Lib.h │ └── ... ├── obj/ │ ├── main.o │ ├── Class0.o │ └── ... ├── bin/ │ ├── game(or game.exe for the windozers) └── data/ ├── player.png ├── ground00.png └── ... To explain, I have a top-level folder containing the whole project. The only file I dump in the top directory is the Makefile. The rest, code/art/sounds/whatever go in their own folders within the top directory. I mentioned folders for the source code, assets, compiled object files, the final executable(s), etc. but some other useful directories could be 'doc' - design document, license, notes, etc., and 'reference' - notes, sketches, art, stolencode projects for inspiration, etc. Add whatever you need. 'data' is a flat directory right now, but as I add folders as I need them - 'levels', 'maps', 'sound', 'saves', etc.
Open file (1.88 MB 1463x1667 drag head 2.png)
>>495 >I always thought larger enemies simply had larger sprites Its complicated. Basically a single model with bone animation will be much easier to make and take far less space than shitty looking dragon for 2d game. With regular size enemies, animations dont have to flow from one into another, so you will need more animation frames, they will be bigger, and flowing from one animation into another will be extremely complicated. Not to mention interaction with the environment is difficult. I Figured out I could just scale down everything else if I use big enemy, but it will require more work with keeping detail level consistent. Scaling down destroys all details, its really annoying. I made dragon head, but its fucking useless, because details are too small to be visible in low resolution. >why do you need shaders To have dynamic lighting. I even expected to have normal maps, and similar effects, like "using a spell darkens all metallic surfaces around you", and to simply use same sprites but color shifted to different hue. Some of it is possible to do manually, but it will be software renderer on top of sdl. I also use quite big tiles 240*120 (to preserve visible details), so texture(-50red,-50green,-50blue) is not an option for me, since it will be noticeable and I want it to look good. I guess I will just render gradient circle (of few non overlapping triangles) around player with transparency to make everything look darker. And make shadows by using alpha of "vertical" tiles. But no way to make light sources, other than making every texture darker and than adding some colors to brighten up places I want, but its per tile only. Shaders would save a lot of space for spell effects too, since I can just use "scroll 2 textures along any axis, and multiply them to each other" to make a lot of cool effects. I could have a single sword rendered with multiple patterns to make infinite swords. Even simple stuff like "shift all grass from green to yellow" is difficult to make look good without shaders/doing it outside of sdl framework/remapping all colors.
Open file (12.80 KB 128x64 light_above.png)
Open file (12.31 KB 128x64 light_bottom.png)
Open file (12.48 KB 128x64 light_right.png)
Open file (66.70 KB 1452x891 1000hoursingimp.png)
>>500 True. For smaller enemies, you can tolerate a lot more jank. >details getting crushed Have you ever considered a "cutscene mode", where everything is briefly zoomed in to 2x size and resolution? You could do this briefly as the dragon first appears. I'm considering doing it for talking to NPCs or interacting with objects. Downside is you now need a 2x sized sprite for everything, or conversely, "normal mode" is scaled down to 0.5x and you only need the larger resolution assets. >dynamic lighting Actually, I don't believe that there is anything inherent to SDL2 that precludes one from using shaders, or any rendering code really - a lot of commercial games use SDL2 for window creation, operating system I/O, etc. In theory, you should be able to handle your own custom rendering by getting a hold of the OpenGL context, rendering your tiles as textured quads, and using whatever shaders you want in the process. You might also consider checking out the Raylib source code ht tps://github.com/raysan5/raylib and it's rlgl module (linked on that page). All of the drawing in Raylib is actually using OpenGL, quads, textures, shaders, etc. I had another idea for simple dynamic lighting for my game that I've been kicking around but decided to whip up a mock up just now to illustrate it. First pic is a jank-ass stone floor I drew, "lit" from above. Next is the same tile, lit from the bottom, and another lit from the right. The last pic is a mockup using these, plus a diagonal tile created by blending the bottom and right tiles 50/50. The idea being to handle each world tile as if it were a movable sprite - rendered from 8 (9 including above) directions, then selecting one (or blending one or more) depending on the nearest light source(s). Or, the light/shadow part can be a separate image, giving you a total of 10 (above, 8 directions, unlit/base). This way, the light can be colored (Raylib does this easily with the last parameter of its texture drawing functions being RGBA color). Just a thought.
>>501 Forgot to mention, that by making the lighting texture an overlay and not baked-in, you can adjust the alpha (intensity) of it based on distance from the light source and would have the option of making it lower resolution than the base tile, if you're worried about a 10x increase in total game texture size (I really wouldn't though).
>>497 Code continued. Before you do anything else, make sure you've got Raylib installed and that's in on your $PATH (it should be if you installed according to the guide) - see ht tps://github.com/raysan5/raylib#build-and-installation . Remember, I'm assuming you're working in a text editor (not an IDE) like Geany, Notepad++, VSCode? or whatever on linux like I am, if not, you're going to have to set up your own IDE and compiler, etc. Should be plenty of info out there though. Anyway, let's take a look at my Makefile. CC=g++ EXE=game BIN_DIR=bin SRC_DIR=src OBJ_DIR=obj CPP_FILES := $(wildcard $(SRC_DIR)/*.cpp) OBJECTS := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(CPP_FILES)) CFLAGS=-Wall -Wextra -pedantic -Wconversion LINKERFLAGS=-lraylib -lGL -lm -lpthread -ldl -lrt -lX11 all: $(BIN_DIR)/$(EXE) debug: add_flag all add_flag: $(eval CFLAGS+=-DDEBUG) $(BIN_DIR)/$(EXE): $(OBJECTS) ${CC} -o $(BIN_DIR)/$(EXE) $(OBJECTS) $(LINKERFLAGS) $(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp $(CC) $(CFLAGS) -c -o $@ $< clean: rm -f $(BIN_DIR)/$(EXE) $(OBJ_DIR)/*.o I'll explain what it does but can't won't explain how. GNU Make syntax is a weird thing. The bit at the top specifies the compiler - in my case GCC, the file name of the final executable - "game", and directories to look in/put files in during the build process. The next bit sets up lists of files to use in the build process - all of the files ending in .cpp as well as object files for each of those to make. The next are compiler and flags. The rest is Make directives. Don't worry about them too much, just know that typing "make" will compile all .cpp files in your source directory (src/), put a bunch of object files into your objects directory (obj/), and link those into a final executable located in your /bin directory. "make debug" will do the same, but compile with a "DEBUG" flag enabled - you will need to use this in your code with an #ifdef DEBUG if you want it to do anything. "make clean" will delete all object files and the executable, forcing Make to rebuild all files next time you "make".
Open file (7.77 KB 804x638 muh_gayme.png)
>>504 Now that we got that set up, we just need something to actually compile. Let's start by making two new files inside our source folder (game_dir/src/) - main.cpp and main.h (or main.hpp if that's your thing). As a bare-minimum, open-a-window "game", main.h looks like this: #ifndef MAIN_H #define MAIN_H #include "raylib.h" #define WIN_WIDTH 800 #define WIN_HEIGHT 600 #endif The #ifndef and #endif shits wrapping the whole thing prevents the header from being included more than once. Get used to putting them around your header files, but make sure each has a project-unique identifier like MAIN_H here. You typically go .cpp file/class name -> include guard name like Player.h -> PLAYER_H. The rest should be straightforward. A basic bitch main.cpp looks like the following: #include "main.h" int main(int argc, char *argv[]) { /// ################ SETUP BEGIN ################ SetWindowState(FLAG_MSAA_4X_HINT); // enable Multi-Sample Anti-Aliasing - needs to be done before window init SetWindowState(FLAG_VSYNC_HINT); // enable V-Sync - can also be done after window init // Window Initialization // Establishes an OpenGL context, so needs to come before any code involving textures or drawing InitWindow(WIN_WIDTH, WIN_HEIGHT, "MUH GAYME"); // Let's center the window in the screen - needs to come after window created int mon = GetCurrentMonitor(); int mon_width = GetMonitorWidth(mon); int mon_height = GetMonitorHeight(mon); SetWindowPosition((mon_width / 2) - (WIN_WIDTH / 2), (mon_height / 2) - (WIN_HEIGHT / 2)); bool ayy = false; /// ################ SETUP END ################ // Enter main game loop while (!WindowShouldClose()) // Until we detect window close button or ESC key press { /// ################ UPDATE BEGIN ################ if (IsKeyPressed(KEY_SPACE)) { ayy = !ayy; } /// ################ SETUP END ################ /// ################ DRAWING BEGIN ################ BeginDrawing(); ClearBackground(BLACK); DrawFPS(20, 20); if (ayy) { const char * text = "AYY LMAO"; int font_size = 48; int text_width = MeasureText(text, font_size); DrawText(text, ((WIN_WIDTH / 2)- (text_width / 2)), ((WIN_HEIGHT / 2) - (font_size / 2)), font_size, WHITE); // center text } EndDrawing(); /// ################ DRAWING END ################ } // Cleanup CloseWindow(); // Close window and delete OpenGL context } and should pretty much explain itself. Compile it with "make" (executed from the project directory, the one with the Makefile) and you should get an executable inside bin/. Run that with ./bin/game and you should get a black window with a FPS printout, and text that appears or disappears if you hit the space key. Congratulations, this is your "black triangle". If you don't get it -> ht tps://rampantgames.com/blog/?p=7745
>>486 Very cool OP. I'll be monitoring your progress.
Open file (301.12 KB 640x360 ClipboardImage.png)
Open file (970.37 KB 1900x900 dark mask.png)
>>501 Your idea sounds like inventing a bicycle made out of 8(9) wheels. The problem is seen in this picrelated, I dont even care if light direction is wrong at this point. The problem is the fact that whole tile is lit equally. >you can use opengl Yeah, it seems to be the only option, which just increases complexity. I expected to use it anyway, down the line, but it seem to be "necessary" already. I think I just render gradient (image) on top of everything and maybe add some noise, as temporary solution. And technically I can render far away tiles with (-0R,-255G,-255B) to make everything outside of light radius black with only red remain visible, for stylish effect, and gradient on top will hide the discrepancy. Now that I thought of it, it might actually work and look good. Some degree of "stylized" look is acceptable. And since I am going to process all tiles automatically, I can just add additional data to tile json such as "tile color balance", without reading each byte of color data via render engine. I technically only need to hide the brightest color (or change it into some other color, like red), just to tell player "this is dark" and they will understand it. So there are ways around it, but in the end I have to use opengl.
>>492 >UI I've never actually touched any of the UI stuff out there for raylib, I roll my own. You just have to work relative to screen size like you would with css % or em and recalculate everything on resize, use parent and child element positioning, etc. >tracker As cool as that is, I just don't see it ever making the TODO list for new raylib features. I'm sure you can find a library to help you do what you want though, just look into synthesizer or freetard DAW software.
Open file (86.16 KB 650x400 debug_print.png)
Open file (27.04 KB 668x387 highlighting.png)
>>505 Remember the Makefile and the "make debug" directive? Well, we're going to start using that for building for a while. I have a small utility library, just one function right now, that basically wraps "cout << text << endl" with a function that places "DEBUG: " at the head of your string to make it stand out from other text printed to the console. If you have any experience with Raylib, you'll know that it spits out a lot of text at startup, as well as when loading textures and fonts, cleaning up and closing, etc. Here's that library. As usual, it gets chucked in src/ like the rest and compiled with "make" or "make debug". debug.cpp // debug.cpp #include "debug.h" #include <iostream> void debug_out(std::string str) { std::cout << "DEBUG: " << str << std::endl; } debug.h // debug.h #ifndef DEBUG_H #define DEBUG_H #include <string> void debug_out(std::string str); #endif Now, you can use this anywhere you would normally print something to console but, I like to wrap it in pre-compiler directives to only have it included in debug builds and not release builds, like so: #ifdef DEBUG debug_out("RESUMING GAME..."); #endif Also testing pasting code to rentry.co: debug.cpp -> ht tps://rentry.co/2hyqs debug.h -> ht tps://rentry.co/w25dn Apparently pastes are kept forever and it doesn't seem too botnet. I'll be using this with longer files to keep the thread readable or to drop all the current project files at once, and because the code block CSS situation isn't great right now.
Open file (12.33 KB 804x638 main.png)
Open file (8.59 KB 804x638 game.png)
Open file (11.14 KB 804x638 paused.png)
>>516 Until now, I've been jumping right "into the action", so to speak, from program start. Even though it's still early in the development process, I thought I'd make the program more like your typical game or application and boot to a main menu first. To that end, I've created a 'Screen' class and several classes that inherit from that. #include "Screen.h" #include "PauseMenuScreen.h" #include "MainMenuScreen.h" #include "GameScreen.h" The reason I decided to subclass Screen rather than expand it, is because the different screens you'll encounter in a typical game app will have wildly different functionality: A splash screen may show and animated logo and play sounds, a main menu will have buttons or icons that lead to other menus, a load game or select character screen will need to open save files and display them, an options menu might have a bunch of checkboxes, radio buttons, sliders, etc. and an 'apply' button that applies changes and re-calculates things/gets a new OpenGL context, etc. etc. So for now I've got these three to play with (GameScreen may get tossed, since it represents the playable game state, not a menu). I initialize one of each in the setup section and set the main menu screen to currently active: // menus/screens PauseMenuScreen pause_menu; MainMenuScreen main_menu; GameScreen game_screen; //ScreenType current_screen = ScreenType::GAME_SCREEN; // go straight to game ScreenType current_screen = ScreenType::MENU_SCREEN; // go to main menu Because the screens act as a state machine, they need to be able to switch active screen from one to another at will. To let them "know about" each other without circular dependencies or too much bad code, I created a 'ScreenType' enumerator for the different types of possible screens in Screen.h (which the subclasses #include), allowing the subclass update() methods to return a ScreenType specifying their own type if they're still active, and another type to signal main() to switch over to that screen. Example: hitting escape on the 'game' screen will return the 'pause' screen enum during its update(), clicking 'resume' or hitting escape again on the 'pause' screen will return the 'game' type, and so forth. The update() methods are called in the update loop section of main(): // Enter main game loop while (!WindowShouldClose() && game_running) // Until we detect window close button or exit command { /// ################ UPDATE BEGIN ################ if (current_screen == ScreenType::EXIT_GAME) { // do nothing, wait to exit game_running = false; // or just break? } else if (current_screen == game_screen.type) { current_screen = game_screen.update(); } else if (current_screen == main_menu.type) { current_screen = main_menu.update(); } else if (current_screen == pause_menu.type) { current_screen = pause_menu.update(); } It's not the best solution, but I can deal with it for now. The drawing section is handled in a similar way: BeginDrawing(); /// clear background only on game frame render? /// that way, last frame drawn is still visible on pause menu ClearBackground(SKYBLUE); if (current_screen == ScreenType::EXIT_GAME) { // draw nothing break; } else if (current_screen == game_screen.type) { game_screen.draw(); } else if (current_screen == main_menu.type) { main_menu.draw(); } else if (current_screen == pause_menu.type) { pause_menu.draw(); } DrawFPS(20, 20); EndDrawing(); Current project code (This is a big pain in the ass, think I'm gonna start pushing to a repo or something instead): project directory ├── Makefile -> ht tps://rentry.co/6pb9d └── src/ ├── debug.cpp -> ht tps://rentry.co/2hyqs ├── debug.h -> ht tps://rentry.co/w25dn ├── GameScreen.cpp -> ht tps://rentry.co/b46ns ├── GameScreen.h -> ht tps://rentry.co/odc3s ├── main.cpp -> ht tps://rentry.co/8pxsy ├── main.h -> ht tps://rentry.co/8bcsi ├── MainMenuScreen.cpp -> ht tps://rentry.co/5p85a ├── MainMenuScreen.h -> ht tps://rentry.co/qqdyd ├── PauseMenuScreen.cpp -> ht tps://rentry.co/ichkef ├── PauseMenuScreen.h -> ht tps://rentry.co/szbea ├── Screen.cpp -> ht tps://rentry.co/6bkgh └── Screen.h -> ht tps://rentry.co/t4sgk
>>518 It's all looking good anon, thanks.
>>521 Appreciate it. And thanks to everyone else in the thread who voiced their support, or even just lurked; that's cool too. >>518 Isometric pics for some eye candy. Still on a bit of a derail but, one more quality-of-life feature implemented for this post: configuration files. Everybody likes a good config.ini, right? That thing you go in and edit by hand when the game doesn't want to boot for you anymore. So far, I've been setting a handful of variables at the top of main before creating a window: screen size, MSAA, vertical sync, etc. Today I decided to move those out into a configuration file in the top directory of the project, next to our good old Makefile: config.ini ; config.ini ; TEST CONFIGURATION FILE [screen_size] width = 1280 height = 720 [video_options] fullscreen = false msaa_4x = true vsync = true Now playing with files and parsing text can be a bit of a pain with cpp (versus something like Python), so I went looking on the ol' Shithub for a library to do it for me. My requirements for these kinds of external libraries are simple: completely free (read: MIT, BSD, Public Domain, Apache, whatever. JNU public license et all can eat shit), and have no dependencies of their own (just chuck source files and headers in src/ and away we go). I took a look at the one with the most stars: ht tps://github.com/pulzed/mINI/blob/master/src/mini/ini.h and it seemed like it would do the trick so saved to src/ it is. This is a header-only library, so including 'ini.h' is all you need to do to use it in your project. I created a class to handle opening the config file, reading from it, and copying the values to a struct passed to it: IniHandler. The struct itself is also defined in the same header, so including 'IniHandler.h' lets me use both from main(). The struct looks like this for now: struct VideoConfig { int screen_width = 640; int screen_height = 480; bool fullscreen = false; bool msaa_4x = false; bool vsync = false; }; I included default values for each field, even though I spit error messages (which field could not be read from config.ini) and crash the program at this point, if something cannot be parsed from file correctly. In general, it's better to crash the program with no survivors in the case of runtime exceptions, than to jump through hoops trying to handle every possible way it could fuck up. Failing gracefully should not be a thing at this stage of development (or ever, really). Anyway, with all of that shit broken out into a class, the top of main() looks like this now: VideoConfig video_config; IniHandler ini_handle; if (ini_handle.get_video_config(video_config)) { return 1; // could not parse config file, end program (IniHandler will throw error messages) } if (video_config.msaa_4x) { // enable Multi-Sample Anti-Aliasing - needs to be done before window init SetWindowState(FLAG_MSAA_4X_HINT); } if (video_config.vsync) { // enable V-Sync - can also be done after window init SetWindowState(FLAG_VSYNC_HINT); } else { // set FPS max to a reasonable limit // needed to ensure accurate frame delta time, plus keep GPUs from melting SetTargetFPS(300); } // Window Initialization // Establishes an OpenGL context, so needs to come before any code involving textures or drawing ///InitWindow(WIN_WIDTH, WIN_HEIGHT, "ARPG"); InitWindow(video_config.screen_width, video_config.screen_height, "ARPG"); // default to windowed size on creation if (video_config.fullscreen) { int display = GetCurrentMonitor(); SetWindowSize(GetMonitorWidth(display), GetMonitorHeight(display)); ToggleFullscreen(); } else { // center the window in the screen - needs to come after window creation center_window(); } The little bit of code that centered windowed windows in the center of the screen was also broken out, declared up top and defined at the bottom of main.cpp for the time being: void center_window(void) { int display = GetCurrentMonitor(); int display_width = GetMonitorWidth(display); int display_height = GetMonitorHeight(display); SetWindowPosition((display_width / 2) - (GetScreenWidth() / 2), (display_height / 2) - (GetScreenHeight() / 2)); } updated files: main.cpp -> ht tps://rentry.co/ysouv IniHandler.cpp -> ht tps://rentry.co/2hdm9 IniHandler.h -> ht tps://rentry.co/z49tm
Open file (21.04 KB 1114x908 vertex_colors.png)
>>508 IIRC, the first Diablo had per-tile lighting like in that pic as well. I guess it's popular because it's quick and easy. The problem I see with your gradient overlay would be it not being occluded by walls and doors and such. While 1 brightness level per tile may not look the prettiest, it's at least easy to compute light fall-off as well as "flow" from sources through doors, around corners, etc. Are you able to use immediate mode OpenGL? Raylib wraps modern OpenGL in an immediate-mode-like interface and god damn, I had forgotten how easy it was to work in. Whipped up pic related tonight to see if it was possible. A regular isometric rectangular tile with only the visible parts UV mapped to a quad and vertex lit. Test function looks like this: void DrawTileVertexColors(Texture2D texture, Vector2 top_left, Color col1, Color col2, Color col3, Color col4) { int tile_width = texture.width; int tile_height = texture.height; rlSetTexture(texture.id); rlBegin(RL_QUADS); rlNormal3f(0.0f, 0.0f, 1.0f); // top vertex rlColor4ub(col1.r, col1.g, col1.b, col1.a); rlTexCoord2f(0.5f, 0.0f); rlVertex2f(top_left.x + (tile_width / 2), top_left.y); // left vertex rlColor4ub(col2.r, col2.g, col2.b, col2.a); rlTexCoord2f(0.0f, 0.5f); rlVertex2f(top_left.x, top_left.y + (tile_height / 2)); // bottom vertex rlColor4ub(col3.r, col3.g, col3.b, col3.a); rlTexCoord2f(0.5f, 1.0f); rlVertex2f(top_left.x + (tile_width / 2), top_left.y + tile_height); // right vertex rlColor4ub(col4.r, col4.g, col4.b, col4.a); rlTexCoord2f(1.0f, 0.5f); rlVertex2f(top_left.x + tile_width, top_left.y + (tile_height / 2)); rlEnd(); rlSetTexture(0); } Adapted from one of texture drawing functions in the Raylib source. I haven't thought about how I'd go about computing the actual vertex lighting yet but, I wanted to see if it was possible, at least for floor quads.
Open file (13.53 KB 1204x838 light.png)
Open file (35.31 KB 1204x838 light2.png)
>>525 Not much in the way of updates right now, just playing with some lighting code. Got a crude tile-based lighting up and running using Bresenham's line drawing algorithm. I start by getting a "border" of tiles at the furthest extents the light could possibly travel based on its intensity, then, ray trace from the center of the light source tile to the center of each tile in the "border" list of tiles. Bresenham's gives me a line of tiles to walk (with some overlap, especially nearest the source point, but I check for already-calculated tiles). I walk the line of tiles, calculating the light intensity for each based on the Euclidean distance to the light source. Encountering a wall tile stops the ray trace. I still get leaks between diagonal wall sections and the fall-off calculation should be made non-linear but whatever, it works.
>>526 Nice, dynamic lighting will really help with the game's atmosphere.
Open file (21.13 KB 1204x838 light3.png)
Open file (21.46 KB 1204x838 light4.png)
>>527 For sure. I'm aiming for a more tactical, slower pace than your average mow-em-down ARPG, so I want to make sure there's plenty of anticipation and dread in dungeons and other dark places. Also good for quick line-of-sight calculation too apparently. Fixed the diagonal light leaks by adding a check against the two cardinal direction tiles on a diagonal step (tilemap[current_x - ray_x_direction][current_y] & tilemap[current_x][current_y - ray_y_direction]. Still got a few kinks here and there and the code quality is abhorrent so I'm probably gonna put it on the back burner for now.
Open file (996.82 KB 1428x857 blend.png)
Open file (615.26 KB 1920x1047 map.png)
>>528 No code update today, been busy with some real life stuff. Next step is to get a map imported and displayed, so I've been playing around with blender and rendering some placeholder map assets when I can.
>>533 I recognize Blender, but what's that other GUI you're using?
>>534 Tiled my friend. ht tps://www.mapeditor.org/
>>535 Appreciated.
Open file (476.26 KB 1920x1047 lights.png)
Open file (243.24 KB 1284x758 zoom.png)
Open file (103.66 KB 1284x758 zoom_in.png)
Open file (110.35 KB 1284x758 zoom_out.png)
>>533 Busy weekend, no time to do any code cleanup or a write-up for it but, I got the map loader going again, plus an ambient light level setting and placeable lights in the editor (next up is static light calculation), the map displayed in game, plus zooming in and out with the scroll wheel. Proper update soon.
Open file (302.96 KB 1284x758 static.png)
>>538 Static lighting was attempted. See if you can spot all the errors.
Open file (769.04 KB 1608x1276 d2_light0.png)
Open file (399.83 KB 1608x1276 d2_light2.png)
Open file (448.78 KB 1608x1276 d2_light3.png)
Open file (371.31 KB 1608x1276 d2_light4.png)
Open file (312.08 KB 1608x1276 d2_light5.png)
>>543 Let's talk lighting some more. So I fired up the ol' D2 again the other day and played around for a couple of hours, checking how shit works. While the gameplay puts me to sleep, technically it's a very impressive game. While I did like the animated water and rain drops effect on the river around the camp, I want to focus on the lighting right now. Diablo 2 employs a day-night cycle, which is cool. At night, and inside caves and crypts and such, the game is dark, like pitch-black outside of the player's light radius. In addition to the player's light source, there are also torches and fires, as well as light given off by certain enemies. As you can see in the pics, it's rather smooth; no real gradient banding, blockiness, or the 'starburst' effect you get with vertex lighting sometimes. There appear to be two 'layers' to the lighting effect. The first is what I assume to be an overlay, a "fog of war"-like low-resolution black texture overlayed over the screen, with the alpha channel value the inverse of the light level at the tile it covers. The second, which you can see in the last 3 pics, is what appears to be something like vertex lighting, where objects with some height to them, like pillars and stalagmites, block light and project a cone of shadow behind them, calculated in realtime as the player moves around them. I want to say that it's vertex lighting, or something similar, because I've seen some quite thin ones, like the ones cast by single leaf-less trees outdoors, thinner than I think can be achieved by a grid or block-based approach. It comes with some artifacts though, as you can see with the stalagmite in the last pic. I'd like to see if I can't get similar effects in my game.
Open file (140.74 KB 1204x838 blurry_light0.png)
Open file (48.21 KB 1204x838 blurry_light2.png)
Open file (69.99 KB 1204x838 blurry_light1.png)
>>546 A first attempt, done in the usual top-down testbed. The first pic is a 24x16 (the size of the screen in tiles) black texture, which the alpha channel of each pixel based on the underlying tile's illumination level. This texture is then rendered over the screen, with bilinear filtering to provide the blur effect. As you can see, it's not the greatest, there are some artifacts, like areas that should be 100% occluded being partially lit. That said, the banding and discrete light levels of the previous attempts are gone, see second pic. The third pic shows the result of combining this technique, plus the previous one (using tile luminance value to tint rendered texture for that tile). Combining them just results in the shadows "creeping out" into the lit spaces a bit. It's not the worst, but still isn't quite what I'm going for. I can still see the effect being useful for a fog of war, or to cover up not-yet-revealed map tiles. Technique used in this post taken from raylib 'fog of war' example: ht tps://www.raylib.com/examples/textures/loader.html?name=textures_fog_of_war Next, I'd like to see if the shadow overlay can be used to color the scene as well, to provide colored lighting and shadows, versus the black-only Diablo uses, using the texture blend modes shown off in this example: ht tps://www.raylib.com/examples/textures/loader.html?name=textures_blend_modes Maybe >>508 anon could weigh in some. Come up with a decent lighting solution yet?
Open file (158.71 KB 518x468 ClipboardImage.png)
Open file (766.28 KB 1000x500 dirt roads shitty.png)
Open file (106.89 KB 1097x714 ClipboardImage.png)
First of all, yours looks fine, and you might consider changing it after you have other assets, to see how it all fit. I suspect that each tile n diablo is actually 9 tiles in terms of lighting, like picrelated. (I am planning to make roads that way). And resulting brightness/color/ overlay is selected based on their values. Instead of global on screen gradient overlay >>508 dark mask.png I wanted to use here. (unless its simply opengl thing, but they dont always render with opengl, and these days its hard to tell what is used) For example you said "this pillar is wrong". Its 3rd picrelated. Only 5 is dark in that tile, but since big tile to the left is completely dark, it considers small tiles 147 to be dark too. But 236 are bright, because they dont have vision blocking. And ground tiles likely use "this is vision blocking pillar". I think I have... yep, here it is http://paul.siramy.free.fr/_divers/dt1_doc/ almost complete explanation on how everything works in d2. And in diablo 1 they used palette for brightness. Simply -10 to brightness could result in next tier of darkness for a tile (if palette is a set of 10 colors repeating with lower brightness N times). Its actually the fastest and least resource intense solution, however it involved a lot of work to make it. I am planning on making app for managing tilesets, cropping and such, and I could technically bake few darkness levels that way, with just a "cycling" palette. I dont really need many colors without proper shaders. You can also make animations with cycling palettes, I think its quite easy with sdl. However, its either animation or brightness, unless you want to make chimera by combining both, instead of just making it normally. And "easy" as in making separate software to pick colors for animations, without losing too much image quality. In diablo 1 they picked colors for the whole tileset, and divided 128 colors for characters and 128 colors for ground/wall tiles. And they regretted giving too many colors for walls/ground. However in modern sdl, I am 95% sure you can have separate palette for each texture, and even its own number of colors. However its way more labor intense and I likely use global onscreen overlay and simple -10RGB as an easy solution. Palette is really versatile tool, but its just too hard for me to manage. Also character shadow is just his character image(made out of separate parts, like legs, torso arms...), turned dark and rotated a little + transparency. You can actually see how some overlaps of player parts create darker parts of the shadow. Everything you need to know about d2 tiles. http://paul.siramy.free.fr/_divers/dt1_doc/
>>548 PS. with 32x32 tile size, you dont really need dark overlay, I guess. It probably would looks smooth enough as it is, with just color(palette) modulation per tile(texture). Or maybe they use it too, but I doubt it.
Open file (598.65 KB 1367x957 collision1.png)
Open file (304.66 KB 1396x964 collision2.png)
>>548 Thanks for the good write-up about Diablo's tiles. I didn't know that they were using a 5x5 grid of sub-tiles in the lighting and visibility calculations, but it makes matches up with my observations. Cool site, I sometimes forget how they used to really pack the data in on older games, to fit on a CD or two or whatever. Custom binary data formats for everything, run length encoded compression, and on and on. Neat tech, and a lot of brilliant solutions out there but that's not for me at the moment; I'm trying to keep things as simple as I can for now. >(I am planning to make roads that way) By that, do you mean that your roads won't simply be cosmetic, but that you'll actually do some sort of collision check for whether you're on/off the road and have it impact gameplay? (Movement speed, etc.) >Also character shadow is just his character image(made out of separate parts, like legs, torso arms...), turned dark and rotated a little + transparency. This I knew. I think it also explains how torches and fire sprites cast shadows where they shouldn't, it's just using the opaque part of the texture as a shadow. I do like this sub-tile shadow-casting technique though. It makes me wonder if I'm going to have to abandon Tiled as a level editor, since there doesn't seem to be support for different tile size/numbers on a per-layer basis. My current level format has a 'collision' layer, but it's just fully passable/fully blocked at the moment, no sub-tile collision. Here's a couple of shots of what that looks like. It wouldn't be hard though, to make a 'collision editor' that loads and displays a level, minus fancy lighting and such, that creates a grid 5X the resolution of the map, and lets you paint collision/collusion on top of the level and saves it to a simple XML file. I might give it a try one of these days.
>>550 >and lets you paint collision/collusion on top of the level Couldn't you store collision for each tile as metadata? Then you wouldn't need to manually paint it.
Open file (10.30 MB 4000x2000 dirt tiles.png)
Open file (61.88 KB 480x240 flower 200x100.png)
Open file (20.51 KB 100x108 freeb_01.png)
>>550 >roads >By that, do you mean that your roads won't simply be cosmetic, but that you'll actually do some sort of collision check I mean, I would use same way I calculate light on tiles. Instead of "this is crossing +, this is straight horizontal road -", I would have subtiles with "this is road" and draw lines based on that. Same with walls, so instead of "this is corner <", I will use "this is wall, above it is another wall, and to the right is a wall, so this drawn as corner wall". So for a wall, yeah, they can serve as collision check. >abandon tiled I think you should just use same tile format as you are right now, and just save textures differently in engine. I am actually making (soon™) tool to turn picrelated (big high res image, probably uncropped and unmasked, or stuff like grass going over tile borders and I want to keep some of it) and for player/npc tiles, which will downscale (downscaling gives much better image quality compared to just rendering in regular resolution) and cropping empty image parts, and saving metadata json, with offsets, png locations, animaton frames, replacing transparency with colorkey, etc. Essentially almost exactly the same tool they use in brigador, it comes with installation, you should check it out. I also want to make tool for palette manipulation/packing to save space, but it sounds really hard, since I have to check if everything looks alright after I decimated the palette, which sounds like a nightmare to process, and I am not well versed in image color manipulation at all. But I am afraid, it might be necessary for smooth (acceptably smooth, dont need perfect) shadows. I looked up available image editors, and tools for batch editing, but most of them have flaws which make them unusable to me. >checked it out myself >see based on SpriteSheetPacker https://spritesheetpacker.codeplex.com/ https://github.com/walteryoung/SpriteSheetPacker at the bottom I should check it out myself. And btw, consider allowing players to see monsters behind the pillars. Otherwise it might look like they appear out of nowhere, even if tile was quite bright and right next to them.
Open file (53.13 KB 1507x882 ex1.png)
Open file (48.04 KB 1506x879 ex2.png)
Open file (47.42 KB 1500x882 ex3.png)
Open file (31.87 KB 1501x879 ex4.png)
Open file (58.28 KB 1506x882 ex5.png)
>>551 >Couldn't you store collision for each tile as metadata? Sure, ideally. Right now though, I'm only using grid-aligned tiles and objects, so it's easy to just bucket fill the map as passable, then draw on top of the wall/object tiles. In the near future I'll be placing things at arbitrary locations - decorative bits, interactive objects, etc. In that case, I'd like to try my hand at at a system like diablo 2's, where there is a grid-aligned visible/walkable array at 5x or so the native tile resolution. See this mockup for an example of what I'm talking about. You'd have an object placed on the map at some arbitrary coordinates, in this case, a couple of shitty crates with half-assedly drawn shadows. With that object, there would be an associated 'blocking' layer or separate image, for path-finding or whatnot. This would then be pre-processed at some point, to "rasterize" it to the underlying grid. That grid would then be used in game for whatever purpose and the image itself used only for drawing at that point. >Then you wouldn't need to manually paint it. Sometimes some things are just faster to do by hand when prototyping. It's hacky for sure, but see above for what I'd like to do eventually.
Open file (134.65 KB 1415x879 wallmock1.png)
Open file (43.21 KB 1415x882 wallmock2.png)
Open file (117.41 KB 1409x878 wallmock3.png)
Open file (111.13 KB 1413x885 wallmock4.png)
>>554 Another shitty GIMP mock-up showing how it would work with macro-grid (tile-sized) aligned wall tiles and doors.
Open file (669.81 KB 384x1728 t_alltiles.png)
>>555 You can probably save yourself the trouble and make doors same path blocking as the walls, because why bother? Same with small (smaller than "full" tile) objects, just make them destructible, when someone walks over them, and ignore them for pathing. Sure, they still need collision, but just make it full tile size. Players will enjoy breaking objects by walking alone. I guess, alternatively, you can generate maps with random path blockers, and just place whichever object fits this "hole". But its probably not worth the trouble, even if just a minor addition to map generation. Sure, objects that are not at 45/90 degree angles are really fucking fancy, but I will not be surprised if autists would prefer proper 45/90 degree angles. It just looks more orderly.
Open file (44.04 KB 253x120 out scaled.png)
Open file (56.08 KB 253x120 outscaled2.png)
Open file (2.85 MB 1900x900 out 1.png)
Open file (4.01 MB 1900x900 sss.png)
I wonder if I should worry that 50% of tile textures are empty. (also I feel like I had this conversation before) I can do what diablo did, but that involves making my own renderer, which is obviously not an option. Or editing SDL2 to save stuff like it described here http://paul.siramy.free.fr/_divers/dt1_doc/ . I read that "empty(including the ones which are excluded by color key, usually(255,0,255) pixels are rendered very fast" so I dont really need to worry that textures have a bunch of zeros in them... I guess "dont optimize too early" is a rule for a reason, however I do want to avoid remaking everything later. However doing shading just by modulating texture and rendering it in smaller chunks will totally work for smooth shadows. It will look slightly better than picrelated (out scaled.png) . And apparently I can actually use it to make perspective effect (but I doubt I ever will). Still, cool option to have and consider. I can have Z levels on completely flat map, just by having sharp difference in brightness. Even diablo 2 (20 years ago on software renderer) didnt have this amazing technology.
Open file (1.85 MB 1900x900 out 16l.png)
Open file (1.71 MB 1900x900 out 8l.png)
Open file (1.73 MB 1900x900 out 8l 96x48.png)
Even with 8 levels of brightness, unadjusted with the exact same -32/256 shift, it looks fine. And with 16 levels of brightness it is basically perfect gradient, for 32x16 tiles. 8levels with 96x48 tiles, with slightly adjusted shift should look fine, in case this lighting + too many render calls would be slow. But I doubt it.
Open file (4.78 KB 128x256 goblin16.png)
Open file (30.84 KB 804x638 palette1.png)
Open file (31.31 KB 804x638 palette2.png)
>>553 I am going to stick with Tiled for now but, I may end up putting some custom tooling between the map and tileset files output by Tiled and the actual game. Looks like you might end up going the same route. >palettes I have not yet looked into palette manipulation but it is a mainstay of games, especially palette-swapped enemies. Raylib has a bunch of image-manipulation functions and from what I can tell, shouldn't be too hard to palette-swap. It looks like you can get a pointer to an array of colors (palette) of an image Actually I just went ahead and did it to see if it would work. Turns out the Raylib functions Color * palette = LoadImagePalette(img, total_colors, &color_count); and ImageColorReplace(&img, palette[i], new_palette[i]); did the trick. Pardon the shitty new palette, I just adjusted the hue of the original colors in gimp and took the rgb value from there. >And btw, consider allowing players to see monsters behind the pillars. Otherwise it might look like they appear out of nowhere, even if tile was quite bright and right next to them. If I recall correctly, that would happen to me a lot in the first Diablo. Usually large monsters on the tile next to a doorway (on the other side of a wall), they just suddenly pop in when moving to the tile next to the door. That shouldn't be a problem for me, since monsters are drawn whether they're visible or not. They should naturally "stick out" and be visible if they're physically larger than the thing occluding them.
>>559 P.S. When working in GIMP and you want to palettize (sp?) a PNG image with 1-bit alpha transparency, go to image -> mode -> Indexed and select one less color than you want, since it doesn't seem to count the transparent one. In my case, I scaled down to 128x256 and picked "15 colors" for this goblin I picked up somewhere. I haven't yet tried with images with semi-transparency where you would need 8-bit alpha though.
Open file (12.58 KB 1284x758 fps0.png)
Open file (139.82 KB 1284x758 fps1.png)
Open file (92.00 KB 1506x855 levelchange0.png)
Open file (131.33 KB 1500x855 levelchange1.png)
Open file (108.09 KB 1500x854 levelchange2.png)
>>557 I see you got some lighting in, cool. >I read that "empty(including the ones which are excluded by color key, usually(255,0,255) pixels are rendered very fast" This is probably true. I'm sure, given the prevalence of alpha transparency in games programming, someone at some point, put in a if (pixel == 0) { return; } else { actually_render(pixel); } somewhere deep in the OpenGL texture pixel rasterizing code. I mean, look at my screenshots; I really wouldn't worry about it. This is running on a micro PC the size of a sandwich with some ultra-low power Ryzon processor too by the way. I am absolutely targeting potatoes with integrated GPUs with my game. >I guess "dont optimize too early" is a rule for a reason, however I do want to avoid remaking everything later. Sure. There's a difference between "early optimization" and "planning ahead", though, which is why I do all the little experimentation that I do. You also have to consider that not all "optimizations" are really improvements, if they complicate code or asset creation for little improvement in gameplay or engine performance or whatever. >However doing shading just by modulating texture and rendering it in smaller chunks will totally work for smooth shadows. Are you refering to the screens in >>558 ? First thing I thought looking at those was how much of a bitch it would be to work at that grid size, the sheer number of tiles to place for a level of any playable size. I hadn't thought of taking the texture for a tile and mapping it 4 or more quads with their brightness and hue taken from a grid of higher resolution than the tile grid. I might have to steal that one. >I can have Z levels on completely flat map Yeah, there are a bunch of different ways to do that. >just by having sharp difference in brightness. That I'm not quite seeing. I only see a difference in brightness; it isn't giving me any illusion of curvature or height/depth. You would probably be better served with a more traditional approach like I sketched up in pics 3-5.
Open file (64.75 KB 804x638 gobs.png)
Open file (591.00 B 16x1 blue_pal.png)
Open file (591.00 B 16x1 red_pal.png)
Open file (4.78 KB 128x256 goblin16.png)
>>559 OK, I got it to where you can load a palette file and apply it to an image, done messing with this for a while. If you want to check it out, just set up your folders like I did above and drop these images in 'data'. main is as follows, if it'll let me post it all: // main.cpp #include "main.h" #include "raylib.h" #include "debug.h" #include "rlgl.h" // OpenGL abstraction layer to OpenGL 1.1, 2.1, 3.3+ or ES2 void DrawTextureSimple(Texture2D & texture, Vector2 origin, Color tint, float scale) { // Check if texture is valid if (texture.id > 0) { float width = (float)texture.width * scale; float height = (float)texture.height * scale; rlSetTexture(texture.id); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); rlNormal3f(0.0f, 0.0f, 1.0f); // Normal vector pointing towards viewer // Top-left corner for texture and quad rlTexCoord2f(0.0f, 0.0f); rlVertex2f(origin.x, origin.y); // Bottom-left corner for texture and quad rlTexCoord2f(0.0f, 1.0f); rlVertex2f(origin.x, origin.y + height); // Bottom-right corner for texture and quad rlTexCoord2f(1.0f, 1.0f); rlVertex2f(origin.x + width, origin.y + height); // Top-right corner for texture and quad rlTexCoord2f(1.0f, 0.0f); rlVertex2f(origin.x + width, origin.y); rlEnd(); rlSetTexture(0); } } int main(int argc, char *argv[]) { /// ################ SETUP BEGIN ################ SetWindowState(FLAG_MSAA_4X_HINT); SetWindowState(FLAG_VSYNC_HINT); //SetTargetFPS(300); InitWindow(WIN_WIDTH, WIN_HEIGHT, "PALETTE SWAP"); // default to windowed size on creation center_window(); int center_x = GetScreenWidth() / 2; /// texture and palette stuff // load base goblin image Image img = LoadImage("data/goblin16.png"); // resize goblin and convert to GPU texture ImageResizeNN(&img, 256, 512); Texture2D green_gob = LoadTextureFromImage(img); int total_colors = 16; int color_count = 0; // get the palette for the base goblin image Color * palette = LoadImagePalette(img, total_colors, &color_count); // load red palette image file Image pal_img = LoadImage("data/red_pal.png"); // get palette from palette image Color * red_palette = LoadImagePalette(pal_img, total_colors, &color_count); // replace the colors in the base goblin image with the ones in the palette (image in CPU memory) for (int i = 0; i < total_colors; i++) { ImageColorReplace(&img, palette[i], red_palette[i]); } // resize goblin and convert to GPU texture ImageResizeNN(&img, 256, 512); Texture2D red_gob = LoadTextureFromImage(img); // load base goblin image again since we overwrote it with the red palette data img = LoadImage("data/goblin16.png"); // load blue palette image file pal_img = LoadImage("data/blue_pal.png"); // get palette from palette image Color * blue_palette = LoadImagePalette(pal_img, total_colors, &color_count); // replace the colors in the base goblin image with the ones in the palette (image in CPU memory) for (int i = 0; i < total_colors; i++) { ImageColorReplace(&img, palette[i], blue_palette[i]); } // resize goblin and convert to GPU texture ImageResizeNN(&img, 256, 512); Texture2D blue_gob = LoadTextureFromImage(img); /// have to unload palette data then /// ################ SETUP END ################ // Enter main game loop while (!WindowShouldClose()) // Until we detect window close button or exit command { /// ################ UPDATE BEGIN ################ /// ################ UPDATE END ################ /// ################ DRAWING BEGIN ################ BeginDrawing(); ClearBackground(BLACK); // draw the three color goblins DrawTextureSimple(red_gob, {0, 0}, WHITE, 1.0f); DrawTextureSimple(green_gob, {center_x - 128, 0}, WHITE, 1.0f); DrawTextureSimple(blue_gob, {GetScreenWidth() - 256, 0}, WHITE, 1.0f); DrawFPS(20, 20); // draw red palette for (int i = 0; i < total_colors; i++) { // draw red palette DrawRectangle(i * 16, 550, 16, 16, red_palette[i]); DrawRectangleLines(i * 16, 550, 16, 16, WHITE); // draw base palette DrawRectangle((center_x - 128) + i * 16, 550, 16, 16, palette[i]); DrawRectangleLines((center_x - 128) + i * 16, 550, 16, 16, WHITE); // draw blue palette DrawRectangle((GetScreenWidth() - 256) + i * 16, 550, 16, 16, blue_palette[i]); DrawRectangleLines((GetScreenWidth() - 256) + i * 16, 550, 16, 16, WHITE); } EndDrawing(); /// ################ DRAWING END ################ } // Cleanup CloseWindow(); // Close window and delete OpenGL context } void center_window(void) { int display = GetCurrentMonitor(); int display_width = GetMonitorWidth(display); int display_height = GetMonitorHeight(display); SetWindowPosition((display_width / 2) - (GetScreenWidth() / 2), (display_height / 2) - (GetScreenHeight() / 2)); }
>>558 >If I recall correctly, that would happen to me a lot in the first Diablo. Usually large monsters on the tile next to a doorway (on the other side of a wall), they just suddenly pop in when moving to the tile next to the door. That shouldn't be a problem for me, since monsters are drawn whether they're visible or not. They should naturally "stick out" and be visible if they're physically larger than the thing occluding them. I was worried about half-lit tiles which look like player can see what is inside of them, but in actuality everything in that tile is invisible. >That I'm not quite seeing. I only see a difference in brightness; it isn't giving me any illusion of curvature or height/depth Really? For me >>558 these look like obvious layers floating over each other. But I did mean it more like a joke, however having additional detail level, basically for free is a neat thing to have. Also I haven't figured out how to render cliffs which would look good enough. In fact brigador uses similar system (I think), with depth image textures, unless they use shaders and its just like 1d normal map. >too many tiles I though of that too. Its 4k ground tiles per screen, which is equivalent of rendering 4k pixels with software renderer, which is not that many. Worst case, I can reduce it by a lot by using 96*48 tiles, like in last picture here >>558 . I can also use grid of just 9 subtiles, but it seems like a bad idea, visually. Its not that much data, to store or even generate on the fly. I mean, it worked for diablo2, and subtile data is probably just an additional byte per subtile, either stored or generated. Even assuming full size tile is 32 bytes of map data, and 4k (32*16) tiles per screen, and with map size of 80 screens, its only 10 mb of data. Might be a bit annoying to wrap a head around it, but it should be fine.
Open file (34.66 KB 804x638 pp0.png)
Open file (35.39 KB 804x638 pp1.png)
Open file (35.83 KB 804x638 pp2.png)
Open file (12.20 KB 128x128 crate.png)
Open file (4.78 KB 128x256 goblin16.png)
>>562 Implemented one of the vertical slice/proof-of-concept features today - pixel-perfect object selection. The selection is done in two steps: the first is to see if the mouse click coordinates are within an object's axis-aligned bounding box (AABB), for which I'm just using the texture size for right now (eventually I will use a per-image settable AABB, to reduce the number of pixel-selected searches, since they are far more expensive. The second step is to load the image data from the GPU texture back into main RAM, then query the pixel at the click location to see if it's alpha channel value is higher than 0 - in other words, is not a transparent pixel. When an object is selected, I then use a shader (taken unedited from Raylib example ht tps://github.com/raysan5/raylib/blob/master/examples/shaders/shaders_texture_outline.c) to draw an outline around it. main.cpp is as follows: // main.cpp #include "main.h" #include "raylib.h" #include "debug.h" #include "rlgl.h" // OpenGL abstraction layer to OpenGL 1.1, 2.1, 3.3+ or ES2 void DrawTextureSimple(Texture2D & texture, Vector2 origin, Color tint, float scale) { // Check if texture is valid if (texture.id > 0) { float width = (float)texture.width * scale; float height = (float)texture.height * scale; rlSetTexture(texture.id); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); rlNormal3f(0.0f, 0.0f, 1.0f); // Normal vector pointing towards viewer // Top-left corner for texture and quad rlTexCoord2f(0.0f, 0.0f); rlVertex2f(origin.x, origin.y); // Bottom-left corner for texture and quad rlTexCoord2f(0.0f, 1.0f); rlVertex2f(origin.x, origin.y + height); // Bottom-right corner for texture and quad rlTexCoord2f(1.0f, 1.0f); rlVertex2f(origin.x + width, origin.y + height); // Top-right corner for texture and quad rlTexCoord2f(1.0f, 0.0f); rlVertex2f(origin.x + width, origin.y); rlEnd(); rlSetTexture(0); } } class PickableObject { public: std::string name; Texture2D texture; Shader outline_shader; Vector2 center; bool within_aabb = false; bool selected = false; // constructor PickableObject(std::string name, std::string texture_file, Vector2 center); // class methods void draw(void); void check_collision(Vector2 mouse_coords); }; PickableObject::PickableObject(std::string name, std::string texture_file, Vector2 center) { // set name this->name = name; // set position this->center = center; // load image #ifdef DEBUG debug_out("ATTEMPT TO OPEN PATH: " + std::string(TextFormat("data/%s.png", texture_file.c_str()))); #endif Image img = LoadImage(TextFormat("data/%s.png", texture_file.c_str())); // ImageResizeNN(&img, 256, 512); // resize // convert to GPU texture texture = LoadTextureFromImage(img); UnloadImage(img); // create shader outline_shader = LoadShader(0, "data/shaders/outline.fs"); float outlineSize = 2.0f; float outlineColor[4] = {1.0f, 0.0f, 0.0f, 1.0f}; // 0.0-1.0 normalized color float textureSize[2] = { (float)texture.width, (float)texture.height }; // Get shader variable locations int outlineSizeLoc = GetShaderLocation(outline_shader, "outlineSize"); int outlineColorLoc = GetShaderLocation(outline_shader, "outlineColor"); int textureSizeLoc = GetShaderLocation(outline_shader, "textureSize"); // Set shader values (they can be changed later) SetShaderValue(outline_shader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT); SetShaderValue(outline_shader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4); SetShaderValue(outline_shader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2); } void PickableObject::draw(void) { // offset by half of image dimensions float draw_x = center.x - ((float)texture.width / 2); float draw_y = center.y - ((float)texture.height / 2); if (selected) { // draw texture with outline BeginShaderMode(outline_shader); DrawTextureSimple(texture, {draw_x, draw_y}, WHITE, 1.0f); EndShaderMode(); } else { // draw without DrawTextureSimple(texture, {draw_x, draw_y}, WHITE, 1.0f); } if (within_aabb) { // draw border around image DrawRectangleLines(draw_x, draw_y, texture.width, texture.height, WHITE); } } void PickableObject::check_collision(Vector2 mouse_coords) { /// first, check for aabb collision if (mouse_coords.x > (center.x - ((float)texture.width / 2)) && mouse_coords.x < (center.x + ((float)texture.width / 2))) { if (mouse_coords.y > (center.y - ((float)texture.height / 2)) && mouse_coords.y < (center.y + ((float)texture.height / 2))) { #ifdef DEBUG debug_out("CLICKED WITHIN AABB OF " + std::string(TextFormat("%s", name.c_str()))); #endif within_aabb = true; /// next check for object selection (pixels) // load image from GPU Image img = LoadImageFromTexture(texture); // get clicked-on pixel coordinates int pixel_x = (int)mouse_coords.x - ((int)center.x - (texture.width / 2)); int pixel_y = (int)mouse_coords.y - ((int)center.y - (texture.height / 2)); #ifdef DEBUG debug_out("CLICKED IMAGE PIXEL COORDS: " + std::to_string(pixel_x) + ", " + std::to_string(pixel_y)); #endif Color pixel = GetImageColor(img, pixel_x, pixel_y); if (pixel.a > 0) // not transparent { #ifdef DEBUG debug_out(std::string(TextFormat("%s", name.c_str())) + " SELECTED"); #endif selected = true; } else { selected = false; } UnloadImage(img); return; } } within_aabb = false; selected = false; } int main(int argc, char *argv[]) { /// ################ SETUP BEGIN ################ SetWindowState(FLAG_MSAA_4X_HINT); SetWindowState(FLAG_VSYNC_HINT); //SetTargetFPS(300); InitWindow(WIN_WIDTH, WIN_HEIGHT, "PIXEL PICKING"); // default to windowed size on creation center_window(); PickableObject gob = {"goblin", "goblin16", {250, 300}}; PickableObject crate = {"crate", "crate", {550, 300}}; /// ################ SETUP END ################ // Enter main game loop while (!WindowShouldClose()) // Until we detect window close button or exit command { /// ################ UPDATE BEGIN ################ if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { Vector2 mouse = GetMousePosition(); gob.check_collision(mouse); crate.check_collision(mouse); } /// ################ UPDATE END ################ /// ################ DRAWING BEGIN ################ BeginDrawing(); ClearBackground(BLACK); // draw our objects gob.draw(); crate.draw(); EndDrawing(); /// ################ DRAWING END ################ } // Cleanup CloseWindow(); // Close window and delete OpenGL context } void center_window(void) { int display = GetCurrentMonitor(); int display_width = GetMonitorWidth(display); int display_height = GetMonitorHeight(display); SetWindowPosition((display_width / 2) - (GetScreenWidth() / 2), (display_height / 2) - (GetScreenHeight() / 2)); } The shader code file, outline.fs, is from the example: #version 330 // Input vertex attributes (from vertex shader) in vec2 fragTexCoord; in vec4 fragColor; // Input uniform values uniform sampler2D texture0; uniform vec4 colDiffuse; uniform vec2 textureSize; uniform float outlineSize; uniform vec4 outlineColor; // Output fragment color out vec4 finalColor; void main() { vec4 texel = texture(texture0, fragTexCoord); // Get texel color vec2 texelScale = vec2(0.0); texelScale.x = outlineSize/textureSize.x; texelScale.y = outlineSize/textureSize.y; // We sample four corner texels, but only for the alpha channel (this is for the outline) vec4 corners = vec4(0.0); corners.x = texture(texture0, fragTexCoord + vec2(texelScale.x, texelScale.y)).a; corners.y = texture(texture0, fragTexCoord + vec2(texelScale.x, -texelScale.y)).a; corners.z = texture(texture0, fragTexCoord + vec2(-texelScale.x, texelScale.y)).a; corners.w = texture(texture0, fragTexCoord + vec2(-texelScale.x, -texelScale.y)).a; float outline = min(dot(corners, vec4(1.0)), 1.0); vec4 color = mix(vec4(0.0), outlineColor, outline); finalColor = mix(color, texel, texel.a); } You're gonna need the attached images to make it run too.
>>565 >The second step is to load the image data from the GPU texture back into main RAM, then query the pixel at the click location to see if it's alpha channel value is higher than 0 - in other words, is not a transparent pixel. Neat. Interesting ideas Anon. GG.
Open file (689.83 KB 707x923 hand7.png)
>>565 Are you sure its a good idea? Pixel perfect selection is only needed when stuff overlaps and you can select the wrong object. In that case you should either avoid that problem, or have priorities, like attacking is always top priority, over opening chests. In fact diablo2 had "-nopickup" option, to disable picking up items, unless you press a button for it. Or have something like regular hitbox system, but 2d, with selection between overlapping objects being made by selecting the object by moving the mouse closer to the center of that object. So when you have 2 objects almost stacked, you can move mouse away from object you dont want. Also you can prebake outlines for static objects, to have 2000 fps instead of 1999. Or change brightness/color slightly. However outlines look way better than changing brightness or color. You dont want to miss when clicking the enemy, so their hitboxes should be a little bigger than they are, and most devs figured out "move only" button to avoid accidently attacking when you want to move. You are not planning to revive primordial quest games, are you? With pixel hunting and all that. And you can have bigger cursor than regular mouse point, so it would look like you hovering over object, even if you hover outside of it. Maybe even slight offset away from the point, but it probably would cause more problems than solve.
>>567 >Are you sure its a good idea? Maybe? I wanted to see how hard it would be to implement first (It's not hard); playtesting comes later. By the design that I'm currently working with, there won't be 'click to attack' like in Diablo and Diablo-likes. Clicking will be used for movement, targetting, and item interaction. It may also be used to command team members like: click on team member to select, click move, click destination, click on other team member, click cast heal, click on self as target for heal, etc. I never liked 'click to attack', or 'click all over the fucking ground trying to pick up the tiny jewel' kind of shit Diablo did. I find those sort of systems painful and tedious. I'm not too worried about overlapping objects either, since items that end up on the ground, as well as enemies, will have collision to prevent them from occupying the exact same space in the world. At which point pixel-perfect selection will let you pick which one you want. Clicking exactly on things is also more natural, I feel, than just clicking in the ballpark of things. It may be a different story though for games that require fast reaction times and high actions-per-minute like RTSs but I'm aiming for a slower, more tactical experience. >Also you can prebake outlines for static objects, to have 2000 fps instead of 1999. I had considered this except that: a single MOB or PC may consist of hundreds of images (animation frames X 8-16 directions), leading to a huge increase in total game size. Sure, that isn't the case for static images but, it's a lot more images to author and keep track of. Before I came across this shader, I was dreading having to write some kind of GIMP plugin or command-line script to generate an outline image for every selectable thing in the game. A shader, too, can be adjusted on the fly to use different line weights and colors. Hell, there is modding potential in it as well, since theoretically anything with a graphic could be clicked on and selected/interacted with, without having to create an outline graphic for it specifically. >You are not planning to revive primordial quest games, are you? With pixel hunting and all that. Why not? RPGs and action games could always stand to have more interaction. I do in fact plan on having small things to click on, like levers and shit, and possibly small items to pick up like keys and coins. I plan to have a zoom feature, which we've seen already, that should make finding and selecting even small objects easy. For instance: you enter a room. It's dark but there's a table with a candle on it. Something glints on the table. You walk up to the table and zoom in. It's the jailer's key ring. You click on it and pick it up. Shit like that. Also, in my little demo today, I used click-to-select, but what I'm really planning on doing is 'hover-to-highlight', to help find things and possibly display contextual information, like a text bubble "door", "key", "portcullis lever", or whatever. So hovering over something will display a border around it, showing you that it's a thing and not just some background decoration, with the color of the outline possibly giving more information (if it's a MOB, container, interactive element, etc.), possibly corresponding to the button used to interact with it (for instance, if the 'attack' icon is red, a MOB may get a red outline, etc.). Clicking on the thing would then interact with it, possibly leaving the outline around it, if it's your current target, or the destination of your 'move to and interact with' action or whatever. We'll see.
Open file (41.21 KB 804x638 lever.mp4)
>>565 Now using an animated texture atlas.
Open file (99.91 KB 804x638 lever_zoom.mp4)
>>569 Now with zoom, in case something is too small to easily see or click on.
>>568 >Also, in my little demo today, I used click-to-select, but what I'm really planning on doing is 'hover-to-highlight', to help find things and possibly display contextual information, like a text bubble "door", "key", "portcullis lever", or whatever. Good thinking Anon. Those kind of little tweaks can make a major difference to the playablity/usability of a game/system. GG.
Open file (6.36 KB 1152x128 lever_anim.png)
>>570 main.cpp and the image used for this: // main.cpp #include "main.h" #include "raylib.h" #include "debug.h" #include <vector> #include "rlgl.h" // OpenGL abstraction layer to OpenGL 1.1, 2.1, 3.3+ or ES2 // Draw a part of a texture (defined by a rectangle) void DrawTextureRecZoom(Texture2D texture, Rectangle source, Vector2 position, Color tint, float scale) { // Check if texture is valid if (texture.id > 0) { float width = (float)source.width * scale; float height = (float)source.height * scale; // x-axis (u?) texture coordinates within 0.0-1.0 uv coordinate space // only works for Nx1 animated textures float text_x_start = source.x / (float)texture.width; float text_x_end = (source.x + source.width) / (float)texture.width; rlSetTexture(texture.id); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); rlNormal3f(0.0f, 0.0f, 1.0f); // Normal vector pointing towards viewer // Top-left corner for texture and quad rlTexCoord2f(text_x_start, 0.0f); rlVertex2f(position.x, position.y); // Bottom-left corner for texture and quad rlTexCoord2f(text_x_start, 1.0f); rlVertex2f(position.x, position.y + height); // Bottom-right corner for texture and quad rlTexCoord2f(text_x_end, 1.0f); rlVertex2f(position.x + width, position.y + height); // Top-right corner for texture and quad rlTexCoord2f(text_x_end, 0.0f); rlVertex2f(position.x + width, position.y); rlEnd(); rlSetTexture(0); } } class Lever { public: std::string name; Texture2D texture; Shader outline_shader; Vector2 center; // location of the center of the texture on the screen std::vector<float> zoom_levels = {0.0625f, 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f, 16.0f}; int zoom_index = 4; // current zoom level float zoom = zoom_levels[zoom_index]; bool within_aabb = false; bool selected = false; // animation int total_frames = 9; int current_frame = 0; int frame_size_x = 128; int frame_size_y = 128; bool moving = false; float elapsed = 0.0f; float frame_step = 0.12f; // seconds per animation frame // movement directions bool pos_right = true; // switch is in the 'right' position bool pos_left = false; // switch is in the 'left' position // constructor Lever(std::string name, std::string texture_file, Vector2 center); // class methods void draw(void); void update(void); void check_collision(Vector2 mouse_coords); }; Lever::Lever(std::string name, std::string texture_file, Vector2 center) { // set name this->name = name; // set position this->center = center; // load image #ifdef DEBUG debug_out("ATTEMPT TO OPEN PATH: " + std::string(TextFormat("data/%s.png", texture_file.c_str()))); #endif Image img = LoadImage(TextFormat("data/%s.png", texture_file.c_str())); // ImageResizeNN(&img, 256, 512); // resize // convert to GPU texture texture = LoadTextureFromImage(img); UnloadImage(img); // create shader outline_shader = LoadShader(0, "data/shaders/outline.fs"); ///float outlineSize = 2.0f; float outlineSize = 1.0f; // seems to only work with integer values, makes sense since it's pixel width float outlineColor[4] = {1.0f, 0.0f, 0.0f, 1.0f}; // 0.0-1.0 normalized color float textureSize[2] = { (float)texture.width, (float)texture.height }; // Get shader variable locations int outlineSizeLoc = GetShaderLocation(outline_shader, "outlineSize"); int outlineColorLoc = GetShaderLocation(outline_shader, "outlineColor"); int textureSizeLoc = GetShaderLocation(outline_shader, "textureSize"); // Set shader values (they can be changed later) SetShaderValue(outline_shader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT); SetShaderValue(outline_shader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4); SetShaderValue(outline_shader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2); } void Lever::draw(void) { zoom = zoom_levels[zoom_index]; // offset by half of image dimensions float draw_x = center.x - ((float)frame_size_x / 2) * zoom; float draw_y = center.y - ((float)frame_size_y / 2) * zoom; Rectangle frameRec = {(float)(frame_size_x * current_frame), 0.0f, (float)frame_size_x, (float)frame_size_y}; if (selected) { // draw texture with outline BeginShaderMode(outline_shader); ///DrawTextureSimple(texture, {draw_x, draw_y}, WHITE, 1.0f); DrawTextureRecZoom(texture, frameRec, {draw_x, draw_y}, WHITE, zoom); EndShaderMode(); } else { // draw without ///DrawTextureSimple(texture, {draw_x, draw_y}, WHITE, 1.0f); DrawTextureRecZoom(texture, frameRec, {draw_x, draw_y}, WHITE, zoom); } if (within_aabb) { // draw border around image DrawRectangleLines((int)draw_x, (int)draw_y, (int)((float)frame_size_x * zoom), (int)((float)frame_size_y * zoom), WHITE); } } void Lever::check_collision(Vector2 mouse_coords) { zoom = zoom_levels[zoom_index]; /// first, check for aabb collision if (mouse_coords.x > (center.x - ((float)frame_size_x / 2) * zoom) && mouse_coords.x < (center.x + ((float)frame_size_x / 2) * zoom)) { if (mouse_coords.y > (center.y - ((float)frame_size_y / 2) * zoom) && mouse_coords.y < (center.y + ((float)frame_size_y / 2) * zoom)) { #ifdef DEBUG debug_out("CLICKED WITHIN AABB OF " + std::string(TextFormat("%s", name.c_str()))); #endif within_aabb = true; /// next check for object selection (pixels) // load image from GPU Image img = LoadImageFromTexture(texture); // get clicked-on pixel coordinates int pixel_x = (int)mouse_coords.x - ((int)center.x - (int)(((float)frame_size_x / 2) * zoom)); pixel_x += (int)((float)frame_size_x * (float)current_frame * zoom); // slide to current animation frame pixel_x = (int)((float)pixel_x / zoom); int pixel_y = (int)mouse_coords.y - ((int)center.y - (int)(((float)texture.height / 2) * zoom)); pixel_y = (int)((float)pixel_y / zoom); #ifdef DEBUG debug_out("CLICKED IMAGE PIXEL COORDS: " + std::to_string(pixel_x) + ", " + std::to_string(pixel_y)); #endif Color pixel = GetImageColor(img, pixel_x, pixel_y); if (pixel.a > 0) // not transparent { #ifdef DEBUG debug_out(std::string(TextFormat("%s", name.c_str())) + " SELECTED"); #endif selected = true; moving = true; } else { selected = false; } UnloadImage(img); return; } } within_aabb = false; selected = false; } void Lever::update(void) { if (moving) { // see if it's time to progress to next animation frame elapsed += GetFrameTime(); if (elapsed > frame_step) { elapsed -= frame_step; if (pos_right) // moving from right to left { current_frame++; if (current_frame > (total_frames - 1)) { current_frame = (total_frames - 1); moving = false; pos_right = false; pos_left = true; selected = false; } } else if (pos_left) // moving from left to right { current_frame--; if (current_frame < 0) { current_frame = 0; moving = false; pos_right = true; pos_left = false; selected = false; } } } } /// handle mouse wheel zoom float mw = GetMouseWheelMove(); //if (abs(mw) > 0) if (mw != 0.0f) { #ifdef DEBUG debug_out("MOUSE WHEEL: " + std::to_string(mw)); #endif if (mw > 0) { // zoom in zoom_index++; #ifdef DEBUG debug_out("ZOOMING IN"); #endif } else { zoom_index--; #ifdef DEBUG debug_out("ZOOMING OUT"); #endif } if (zoom_index < 0) { zoom_index = 0; } else if (zoom_index == (int)zoom_levels.size()) { zoom_index = ((int)zoom_levels.size() - 1); } #ifdef DEBUG debug_out("ZOOM LEVEL: " + std::to_string(zoom_levels[zoom_index]) + "X"); #endif } } int main(int argc, char *argv[]) { /// ################ SETUP BEGIN ################ SetWindowState(FLAG_MSAA_4X_HINT); SetWindowState(FLAG_VSYNC_HINT); //SetTargetFPS(300); InitWindow(WIN_WIDTH, WIN_HEIGHT, "PIXEL PICKING - ANIMATED"); // default to windowed size on creation center_window(); Lever lever = {"lever", "lever_anim", {400, 300}}; /// ################ SETUP END ################ // Enter main game loop while (!WindowShouldClose()) // Until we detect window close button or exit command { /// ################ UPDATE BEGIN ################ if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { Vector2 mouse = GetMousePosition(); lever.check_collision(mouse); } lever.update(); /// ################ UPDATE END ################ /// ################ DRAWING BEGIN ################ BeginDrawing(); ClearBackground(BLACK); // draw our objects lever.draw(); EndDrawing(); /// ################ DRAWING END ################ } // Cleanup CloseWindow(); // Close window and delete OpenGL context } void center_window(void) { int display = GetCurrentMonitor(); int display_width = GetMonitorWidth(display); int display_height = GetMonitorHeight(display); SetWindowPosition((display_width / 2) - (GetScreenWidth() / 2), (display_height / 2) - (GetScreenHeight() / 2)); }
Open file (39.72 KB 1000x1000 aaa reference tiles2.png)
Open file (122.19 KB 836x557 ClipboardImage.png)
Open file (776.96 KB 409x500 1468398307939.gif)
I wonder if I should chop tiles like picrelated (red tiles chopped into blue sub tiles), or chop them into smaller diamonds, or stop fucking around and keep tiles like they are right now(maybe a little bit smaller. I think, chopping them into blue rectangles should be a good idea, since it works for wall tiles, and I can avoid rendering ground tiles which hide behind the wall (optimizing for 2k+ fps, I really should stop). But it might fuck up shading a little. But if a I wanted perfect shading, I would use proper shaders in openGL. And I dont need to render/store walls and ground the same way. In fact I dont need to store and render any kind of tiles the same way. And I can render roads on top of other ground tiles, instead of making merged road+ ground tiles, which would save time and effort for me, since it is quite annoying thing to do. I dont even need diamond shape, other than it being quite useful, in case I switch to more efficient way to store/render tiles, or just sell tiles. And in general, its useful for dimetric projection I am using. And I plan to make tools to chop tiles into proper shape anyway, and I am likely will have tiles which are bigger than intended and overlap other tiles, like grass, which should look good. In fact its applicable to all tiles, especially walls/objects. But without diamond subtiles, tile grid and data grid will be at 45 degree angle, despite both being 2d matrix... I think, I will chop sub-tiles like in reference picrelated, at least until I make proper metadata+tile packer tool. Either 4x4 subtile grid for 64x32 subtiles or 8x4 for 32x32 subtiles.
Open file (240.66 KB 1024x1024 savespace.png)
Open file (359.48 KB 1284x758 attempt.png)
Open file (367.35 KB 1284x758 wouldbenice.png)
>>573 I'd just keep what you have and move on. It's fun to think about how to optimize for file size or minimum overdraw or whatever, and I made a mock-up showing a couple of different ways, but it's just not worth it at this point. Like I said before, saving tiles as PNG, either separately or packed into an atlas (I do both), and rendering them as-is, should be fine for our purposes. The PNG file format compresses nicely, especially when you have large contiguous areas. A "diamond" isometric floor tile may be 50% blank, "wasted" space, but I guarantee you its file size is not double that of one with only the visible pixels. The overdraw issue as well, is likely a non-issue, since it seems OpenGL, and probably Directx and Vulkan as well, are optimized to very quickly deal with transparent pixels, given their prevalence in 2D and 3D games. >I dont even need diamond shape, other than it being quite useful, ... You actually really do, because like you said, it's quite useful. The diamond marks the bounds of an object in 3D space, whether a tile is 1m x 1m, or whatever your scale is. It allows you to properly depth sort your static and dynamic objects quickly and easily (First, compare x and y grid coords. If they're on the same grid coordinate, compare world space x, y). It should start to come together better when we get more walls/buildings/objects in place. >tile grid and data grid will be at 45 degree angle, despite both being 2d matrix... This will absolutely fuck you. I'd like to think that, given it's long history and sheer number of games using it, that the tried and true isometric "2.5D" projection of a 3D world to a 2D screen cannot be (easily) improved upon. >subtiles I really wouldn't. Honestly, it's probably best if we moved on to gameplay and set some of the graphical stuff aside for now. I'm confident that the issues will get worked out in time. Really though, I wouldn't sweat the graphical "optimization". It's just one of many possible representations of what should be a plausible 3D space, and it's that game world and gameplay that, at least myself, need to focus on now.
>>574 Maybe I should explain the first pic. It's two different "optimizations" that one could do. The top bit is if you took your regular old rectangular texture with "diamond" visible isometric part, and UV mapped the inner diamond to a diamond-shaped quad and rendered that. This renders only the visible part, clipping the transparent bits. This eliminates the so-called overdraw problem. Still 1 texture, 1 quad. I may benchmark this compared to rendering the whole texture sometime if I get bored. The middle bit is another method: using a square texture with no transparent parts mapped to a diamond-shaped quad, such that it is rotated -45 degrees and squished to half height. Eliminates the so-called wasted space/excess file size problem but comes with major drawbacks - it's no longer "pixel perfect" 1 to 1 mapping with what is rendered to screen. You will need to use bilinear texture filtering for it to not look like total ass. Also, if you're using power-of-two texture sizes, 128x128 will be larger than a 128x64 isometric tile (and possibly look worse) and 64x64 will be smaller than a 128x64, but need to be upscaled and stretched (definitely look worse). Also, both of these "solutions" only work for perfectly flat (that is, no depth, level transitions, grass sticking up, anything) diamond "floor" tiles. As you can see, both these methods come with serious drawbacks and limitations and you will probably not see these techniques employed anymore.
>>575 >I may benchmark this compared to rendering the whole texture sometime if I get bored. From what I read, rendering a single triangle, and rendering 2k triangles is exactly the same, because of how gpu works. In fact, I suspect that textured 3d grid without shaders will render faster than my "render one 32x32 tile at a time" approach, because rendering 3d scene is done as a single cpu>gpu action, while cpu calls for rendering of each tile. But its only 4k ground tiles and for zooming out, I will use a trick, in which I render same ground tiles (but scale down data grid), but only characters will be squished. So regular 32x32 tile will look exactly the same, but instead of being 2m*2m it will represent 4m*4m. 2 tile wide road will become 1tile wide road. >using 3d Kind of pointless to me, since I use true 2d, because using 3d is much more work. If I use 3d ground(which is the most 2d thing I have) why not use 3d models for walls? And why not make ground stuff to be actual 3d modeled? And if 3d models for walls, why not use them for characters? It will be better and solve some problems, especially with animations, but its a lot of work. I suspect I will have to move to 3d, but not today. The main goals are to have an engine, which is easy to work with, and easy to make content for it, and for it to look good enough.
Open file (23.98 KB 200x200 hairy.png)
Open file (118.05 KB 1494x881 hairytiled.png)
>>577 >From what I read, rendering a single triangle, and rendering 2k triangles is exactly the same, because of how gpu works. This is true. I believe it's called batch rendering. There is a Raylib demo that demonstrates this, though it's all handled under-the-hood: ht tps://www.raylib.com/examples/textures/loader.html?name=textures_bunnymark >In fact, I suspect that textured 3d grid without shaders will render faster than my "render one 32x32 tile at a time" approach, Raylib is, and I suspect your libraries are as well, in fact, all 3D and running on the GPU (the shape-drawing functions are software, IIRC). You are using SDL right? Is there no SDLDrawQuad(Texture texture, Quad quad, Color ...) etc. function? What are you using to draw images to the screen now? >using 3d >Kind of pointless to me, since I use true 2d, because using 3d is much more work. I think you misunderstood me. Let me try again. I don't think you're retarded; I just haven't been clear enough. The worlds you and I are creating, as they exist in our minds and in our design notes, are fundamentally 3-dimensional. Buildings, fences, walls, objects, whatever, have height. Human males might be 6ft on average, whereas a goblin might be 4ft. They have height. This is all independent of how we try to present these worlds to the player. You may have bats or birds flying around. They get drawn with a Y-axis offset to show them as being above their X, Y position. In our case, we have chosen the "2.5D", "isometric" perspective to present this 3D world to the player. Why? Because the isometric perspective allows the viewer to see the front, left, and top of an object (or right, back, top, whatever, depending on its orientation). These 3 views, similar to what you might see in a 3D editor, are enough to accurately communicate something's 3-dimensional form. If something looks like a circle from those 3 angles, you know it's a sphere. If something looks like a square from those 3 angles, you know it's a cube. From a gameplay point of view as well, it allows the player to move freely in the X, Y plane, while also allowing a look at things' vertical dimension (a door in a wall, the face of an NPC, etc.) that a strictly top-down view does not. Of course, we aren't using 3D engines with 3D models to achieve this effect. I'm using pre-rendered 2D sprites just like you are, and for the same reasons that you are - it is less work. I mean, look at this asshole. My poor fucking goblin, stolen from a stock photo site and cruelly reduced to only 16 colors, is shown here jumping up and down jankily. Everything in this scene is a 2D texture, drawn on a 2D plane. The underlying asset, the Goblin(), really has a z coordinate though. I use that to determine the Y-offset to draw him at, and also the position of his shadow. In the underlying simulation, he is well and truly moving up and down. For the 2-dimensional on-screen presentation of that, I have to use some tricks. I do hope you get what I'm saying. >If I use 3d ground(which is the most 2d thing I have) You can render a lumpy, bumpy, rocky, grassy 3D terrain to a 2D texture and draw that. In fact, you're already doing it. See my edit of one of your ground tiles that exaggerates the z-axis element. You can have a 3-dimensional element to it, in this case, the rocks and grass, while still having it tile nicely (if drawn properly back to front). That's what I meant. That's the beauty of pre-rendered 3D. There are always drawbacks though - make your ground tiles too "3D" and things will look weird moving over them. >why not use 3d models for walls? And why not make ground stuff to be actual 3d modeled? And if 3d models for walls, why not use them for characters? It will be better and solve some problems, especially with animations, but its a lot of work. Well, yeah. That must have been the question Blizzard asked themselves when they started work on Diablo 3, because that game's all realtime 3D. There are strengths and weaknesses to each approach. I considered them and went with 2D. Blizzard considered them and went with 3D for D3 and D4. Realtime 3D allows for more animations, more character skins and visible items, realtime lights and shadows, and so on and and so forth. >I suspect I will have to move to 3d, but not today. You just might. As might I. As long as we keep the world being simulated separate from it's on-screen representation, there shouldn't really be any problem switching over. >The main goals are to have an engine, which is easy to work with, and easy to make content for it, and for it to look good enough. You and me both.
>>581 >Well, yeah. That must have been the question Blizzard asked themselves when they started work on Diablo 3, because that game's all realtime 3D If I was a big studio, I would not bother with 2d. However they do optimize a lot of things, like how trees are basically a couple of 2d planes. And with normal maps, a lot of things could be 2d with shaders. >sdl draw It only can store and render textures and primitives. As far as I know, everything works like on analogue monitor, Textures are 1d array of pixels (when you reach X res of texture, it moves to the next line), which are rendered to buffer/texture. (or surface, which is like a texture, but stored in ram, not gpu). >I think you misunderstood me. Let me try again Yeah, I will have few layers, and I considered rendering some grass as part of object layer, not ground, like picrelated. Or have tiles which are "bleed out" of their boundaries. like in your 2 and 3 pictures. But I dont think I will, because I cant think of a situation I need to, except for grass. With stones I can render them in the middle of the tiles, or in object layer. And other than random stones and grass, what else belongs in the ground layer but has height? And stuff like levelchange in >>561 will be walls instead. Also, a little trick I read somewhere. Make walls and other similar tiles a little bit darker at the bottom, it helps perspective. You also can make ground tiles a little bit brighter. But second one is probably less relevant if we render everything in blender, in which case ground likely will be brighter anyway. >separate rendering and game data It cant be done completely, since everything affects each other. I decided that player character will be 200 pixels tall(or 180), which dictates how tall objects are, which dictates level of details I need, and size of tiles. And it affects how I should draw objects, like in wall ref picrelated. I will need walls to be around 6m tall, when modeling, so they will cover empty ground tiles when Z levels change, and 2.825 meters tall in some other situations. I do generate everything procedurally, but even so, its hard to change how things look, in different resolution/scale. >>558 in this I just scaled down grass texture, which turns it into green noise. I do plan to render stuff at double or triple resolution, so stuff would look better(than simply rendering at final tile resolution), but I need to know final resolution of sprite, to make details at appropriate scale, so they dont turn into mush. Its not the end of the world, to redo stuff (I have like 20 versions of differently rendered grass) but still.

Report/Delete/Moderation Forms
Delete
Report

no cookies?