Thursday, December 22, 2016

Bioshock II Analysis

Here is one of about 10 game analyses I've completed over the last year. Probably about as exciting as reading Wikipedia, but there you go. The 1960 Presidential Debates were less interesting than the ones between Clinton and Trump, but I'd still say they had a lot more actual content.

1 Overview


Bioshock II is a story-driven FPS with RPG elements developed by 2K Marin and published in 2010. It takes place after the events of Bioshock and the levels feature similar environments to the first game.

2 Formal Elements


2.1 Players


The main campaign is single-player only, but there is a multiplayer mode as well. You play as a Big Daddy, known as subject Delta, who has been separated from his Little Sister, Eleanor.

2.2 Objectives


The main objective is to reunite with the Little Sister which you are bonded to, but there are optional objectives in the form of rescuing or harvesting all the Little Sisters, buying and/or upgrading all plasmids, completing the research on some or all of the enemies, and a few others.

2.3 Rules


The player may only have one weapon in the right hand and one plasmid in the left. If the player dies, they will respawn at a Vita-Chamber with half-health and some amount of EVE restored.
As a Big Daddy, you can attack other Big Daddies and take possession of their Little Sister. If you choose to, you can have them guide you to a corpse with ADAM to be harvested. While they harvest the ADAM, the player will have to protect her from waves of enemies.
There are a limited number of Little Sisters in each level, and in the game as a whole. The player must decide how to treat them, either rescuing them or harvesting them. Either choice awards the player with ADAM to use in leveling up, but harvesting gives more immediate benefit. After gathering all the Little Sisters in a level, the player will face a Big Sister, which is a faster and plasmid-enhanced version of a Big Daddy.
The game is a linear set of levels, yet each level may be explored freely by the player. Unlike the first game, there is no backtracking, as some levels are flooded or destroyed following story-events.
2.3.1 Equipment and Inventory

Managing one’s inventory is extremely important. You may only carry a limited amount of ammo for each weapon (although this may be upgraded throughout the game), and 5 health kits and 5 EVE hypos. Running short of ammo, health kits, or EVE hypos can easily result in death and respawning. Scavenging around the environments for these items and for cash to buy them at vending machines.
2.3.2 Hacking and Plasmids

Throughout the levels, the player will find vending machines, security cameras, and turrets of different kinds. These may all be hacked. The vending machines give reduced prices to the player, while the security cameras, turrets, and flying bots become allies in a fight. If the player is clever, they can maneuver a Big Daddy or Bid Sister into an area with hacked security cameras and turrets, turning a challenging fight into a fair or even easy one.

2.4 Procedures


There are several procedures that the player will engage in during play.
  1. Defeat Big Daddy/Big Sister: In order to acquire a Little Sister, the player must defeat a Big Daddy. These are initially neutral to the player as you go through each level, and only become hostile upon attacking them. Before attacking, a wise player will pick an ambush spot and stock up on ammo and health kits/EVE hypos. The Big Sister encounters are more difficult, since they will occur automatically after the player has freed or harvested all the Little Sisters in a level. These are one of the few times where I felt frustrated in the game, since I would often die, only to respawn and have half health/EVE and be minus the ammo and health items I’d used trying to defeat her the previous times. The player cannot advance without defeating the Big Sister.
  2. Defend Little Sister: During ADAM harvesting, the Little Sister is defenseless, and the player must defend her from waves of enemies. As the game progresses, the level of the enemies increases, making each harvest more harrowing.
  3. Find/Destroy Item: At several points during the game, the player will be tasked with finding or destroying a specific item(s). The player must explore the environment or follow the quest markers until the item is taken care of.
  4. Defeat Boss: There are several bosses in the game, and the player must learn their attack patterns to some extent in order to defeat them. However, many of them are simple bullet sponges.
  5. Explore Level: Exploring each level is a joy. The developers have hidden audiologs which tell backstory, give combinations for safes and locked doors, rare plasmids, and lots of random items and cash. Scavenging the items is necessary to succeed in the game, but learning about the history of the city of Rapture is also a pleasure.

2.5 Resources


2.5.1 Abstract
  • Health: Health measures how much damage the player can take. If it runs out, the player will respawn at the nearest discovered Vita-Chamber.
  • EVE: EVE allows the player to use plasmids.
  • ADAM: ADAM is gathered from corpses by Little Sisters. The player may either rescue the Little Sisters to receive their ADAM, or harvest them directly. Either way, the ADAM is necessary to purchase and upgrade plasmids.
  • Plasmid (upgrades): There are 10 plasmids in the base game, each of which may be upgraded twice. Each allows the player different abilities, such as becoming invisible, causing enemies to become allies and attack other enemies, summoning fire, ice, or electricity, etc.
  • Passive Plasmids: There are a large number of passive plasmids that the player can equip. These include abilities like linking health kits and EVE, so that whenever the player uses a health kit, a small amount of EVE is also replenished.

