Monday, October 13, 2014

void EverythingThatRisesMust(Collision collision)



In its resting state, the current build of project_catalan may not look much different from the previous one (a bit more red maybe), but a lot has changed under the hood. The changes, in my mind, are akin to standing in the doorway of the empty shell of a house and thinking "all right, we're going to need to shape out the living room, so we'll need walls here, and a load-bearing column there, through which we'll route water and electricity..." We haven't even gotten to the studs and sheetrock, much less the arguments about which of your friends' paintings are going to hang near where people might be, like, eating. As in any self-impelled project, you're always glancing that far ahead from time to time, and it can be useful to attempt to anticipate what problems you may need to solve in the future, to the extent that it helps you choose the solutions to your current problems that make future you's life easier, but it would be a classic game development mistake to fall afoul of something so seductive and misleading as a plan.

This last sprint was about damage, death and communication. How does one character damage another, and how is that information communicated to everyone who needs it, including the player. I found Unity's new UI system (introduced in 4.3?) to be an absolute miracle, in the sense that it allowed me to do sensible things. My previous experiences with Unity UI involved making instances of things like GUI.Box in the OnGUI function of something attached to the camera, which seemed to mostly work for what I needed but had the usability flavor of writing a Windows GUI application in C# without the benefit of XAML, or doing web layout by changing numerical values in raw CSS files and then refreshing the page. It wasn't very IDE-integrated, is what I'm saying, and the new version totally is. When I was contemplating how to implement MMO health bars for the enemies, I was at first thinking about scripted GUI.Texture instances translating and scaling based on a mathematical relationship between the enemy and the camera. You'd have to be a masochist to write such a system, so I imagine the preferred solution was just some Doom-style billboarding on quad meshes attached as children of whoever gets a healthbar, all on some custom collision layer that ignores the rest of the game world. Unity recognized that not using the UI system was better than using it, so they fixed it. Now we can have world space Canvases as children of moving gameObjects, and a an enemy's health bar can be built with basically no code:



Allowing for player damage was somewhat trickier. I wanted a standard player HUD, and using the new Canvas tools in combination with the main gameplay camera produced undesirable results. I ended up adapting a technique I had seen the Snuggletruck guys explain in a seminar: I created an orthograhic camera "box" far away from the action, and gave it a positive Depth so that it would render over the gameplay camera. This freed me to compose a UI in Scene view without feeling crowded by actual gameplay objects:


The problem with this approach (Screen Space - Camera) is that it's not very forgiving to resizing / rescaling. Right now I'm viewing all of my current projects as portfolio pieces aimed at a standard web browser, but not building in UI scaling functionality as you go is a recipe for "platform prison syndrome". The new anchor system is quite elegant and only a little over my head, so this feels like a very solvable problem once it needs to get solved.

