So I have a fairly simple game that runs with a constant framerate, and I have objects that all do their thing in the game loop.
No you don’t. You have a game that appears to be running at a steady framerate but in fact is always off by a few nanoseconds either way, more so on older machines, and possibly even on your machine when too many objects appear.
If you are basing time in your game off how many frames you have rendered you are doing it wrong.
My first question involves things like projectiles and like objects that are spawned by other objects. Should calling the projectile's functions be the job of the object that it belongs to? The way I have it, my player object has a projectile object called, let's say, "ball", as a member object, and the player receiving input calls the function that causes the ball to move. The ball is initialized with a constant pointer to the player's position so it knows where to spawn. I then call its move() function from the player object, so each time the player object iterates through the game loop, the ball also moves. I also have to have the player call the ball's die() function for when the player leaves a room, because otherwise the ball hangs in limbo until the next time I enter a similar room, where it continues where it left off.
You are far from the mark here. Both the ball and the player are world entities and are updated by the world manager. One does not update the other. The player class does not update the ball. The world manager updates all objects in the world. It’s that simple.
You’ve already encountered one of the most obvious reasons for this: If the player stops updating so does the ball.
You’ve made a connection between the player and the ball because you know from a high-level design-based view that the player owns the ball as its weapon.
That high-level design-based idea does not apply to the low-level implementation of the system in which their only relationship is that the ball came from the player’s position and travels in a line based on which way the player was facing. Aside from spawning the ball, there is no relationship to the player at all.
Spawning the ball is done at higher-level logic, specifically it would be scripted if scripts are available or coded into the “game” layer of C++ otherwise. The “engine” layer would handle updating and moving the ball.
The second question is about programming the objects' behaviors. When I want my objects to do specific actions for a set amount of time, such as traveling a set distance, or flashing for a set amount of time, I've utilized a timer variable that is exclusive to that object that counts up to a point and then calls all the functions it needs to based on what position in the timer it's in.
These kinds of timers are implemented by setting them to the amount of time before they need to trigger and counting down until they reach 0.
The reason is to save memory; with your implementation you need to store both the current time and the target time.
Just store the current time but have it always count down to 0.
So should my game loop handle spawning objects that are dependent on other objects, or should the objects that use them? And is using timer variables clunky/is there a better way?
As I mentioned, this is a high-level task which should be done at a scripting or equivalent level.
No timers are not clunky. If you want a time-delay before spawning, use a timer. Again, it could be as simple as something that always counts down to 0.
Also, how big should my game loop be before it gets ugly? Say, if I have an adventure RPG going on with the complexity of the original Legend of Zelda. Most of my questions here are about conventions, lol. What do you guys do/recommend I do, based on personal experience?
Your game loop should not contain any game logic. It gets ugly the second a single line of game logic is written into it.
General Game/Engine Structure