2.5.2 Physical
  • Weapons: There are 8 different weapons, each of which uses different ammo and is useful in different situations. The speargun, for example, functions as a sniper rifle, but has the useful property of allowing the player to retrieve the spears from fallen enemies and the walls/floor, etc. However, like most sniper rifles in other games, the amount of ammo that can be carried is extremely limited and the firing rate is quite slow.
  • Health kits: Health kits are used to replenish health. Only a limited number may be carried at any given time.
  • EVE hypos: EVE hypos are used to replenish one’s EVE, which is like mana or magicka and is used to cast plasmids. Only a limited number may be carried at any given time.
  • Ammo: Each gun has multiple types of ammo, but the player can only carry a limited amount each time.
  • Cash: Money is used to buy ammo and other items.

2.6 Conflicts


2.6.1 Rescuing versus Harvesting

As mentioned before, there is an immediate reward versus delayed benefit mechanic at work in this conflict. Harvesting, although morally questionable, gives the player a larger reward initially. However, for rescuing the Little Sisters the player receives extra ADAM and plasmids later. Thus the player could look at this simply as an economic choice, but personally, I remember seeing the struggle of a Little Sister while I harvested her the first time I played Bioshock, and after that I created a new game file just so that I would not have that ”virtual” crime on my conscious.
2.6.2 Revenge versus Forgiveness

While playing through certain levels, three different NPCs will either taunt the player, or send the player on dangerous missions before helping. At the end of the level, the player is faced with the choice of killing them or sparing them. Sparing them allows the player to achieve a ”positive” game ending, and for one of the NPCs at least, gives the player robot allies at specific points in the game.

2.7 Boundaries


2.7.1 Map

Each map may be freely explored, but they are arranged in a linear order and there is no backtracking allowed.
2.7.2 NPCs

Almost every NPC in the game is an enemy and must be killed or avoided. The only exceptions are the Little Sisters.

2.8 Outcomes


The game has limited outcomes. Depending on how you treat the Little Sisters and how you treat three key NPCs throughout the game, your Little Sister, Eleanor, will either forgive or try to exact revenge on the woman who kidnapped her.

3 Dramatic Elements


Like the first Bioshock and its successor, Bioshock Infinite, Bioshock II features a strong, well-written and voice-acted story. As a Big Daddy, the player has been genetically and experimentally modified to be bigger and stronger than a normal person. You are also bonded to a Little Sister, whose death will break the bond and cause you to drop into a coma.
The action of the story takes place entirely in and around the mid-ocean underwater city of Rapture, a Libertarian haven created by Andrew Ryan. As learned from the first game, and during play of the second, Rapture was supposed to be a utopia for free thinkers, artists, business-people and scientists who wanted to live free of the restrictions of conventional society. However, soon classes emerged, gangs appeared, and scientists created plasmids, genetic modifications which gave powers like telekinesis, the ability to summon fire, and freeze or electrocute things.
Little Sisters were introduced to harvest ADAM, a resource needed to create plasmids, and the Big Daddies were created to control the them. Chaos soon ensued, with most citizens going crazy and selfishly trying to get as much ADAM as they could.
Whereas the first game primarily explores the breakdown of a Libertarian society, the second game introduces the character of Sophia Lamb, a Collectivist. This allows the game to explore the concepts of collectivism. However, from a game mechanics point of view, there are no enemy types that could be called collective, for instance, an enemy that grabs onto the player and is killed in order to allow other enemies to damage the player.

3.1 Characters


The player receives messages from Eleanor Lamb throughout the game, and is in frequent passive radio contact with Sophia Lamb, Augustus Sinclair, Holloway, Poole, and Alexander, which copies the first game’s use of radios to provide context and mission goals. The player may also find notes scrawled on walls, audiotapes, and other indirect forms of communication from other characters.

3.2 Story


Your Little Sister has been kidnapped, and you must rescue her. The rest of the plot revolves around the player encountering multiple obstacles on the way to rescuing her.

4 Dynamic Elements


4.1 Hacking


Hacking allows the player to save money and to turn the security elements of the environment to the player’s advantage.

4.2 Plasmid/Weapon Interactions


The Inferno and Electrocute plasmids cause enemies to become shocked or set on fire, and thus create opportunities for the player to attack them with impunity. The Freeze plasmid turns enemies into blocks of ice, which can be destroyed if enough damage is dealt.