The idea of implementing one last feature for this build, pushing a character away from a damage impact, turned out be much like Columbo un-mouthing his cigar, turning his head slightly, and asking about "one more thing". The ur-idea here is of course from the NES Legend Of Zelda  (yes, catalan is a Zelda clone. I will also stay up all night with you argiung that the Diablo games are Zelda clones, provided you're buying). A character being melee'd by an enemy should be slammed back, not far enough or long enough to disrupt gameplay, just enough to provide that sensation of "yes, dumbass, you touched a hot stove. How bout you try to avoid doing that?"

Teaching in video games is not done thorough tutorials, which are not widely read or played or viewed or whatever. Teaching is done by presenting the player with a variety of hot stoves and inviting them to make like Buddy Rich on some bongos. See what you like! They'll figure it out. If not, they'll leave you a nasty review claiming the game is too hard. Look, all I can do here is make stoves and heat them to various degrees. It's up to you to decide what to touch, that's what our relationship is about.

Trying to sort out combat, I worked through all the permutations of Unity colliders, triggers, rigidbodies... you can't just point at any of the onscreen characters and ask what collisions they've experienced recently, it's all very dependent on what components the attacker and defender were sporting, the complexity got silly, I even used some joints (er, Fixed Joints, that is). One morning at about 3:40AM I realized part of one of my "Push" function involved assigning one variable's value to another variable that just had a different name. I was writing in circles. I called it off.

The "Zelda Push" ended up not making use of colliders at all. It doesn't even require rigidbodies! It's dirty secret is performing a while loop inside a CoRoutine, and using "yield return null" at the end of each loop iteration. so the while loop actually updates at the same speed as the main game loop. Here's how the whole thing ended up looking (for context, this method lives in healthStates.cs, which is attached to any object whose health we care about:
IEnumerator DoPush(GameObject pusher)
 {
  Vector3 startPoint = transform.position;
  Ray pushRay = new Ray();
  if(pusher.GetComponent())
  {
   //Debug.Log ("melee push");
   pushRay = new Ray(startPoint, transform.position - pusher.transform.position);
  }
  else if(pusher.GetComponent())
  {
   //in this case using the bullet for the ray origin gets wonky
   //so we get the gun location and start the ray there
   //Debug.Log ("bullet push");
   bullet_base bulletScript = pusher.GetComponent();
   Transform firearm = bulletScript.weapon;
   pushRay = new Ray(startPoint, transform.position - firearm.transform.position);                
  }
  else
  {
   Debug.Log ("invalid pusher");
  }
  Vector3 flatPushDir = new Vector3(pushRay.direction.x, 0.0f, pushRay.direction.z);
  pushRay.direction = flatPushDir;
  Debug.DrawRay(pushRay.origin, pushRay.direction*1000, Color.red, 10.0f);

  float pushLength = 5.0f;
  Vector3 pushPoint; 
  pushPoint = pushRay.GetPoint(pushLength);
  //pushBall = Instantiate(pushBallTemplate, flatEndPoint, Quaternion.identity) as GameObject;

  float pushStartTime = Time.time;
  float distanceCovered = 0.0f;
  float fractionComplete = 0.0f;
  float pushSpeed = 50.0f; //this actually matters in relation to the pushLength
  while(fractionComplete < 0.9f)
  {
   distanceCovered = (Time.time - pushStartTime) * pushSpeed;
   fractionComplete = distanceCovered / pushLength;
   transform.position = Vector3.Lerp(startPoint, pushPoint, fractionComplete);
   //Debug.Log("fractionComplete = " + fractionComplete);
   yield return null;
  }
 }


Once I built the new version, befoe deploying it to the web I tested it locally and encounteerd this error:
Built with beta version of Unity. Will only work on your computer!
which is pretty delightful. I guess as of 4.6.0, the games we're able to build at home are somehow ahead of the WebPlayer to the point where they could become dysfunctional? As a matter of fact when I ran the game under chrome, the healthbars were offset and didn't scale properly, as I'm sure you'll see if you check the game out. I don't know how FF or IE deal, I didn't have the heart to find out. I mean, yes, it's my own fault for trying to create software to present to the public with beta tools, but I much would have preferred messages more specific to my situation:

  • Other computers may choke on your ridiculous, leaky algorithms!
  • Other computers may just not have the CPU/GPU muscle to simultaneously lift and lower all the vertices of your tremendously detailed and natural game world which is not optimized in any significant way 
  • Your game world has, in defining its origins and boundaries, mishandled the spatial and memory assumptions of the machine you're working on so as to create some sort of novel geometric figure, somewhat like a turd the size of the universe spinning at (0,0,0).

Just some decent error messages. That's all anyone should feel comfortable asking for.

Coming Up:

  • How bout them models
  • Some kind of hooky concept seems about ripe... this can only stay generic for so long
  • Models should perform some kind of death sequence
  • What's it all about, killing purple dudes? Do they drop money which allows you to buy better guns? From whom? The more solid your gameplay systems become, the more there is this need to commit to some kind of content interpretation in order to build more sophisticated sub-systems that enhance that interpretation. Generic gameplay engines are great, but you have to plant an conceptual / content-centric flag in order to make that into a game, and that's a process of walking through one of many doors and feeling a ton of other doors slam closed. You better hope you walked through a door you can live with!  





No comments:

Post a Comment