4.3 Plasmid/Environment Interactions


The Inferno and Electrocute plasmids allow many interactions with the environment. Oil spills are flammable, and any pool of water will kill any enemy standing in it if electrocuted. These interactions encourage player awareness of the environment and tactical planning of battles.

5 Conclusion


Bioshock II enhances many gameplay features from the previous game and gives the player an interesting second look at the world of Rapture.

5.1 Potent Elements


The weapons and plasmids interact with the environment during combat, and exploration of the environment allows the player to freely discover as much or as little of the story and secrets that Rapture has to offer. Besides the pure gameplay, the story is quite interesting and provides a few challenging moral questions.

5.2 Areas for Improvement


Although the UI in itself is quite helpful, I played the game on PC using a controller, which is something that either didn’t exist when the game was released, or was something which the developers never thought of. Consequently, all the user tips and explanations for how to do things were for the mouse and keyboard, and I had to discover what button / button combination did what. Especially in a game where resources are limited, this lead to a lot of wasted ammo, health kits, EVE hypos, etc, until I figured out the controls. New abilities are introduced throughout the game that make use of these unexplained controls.
As mentioned before, the fights against the Big Sisters cannot be avoided or skipped, and this can lead to a cycle where the player has less and less ammo, health kits, and EVO hypos to aid in defeating it.

Multiple Characters and Basic Needs

hey all,

Another busy and dramatic week at work. Three more days of work til winter vacation. Just a quick demo showing multiple characters and displaying their needs and body state. The next step is decision making.




I've setup a new channel on Youtube where you can find my video game videos.
Cheers,

Friday, December 16, 2016

Busy Week

hey all,

Really busy week at work and outside of it as well. Outside, I've been doing research on Voronoi diagrams, how to simulate diffusion, and I've also done a complete revamping of my code for needs. Instead of having Physical Needs and Emotional Needs treated separately, I've decided to make a BaseNeed class, and then have the NPCs have a Needs class which sets them up, initializes them, and takes care of keeping them up to date. The main reason for this is that when I started to write my code for updating mood based on Physical and Emotional Needs, I needed to write some extra functions and variables for dealing with their contributions separately, and then combining them.

More to come.




using UnityEngine;
using System.Collections;
using Gamelogic.Extensions.Algorithms;

public class BaseNeed:IBaseNeed
{
 [Range(0, 100)]
 float severity;
 float maxTime;
 MeetNeedEvent typicalMNE;
 //research the actual rate at which each physical need must be met
 //water: about 250ml every 3 hours, for a total of 2,000ml per day. more could cause problems. but depends on exercise level and temperature.
 //food: 2,000~3,000kC every day, spread between 2~5 meals. also depends on exercise
 //rest: between 6~9 hours per day. 
 //sex:??? once/twice per week?
 //cleanliness: about once per day, depending on exercise
 //comfort: ???increases if:
 //                          standing
 //                          working
 //                          sitting on ground
 //                          
 //
 //
 //public float changeRate;
 NeedType needType;
 ResponseCurveFloat curve;
 float timeSinceMet;
 bool isBeingMet;

 //used for determining the effect during decision making
 float multiplier;
 //public PhysicalNeedType type;

 #region Getters and Setters


 public NeedType Type
 {
  get { return needType; }
  protected set {; }
 }

 public float Severity
 {
  get {return severity; }
  protected set {; }
 }

 public bool IsBeingMet
 {
  get { return isBeingMet; }
  protected set {; }
 }

 public float Multiplier
 {
  get { return multiplier; }
  protected set {; }
 }

 //this is probably not needed, as the response curve is only ever referenced internally
 public ResponseCurveFloat Curve
 {
  get { return curve; }
  protected set {; }
 }


 #endregion

 public BaseNeed(NeedType ty, float sev, ResponseCurveFloat rc, float maxT, MeetNeedEvent mne ,float mult)
 {
  severity = sev;
  curve = rc;
  multiplier = mult;
  isBeingMet = false;
  timeSinceMet = 0f;
  typicalMNE = mne;
  maxTime = maxT;
  needType = ty;
 }

 // Use this for initialization
 void Start ()
 {
 
 }
 
 // Update is called once per frame
 void Update ()
 {
 
 }

 public void IncreaseNeed(float timePassed)
 {
  if (!isBeingMet)
  {
   timeSinceMet += timePassed;
   severity = curve[timeSinceMet];
   //Debug.Log("Need increased to " + severity.ToString());
  }
 }

 public IEnumerator MeetNeed(MeetNeedEvent mne=new MeetNeedEvent())
 {
  //this is because the default parameter must be a constant at compile time
  //so if a value is not passed in, use the typical MeetNeedEvent for this need
  if (mne.eventLength <= 0)
  {
   mne = typicalMNE;
  }
  isBeingMet = true;
  float t = 0f;
  //Debug.Log("meeting need");
  //Debug.Log("mne length is" +mne.eventLength);
  //Debug.Log("typical mne length is" + typicalMNE.eventLength);
  while (t < mne.eventLength)
  {
   t += Time.deltaTime;
   //Debug.Log("value of t in MeetNeed is " + t);
   //Debug.Log("change in severity is " + (t / mne.eventLength) * mne.potency);
   //this value needs to be clamped!!!
   severity = Mathf.Clamp(severity - ((t / mne.eventLength) * mne.potency), 0f, 100f);
   //severity -= (t / mne.eventLength) * mne.potency;
   yield return null;
  }
  //Debug.Log("This should only be called when finished");
  isBeingMet = false;
  //Debug.Log("isBeingMet is now " + isBeingMet.ToString());
  timeSinceMet = Mathf.Clamp(maxTime-(maxTime*mne.potency/100f),0f,maxTime);
  //Debug.Log("Time since met is " + timeSinceMet);
 }
}

public struct MeetNeedEvent
{
 public float eventLength;
 [Range(0,100)]
 public float potency;

 public MeetNeedEvent(float el=-1f, float p =-100f)
 {
  eventLength = el;
  potency = p;
 }
}

public enum NeedType
{
 Food,
 Hydration,
 Rest,
 Sex,
 Comfort,
 Cleanliness,
 Shelter,
 Intimacy,
 Fun,
 Safety,
 Respect,
 MAX
}

public interface IBaseNeed
{
 //(get) accessor functions for:
 //severity
 //type
 NeedType Type
 { get; }
 //Multiplier
 float Multiplier
 { get; }

}

using UnityEngine;
using System;
using Gamelogic.Extensions.Algorithms;
using System.Collections.Generic;

public class Needs : MonoBehaviour
{
 const int numberOfNeeds = (int)NeedType.MAX;
 ////this holds the response curve for how each need will affect mood
 ////does this really belong here or should it go in mood?
 List < ResponseCurveFloat > moodResponseCurve = new List < ResponseCurveFloat > ();



 BaseNeed[] needs = new BaseNeed[numberOfNeeds];

 NPCState npcState;



 public delegate void NeedStateChangeHandler(object source, NeedEventArgs args);//set up delegate
 public event NeedStateChangeHandler NeedStateChanged;//define event based on event


 #region Getters and Setters

 public int NumberOfNeeds
 {
  get { return numberOfNeeds; }
  protected set { }
 }

 public ResponseCurveFloat MoodResponse(int i)
 {
  return moodResponseCurve[i];
 }

 public float Hunger
 {
  get { return needs[(int)NeedType.Food].Severity; }
  protected set { }
 }

 public float Tiredness
 {
  get { return needs[(int)NeedType.Rest].Severity; }
  protected set { }
 }

 public float Thirst
 {
  get { return needs[(int)NeedType.Hydration].Severity; }
  protected set { }
 }

 public float Lust
 {
  get { return needs[(int)NeedType.Sex].Severity; }
  protected set { }
 }

 public float Comfort
 {
  get { return needs[(int)NeedType.Comfort].Severity; }
  protected set { }
 }

 public float Cleanliness
 {
  get { return needs[(int)NeedType.Cleanliness].Severity; }
  protected set { }
 }

 public float Shelter
 {
  get { return needs[(int)NeedType.Shelter].Severity; }
  protected set { }
 }

 public float Intimacy
 {
  get { return needs[(int)NeedType.Intimacy].Severity; }
  protected set {; }
 }


 public float Fun
 {
  get { return needs[(int)NeedType.Fun].Severity; }
  protected set {; }
 }

 public float Safety
 {
  get { return needs[(int)NeedType.Safety].Severity; }
  protected set {; }
 }

 public float Respect
 {
  get { return needs[(int)NeedType.Respect].Severity; }
  protected set {; }
 }

 #endregion

 //the steepness, emults, and xMidpoints need to be adjusted for each emotional need
 void SetupNeeds()
 {

  double[] steepnesses = new double[numberOfNeeds] { 0.2f, 0.4f, 0.4f, 0.1f, 0.4f, 0.1f, 0.2f, 0.2f, 0.4f, 0.4f, 0.1f };
  double[] eMults = new double[numberOfNeeds] { 1f, 1f, 10f, 1f, 10f, 0.1f , 1f , 1f, 1f, 10f, 1f };
  double[] xMidpoints = new double[numberOfNeeds] { 24, 12, 16, 84, 10, 72, 24, 24, 12, 16, 84 };
  double yMax = 100;
  double yMaxforCalc = 99.99999f;

  for (int i = 0; i < needs.Length; i++)
  {
   //Debug.Log("need " + ((PhysicalNeedType)i).ToString());

   double maxX = -((System.Math.Log((yMax / yMaxforCalc) - 1) - System.Math.Log(eMults[i])) / steepnesses[i]) - xMidpoints[i];
   //Debug.Log("double max x is " + maxX);
   //Debug.Log("float max x is " + maxX);
   double dx = maxX / 10f;
   List < float > xVals = new List < float > ();
   List < float > yVals = new List < float > (); //use this as the input for the mood response curve
   float yv;
   float myv;
   List < float > moodYVals = new List < float > (); // use this as the output for the mood response curve
                //Debug.Log("need " + ((PhysicalNeedType)i).ToString());
   for (int xs = 0; xs < 10; xs++)
   {
    xVals.Add((float)(xs * dx));
    //Debug.Log(xs * dx);
    if (xs == 0)
    {
     yv = 0f;
    }
    else if (xs == 9)
    {
     yv = (float)yMax;
    }
    else
    {
     yv = (float)(yMax / (1 + eMults[i] * (System.Math.Pow(System.Math.E, -steepnesses[i] * ((xs * dx) - xMidpoints[i])))));
    }


    yVals.Add(yv);
    //Debug.Log(yMax / (1 + eMults[i] * (System.Math.Pow(System.Math.E, -steepnesses[i] * ((xs * dx) - xMidpoints[i])))));
    if (yv > = 50f)
    {
     //gives a penatly to mood
     //make sure to clamp this value
     myv = Mathf.Clamp(-(Mathf.Pow(yv - 50f, 3) / 1000f), -100, 100);
    }
    else
    {
     //gives a bonus to mood
     //make sure to clamp this value
     myv = Mathf.Clamp(-(Mathf.Pow(yv - 50f, 3) / 50000f) + 5f, -100, 100);
     //Debug.Log(-(Mathf.Pow(yv - 50f, 3) / 50000f) + 5f);
    }
    moodYVals.Add(myv);

   }

   moodResponseCurve.Add(new ResponseCurveFloat(yVals, moodYVals));

   float time = 10f;
   float potency = 100f;
   MeetNeedEvent mne = new MeetNeedEvent(time, potency);
   needs[i] = new BaseNeed((NeedType)i, 0f, new ResponseCurveFloat(xVals, yVals), (float)maxX, mne, 100f);
   //needs[i] = new PhysicalNeed(0f, 1f, 1000f, (PhysicalNeedType)i);
  }
  Debug.Log("Needs setup");
 }

 public void ChangeNeedState(NeedType ty, float amount)
 {
  switch (ty)
  {
   case NeedType.Food:
    //Debug.Log("Trying to increase food need");
    needs[(int)NeedType.Food].IncreaseNeed(amount);
    break;
   case NeedType.Hydration:
    //Debug.Log("Trying to increase hydration need");
    needs[(int)NeedType.Hydration].IncreaseNeed(amount);
    break;
   case NeedType.Rest:
    //Debug.Log("Trying to increase rest need");
    needs[(int)NeedType.Rest].IncreaseNeed(amount);
    break;
   case NeedType.Sex:
    //Debug.Log("Trying to increase sex need");
    needs[(int)NeedType.Sex].IncreaseNeed(amount);
    break;
   case NeedType.Comfort:
    //Debug.Log("Trying to increase comfort need");
    needs[(int)NeedType.Comfort].IncreaseNeed(amount);
    break;
   case NeedType.Cleanliness:
    //Debug.Log("Trying to increase cleanliness need");
    needs[(int)NeedType.Cleanliness].IncreaseNeed(amount);
    break;
   case NeedType.Shelter:
    needs[(int)NeedType.Shelter].IncreaseNeed(amount);
    break;
   case NeedType.Fun:
    needs[(int)NeedType.Fun].IncreaseNeed(amount);
    break;
   case NeedType.Intimacy:
    needs[(int)NeedType.Intimacy].IncreaseNeed(amount);
    break;
   case NeedType.Respect:
    needs[(int)NeedType.Respect].IncreaseNeed(amount);
    break;
   case NeedType.Safety:
    needs[(int)NeedType.Safety].IncreaseNeed(amount);
    break;
   default:
    break;
  }
  OnNeedStateChanged();
 }

 protected void OnNeedStateChanged()
 {
  if (NeedStateChanged != null)
  {
   //treating it like a function
   //this is used to notify subscribers to this event
   NeedEventArgs n = new NeedEventArgs();
   n.need = this;
   NeedStateChanged(this, n);
   //Debug.Log("Physical State changed");
  }
 }

 void OnNPCStateChanged(object source, StateEventArgs n)
 {
  switch (n.state.Body)
  {
   case BodyState.Resting:
    ChangeNeedState(NeedType.Cleanliness, Time.deltaTime);
    if (!needs[(int)NeedType.Rest].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Rest].MeetNeed());
    }
    ChangeNeedState(NeedType.Food, Time.deltaTime);
    ChangeNeedState(NeedType.Hydration, Time.deltaTime);
    if (!needs[(int)NeedType.Comfort].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Comfort].MeetNeed());
    }
    ChangeNeedState(NeedType.Sex, Time.deltaTime);
    break;
   case BodyState.Eating:
    ChangeNeedState(NeedType.Cleanliness, Time.deltaTime);
    if (!needs[(int)NeedType.Food].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Food].MeetNeed());
    }
    ChangeNeedState(NeedType.Rest, Time.deltaTime);
    ChangeNeedState(NeedType.Hydration, Time.deltaTime);
    ChangeNeedState(NeedType.Comfort, Time.deltaTime);
    ChangeNeedState(NeedType.Sex, Time.deltaTime);
    break;
   case BodyState.Drinking:
    ChangeNeedState(NeedType.Cleanliness, Time.deltaTime);
    if (!needs[(int)NeedType.Hydration].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Hydration].MeetNeed());
    }

    ChangeNeedState(NeedType.Rest, Time.deltaTime);
    ChangeNeedState(NeedType.Food, Time.deltaTime);
    ChangeNeedState(NeedType.Comfort, Time.deltaTime);
    ChangeNeedState(NeedType.Sex, Time.deltaTime);
    break;
   case BodyState.Sexing:
    ChangeNeedState(NeedType.Cleanliness, Time.deltaTime);
    if (!needs[(int)NeedType.Sex].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Sex].MeetNeed());
    }
    if (!needs[(int)NeedType.Comfort].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Comfort].MeetNeed());
    }
    ChangeNeedState(NeedType.Food, Time.deltaTime);
    ChangeNeedState(NeedType.Hydration, Time.deltaTime);
    ChangeNeedState(NeedType.Rest, Time.deltaTime);
    break;
   case BodyState.Bathing:
    if (!needs[(int)NeedType.Cleanliness].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Cleanliness].MeetNeed());
    }
    if (!needs[(int)NeedType.Comfort].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Comfort].MeetNeed());
    }
    ChangeNeedState(NeedType.Rest, Time.deltaTime);
    ChangeNeedState(NeedType.Food, Time.deltaTime);
    ChangeNeedState(NeedType.Hydration, Time.deltaTime);
    ChangeNeedState(NeedType.Sex, Time.deltaTime);
    break;
   case BodyState.Default:
    ChangeNeedState(NeedType.Rest, Time.deltaTime);
    ChangeNeedState(NeedType.Food, Time.deltaTime);
    ChangeNeedState(NeedType.Hydration, Time.deltaTime);
    ChangeNeedState(NeedType.Sex, Time.deltaTime);
    ChangeNeedState(NeedType.Comfort, Time.deltaTime);
    ChangeNeedState(NeedType.Cleanliness, Time.deltaTime);
    break;
   default:
    break;
  }

  switch (n.state.Feeling)
  {
   case EmotionalStateType.Socializing:
    if (!needs[(int)NeedType.Intimacy].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Intimacy].MeetNeed());
    }
    if (!needs[(int)NeedType.Fun].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Fun].MeetNeed());
    }
    ChangeNeedState(NeedType.Respect, Time.deltaTime);
    break;
   case EmotionalStateType.Playing:
    if (!needs[(int)NeedType.Fun].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Fun].MeetNeed());
    }
    ChangeNeedState(NeedType.Intimacy, Time.deltaTime);
    ChangeNeedState(NeedType.Respect, Time.deltaTime);
    break;
   case EmotionalStateType.Working:
    ChangeNeedState(NeedType.Fun, Time.deltaTime);
    break;
   case EmotionalStateType.Arguing:
    if (!needs[(int)NeedType.Intimacy].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Intimacy].MeetNeed());
    }
    ChangeNeedState(NeedType.Fun, Time.deltaTime);
    break;
   case EmotionalStateType.BeingRespected:
    if (!needs[(int)NeedType.Respect].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Respect].MeetNeed());
    }
    if (!needs[(int)NeedType.Intimacy].IsBeingMet)
    {
     StartCoroutine(needs[(int)NeedType.Intimacy].MeetNeed());
    }
    ChangeNeedState(NeedType.Fun, Time.deltaTime);
    break;
   case EmotionalStateType.Default:
    if (n.state.IsAlone)
    {
     ChangeNeedState(NeedType.Intimacy, Time.deltaTime);
     ChangeNeedState(NeedType.Respect, Time.deltaTime);
    }
    ChangeNeedState(NeedType.Fun, Time.deltaTime);
    
    //ChangeNeedState(NeedType.Safety, Time.deltaTime);
    break;
   default:
    break;
  }
  OnNeedStateChanged();
 }


 // Use this for initialization
 void Start()
 {
  SetupNeeds();
  npcState = FindObjectOfType < NPCState > ();
  npcState.StateChanged += OnNPCStateChanged;
 }

 // Update is called once per frame
 void Update()
 {

 }
}





public class NeedEventArgs : EventArgs
{
 public Needs need { get; set; }
}


Thursday, December 8, 2016

UI Analysis

hey all,

A little UI analysis from Total War: Warhammer this time. I've only put about 20 hours into this game over the last several weeks, but I have noticed some really amazing and (IMHO) really crappy UI design. On the plus side, there is the map UI. You can see a city and an army icon in the image below. The army is marked by the humanoid figure and the the flag, while the city is the group of buildings partially blocked behind him. The yellow outlines show how far the unit can move during a turn. Everything the player might want to know is shown clearly, or can be displayed with a single click: clicking on the city would display the upgrades it has and that are available, while clicking on the army would show the general in charge and the units, their health and experience levels.


 Should you wish to know more about the general, you can bring up the screen below. This shows the general's stats and battle bonuses and campaign bonuses to the far left. The center panel shows the items, location, traits, and followers. While functional, the inventory on the top half does not live up to any RPG inventory standard. Skyrim, Mount & and Blade: Warband, hell, even Deus Ex from 16 years ago is better looking. Functionally, the drop down menus do a good job of showing what item the general has equipped, either other items are available for the slot, and who has other equivalent items.


Should you wish to invest skill points in abilities, the player can use the menu below. Again, while functional and basically clear, please notice the arrow. I played the game for over 40 turns, that's about 12 hours, before I noticed those little spheres. It turns out that when they are uncolored you have not or cannot invest in the skill. The number of spheres shows the number of times you can invest in that skill. Maybe I am just not observant, but any pretty critical UI element that the player only discovers after 12 hours is not doing its job. Increasing the size of them, making them flash, using highlighted numbers, or breaking each button into sections which progressively become filled in would be improvements.


The next two shots show parts of the UI for generals and cities. The arrows point to elements that allow the player to view the next general or city. Similarly to the spheres for skills, I discovered these after a few hours of play. They blend in a little too well with the background and look so much like decorative elements that they are easy to ignore.



Back on the plus side, there are a large number of events: wars, diplomatic changes, construction project completion, research completion, and generals leveling up. Below the map, there is an event panel list which displays these events. By clicking on them, the player can get more information, and even go to the location of the event.


Last one. When you select a general from the map, you get this panel in the lower left corner. It shows a portrait, the current level and how much progress to the next level, the items currently equipped, and the general's stance. The last is in the lower right corner, and similar to other elements in the UI, I did not discover this until about 30 hours into the game. Granted, it has been quite some time since I played a strategy game, but the basic tutorials are really lacking a few elements.

Cheers,

Tuesday, December 6, 2016

Monday, December 5, 2016

Back to the Basics (Needs)

hey all,

I've been refactoring my code for needs over the last few days, and here are some of the changes I've made.


using UnityEngine;
using System.Collections;
using Gamelogic.Extensions.Algorithms;

public class Need
{
 [Range(0, 100)]
 public float severity;
 float maxTime;
 MeetNeedEvent typicalMNE;
 //public float changeRate;
 public ResponseCurveFloat curve;
 float timeSinceMet;
 public bool isBeingMet;

 //used for determining the effect during decision making
 public float multiplier;


These are the variables that my Need class contains. The severity just tracks the severity of the need, which can only be from 0 to a maximum of 100. For physical needs such as food and water, I'm planning on causing death to occur when it reaches the maximum. maxTime was the most difficult. If you recall from my previous post, I am using variations on the equation


to calculate how severe a need should be after x time has passed. The problem is that this is a logistic equation, and it has a horizontal asymptote at max_y, so you cannot just use




to try and find the x value when y = max_y, because that value does not exist. At the moment, I'm using a y value of 99.999999 and it seems to be working OK, although I do get some floating point errors every now and then.

In any case, I use the first equation to set up the response curve in the PhysicalNeeds and EmotionalNeeds classes, and also create a default MeedNeedsEvent.
 public Need(float sev, ResponseCurveFloat rc, float maxT, MeetNeedEvent mne ,float mult)
 {
  severity = sev;
  curve = rc;
  multiplier = mult;
  isBeingMet = false;
  timeSinceMet = 0f;
  typicalMNE = mne;
  maxTime = maxT;
 }

 // Use this for initialization
 void Start ()
 {
 
 }
 
 // Update is called once per frame
 void Update ()
 {
 
 }

 public void IncreaseNeed(float timePassed)
 {
  if (!isBeingMet)
  {
   timeSinceMet += timePassed;
   severity = curve[timeSinceMet];
   
  }
 }

This is another change from before. Instead of having those ugly incremental changes in an Update function, I'm now calling a coroutine that just ticks along by itself until the MeetNeedEvent is finished. I'm actually thinking that having the IncreaseNeed function be a coroutine might be a much better idea as well, and just use body states in the PhysicalNeeds class to start and stop the appropriate coroutines.
 public IEnumerator MeetNeed(MeetNeedEvent mne=new MeetNeedEvent())
 {
  //this is because the default parameter must be a constant at compile time
  //so if a value is not passed in, use the typical MeetNeedEvent for this need
  if (mne.eventLength <= 0)
  {
   mne = typicalMNE;
  }
  isBeingMet = true;
  float t = 0f;
  while (t < mne.eventLength)
  {
   t += Time.deltaTime;
   //this value needs to be clamped!!!
   severity = Mathf.Clamp(severity - (t / mne.eventLength) * mne.potency, 0f, 100f);
   yield return null;
  }
  isBeingMet = false;
  timeSinceMet = maxTime-(maxTime/100f*mne.potency);
 }
}


The point of having maxTime subtract a fraction of itself is that the potency of the event should affect the level of the need afterwards. If I have a snack, my hunger need will not be as satisfied as if I had had a hearty bowl of sweet and sour pork.

public struct MeetNeedEvent
{
 public float eventLength;
 [Range(0,100)]
 public float potency;

 public MeetNeedEvent(float el=-1f, float p =-100f)
 {
  eventLength = el;
  potency = p;
 }
}


So that's my base Needs class. PhysicalNeeds and EmotionalNeeds inherit from this class. I hope that I can start implementing how these needs affect mood.The current AI model is something like this:

Thursday, December 1, 2016

Ancient Water, Revamped and Refined

hey all,

I've gone through and done a bit of play testing for my Ancient Water game. As you can see from the picture, I've replaced the water slips and crop slips with colored glass beads. These pieces get moved around a lot, and the paper slips were just too hard to count quickly and move around smoothly, so they are gone. Also, I made some new smaller boxes that fit the size of the river much better.

Mechanically, here are the current rules.

  1. Setup
    • The board starts with the empty boxes on the river.
    • The player roles two six-sided dice to determine how much water goes in the top most box.
    • The player then decides how to distribute their population. The player starts with four people who they may distribute however they want. (This is for single player play. For two player or multiplayer, different colored pieces will be needed for each player. Plus an extra roll of the dice to figure out which player goes first, second, third, etc. Only one player may occupy a region at a time.)
  2. Each turn, there are seven actions that take place.
    1. Boxes are cycled down one, and the bottom most box is placed at the top. Roll dice to determine how much water goes in the top river box.
    2. If the player has four people in one area, the player may build an irrigation project. The next level of irrigation requires six people, and the final level nine people.
    3. Both six-sided dice are rolled to see if any one dies. If the total is a 3 or 4, someone dies.
    4. The player may decide to move their population around, but must have at least one person in an area in order to collect water and grow crops.
    5. Water is collected from the river box adjacent to the player's area. Without irrigation, only one unit of water may be collected, but each level of irrigation adds one to this:
      1. Level 1: two units of water
      2. Level 2: three units of water
      3. Level 3: four units of water
    6. Every two units of water collected may be converted in one crop. The people can survive two turns without crops, but if they do not have any crops after two complete turns, they will die.
    7. Lastly, every two units of crops can be converted into one person. It requires one crop to maintain between one to four people, so if the population increases to five, two crops must be saved.
That's it. I've been thinking of creating a short story, something like "You are the chief of the _____ people and it is your job to help them stay alive..." just to motivate the action. There are still no endgame conditions, but if the population reaches zero, of course the game is over. I still need to see about how long it takes to reach a population of ten, but I think being the first player to reach ten or 15 should be the winner.