For starters, I'm implementing a new quest action to make followers. Besides issues registering it (viewtopic.php?f=22&p=58528), the code is kind of working so far. It basically:
- adds the follower as enemy
- respawn it on transitions
- it follows the player around (it actually tries to attack the player, but I made it so it can't use melee attacks; problem is with ranged, though )
UPDATE: I tried going with the follower in a dungeon. Besides obvious pathfinding issues, it's working good enough. I'll probably fix the problem teleporting the follower around the player if too distant.
Code: Select all
using UnityEngine;
using System;
using System.Text.RegularExpressions;
using DaggerfallWorkshop.Utility;
using FullSerializer;
using DaggerfallWorkshop.Game.Utility;
namespace DaggerfallWorkshop.Game.Questing
{
public class CreateFollower : ActionTemplate
{
enum Seq {none, creating, done};
Symbol foeSymbol;
Seq seq = Seq.none;
GameObject foeObject;
EnemyMotor motor;
EnemySenses senses;
EnemyAttack attack;
public override string Pattern
{
get
{
return @"create follower (?<symbol>[a-zA-Z0-9_.-]+)";
}
}
public CreateFollower(Quest parentQuest)
: base(parentQuest)
{
PlayerEnterExit.OnTransitionExterior += PlayerEnterExit_OnTransition;
PlayerEnterExit.OnTransitionInterior += PlayerEnterExit_OnTransition;
PlayerEnterExit.OnTransitionDungeonExterior += PlayerEnterExit_OnTransition;
PlayerEnterExit.OnTransitionDungeonInterior += PlayerEnterExit_OnTransition;
StreamingWorld.OnInitWorld += StreamingWorld_OnInitWorld;
}
public override void InitialiseOnSet()
{
}
public override IQuestAction CreateNew(string source, Quest parentQuest)
{
// Source must match pattern
Match match = Test(source);
if (!match.Success)
return null;
// Factory new action
CreateFollower action = new CreateFollower(parentQuest);
action.foeSymbol = new Symbol(match.Groups["symbol"].Value);
return action;
}
public override void Update(Task caller)
{
// Check for a new spawn event - only one spawn event can be running at a time
if (seq==Seq.none)
{
// Get the Foe resource
Foe foe = ParentQuest.GetFoe(foeSymbol);
if (foe == null)
{
SetComplete();
throw new Exception(string.Format("create follower could not find Foe with symbol name {0}", Symbol.Name));
}
// Start deploying GameObjects
CreatePendingFoeSpawn(foe);
}
// Try to deploy
if (seq==Seq.creating)
{
TryPlacement();
GameManager.Instance.RaiseOnEncounterEvent();
}
//update
if (seq == Seq.done)
{
if (senses.Target == GameManager.Instance.PlayerEntityBehaviour)
{
//follow player without attacking
//TODO: for ranged attacks
attack.ResetMeleeTimer();
}
}
}
#region Private Methods
void CreatePendingFoeSpawn(Foe foe)
{
if (foeObject == null)
{
// Get foe GameObject
MobileReactions reaction = MobileReactions.Hostile;
MobileGender gender = MobileGender.Male;
foeObject = GameObjectHelper.CreateEnemy("lolz",
foe.FoeType,
Vector3.zero,
mobileGender: gender,
mobileReaction: reaction);
if (foeObject == null)
{
SetComplete();
throw new Exception(string.Format("create foe attempted to create {0}x{1} GameObjects and failed.", foe.SpawnCount, Symbol.Name));
}
}
// Initiate deployment process
// Usually the foe will spawn immediately but can take longer depending on available placement space
// This process ensures these foes have all been deployed before starting next cycle
seq = Seq.creating;
}
void TryPlacement()
{
PlayerEnterExit playerEnterExit = GameManager.Instance.PlayerEnterExit;
// Place in world near player depending on local area
if (playerEnterExit.IsPlayerInsideBuilding)
{
PlaceFoeBuildingInterior(foeObject, playerEnterExit.Interior);
}
else if (playerEnterExit.IsPlayerInsideDungeon)
{
PlaceFoeDungeonInterior(foeObject, playerEnterExit.Dungeon);
}
else if (!playerEnterExit.IsPlayerInside && GameManager.Instance.PlayerGPS.IsPlayerInLocationRect)
{
PlaceFoeExteriorLocation(foeObject, GameManager.Instance.StreamingWorld.CurrentPlayerLocationObject);
}
else
{
PlaceFoeWilderness(foeObject);
}
}
#endregion
#region Placement Methods
// Place foe somewhere near player when inside a building
// Building interiors have spawn nodes for this placement so we can roll out foes all at once
void PlaceFoeBuildingInterior(GameObject gameObjects, DaggerfallInterior interiorParent)
{
// Must have a DaggerfallLocation parent
if (interiorParent == null)
{
SetComplete();
throw new Exception("PlaceFoeFreely() must have a DaggerfallLocation parent object.");
}
// Always place foes around player rather than use spawn points
// Spawn points work well for "interior hunt" quests but less so for "directly attack the player"
// Feel just placing freely will yield best results overall
PlaceFoeFreely(gameObjects, interiorParent.transform);
return;
}
// Place foe somewhere near player when inside a dungeon
// Dungeons interiors are complex 3D environments with no navgrid/navmesh or known spawn nodes
void PlaceFoeDungeonInterior(GameObject gameObjects, DaggerfallDungeon dungeonParent)
{
PlaceFoeFreely(gameObjects, dungeonParent.transform);
}
// Place foe somewhere near player when outside a location navgrid is available
// Navgrid placement helps foe avoid getting tangled in geometry like buildings
void PlaceFoeExteriorLocation(GameObject gameObjects, DaggerfallLocation locationParent)
{
PlaceFoeFreely(gameObjects, locationParent.transform);
}
// Place foe somewhere near player when outside and no navgrid available
// Wilderness environments are currently open so can be placed on ground anywhere within range
void PlaceFoeWilderness(GameObject gameObjects)
{
// TODO this false will need to be true when start caching enemies
GameManager.Instance.StreamingWorld.TrackLooseObject(gameObjects, false, -1, -1, true);
PlaceFoeFreely(gameObjects, null, 8f, 25f);
}
// Uses raycasts to find next spawn position just outside of player's field of view
void PlaceFoeFreely(GameObject gameObjects, Transform parent, float minDistance = 5f, float maxDistance = 20f)
{
const float overlapSphereRadius = 0.65f;
const float separationDistance = 1.25f;
const float maxFloorDistance = 4f;
// Must have received a valid array
if (gameObjects == null)
return;
// Set parent - otherwise caller must set a parent
if (parent)
gameObjects.transform.parent = parent;
// Select a left or right direction outside of camera FOV
Quaternion rotation;
float directionAngle = GameManager.Instance.MainCamera.fieldOfView;
directionAngle += UnityEngine.Random.Range(0f, 4f);
if (UnityEngine.Random.Range(0f, 1f) > 0.5f)
rotation = Quaternion.Euler(0, -directionAngle, 0);
else
rotation = Quaternion.Euler(0, directionAngle, 0);
// Get direction vector and create a new ray
Vector3 angle = (rotation * Vector3.forward).normalized;
Vector3 spawnDirection = GameManager.Instance.PlayerObject.transform.TransformDirection(angle).normalized;
Ray ray = new Ray(GameManager.Instance.PlayerObject.transform.position, spawnDirection);
// Check for a hit
Vector3 currentPoint;
RaycastHit initialHit;
if (Physics.Raycast(ray, out initialHit, maxDistance))
{
float cos_normal = Vector3.Dot(-spawnDirection, initialHit.normal.normalized);
if (cos_normal < 1e-6)
return;
float separationForward = separationDistance / cos_normal;
// Must be greater than minDistance
float distanceSlack = initialHit.distance - separationForward - minDistance;
if (distanceSlack < 0f)
return;
// Separate out from hit point
float extraDistance = UnityEngine.Random.Range(0f, Mathf.Min(2f, distanceSlack));
currentPoint = initialHit.point - spawnDirection * (separationForward + extraDistance);
}
else
{
// Player might be in an open area (e.g. outdoors) pick a random point along spawn direction
currentPoint = GameManager.Instance.PlayerObject.transform.position + spawnDirection * UnityEngine.Random.Range(minDistance, maxDistance);
}
// Must be able to find a surface below
RaycastHit floorHit;
ray = new Ray(currentPoint, Vector3.down);
if (!Physics.Raycast(ray, out floorHit, maxFloorDistance))
return;
// Ensure this is open space
Vector3 testPoint = floorHit.point + Vector3.up * separationDistance;
Collider[] colliders = Physics.OverlapSphere(testPoint, overlapSphereRadius);
if (colliders.Length > 0)
return;
// This looks like a good spawn position
foeObject.transform.position = testPoint;
FinalizeFoe(foeObject);
gameObjects.transform.LookAt(GameManager.Instance.PlayerObject.transform.position);
motor = foeObject.GetComponent<EnemyMotor>();
senses = foeObject.GetComponent<EnemySenses>();
attack = foeObject.GetComponent<EnemyAttack>();
seq = Seq.done;
}
// Fine tunes foe position slightly based on mobility and enables GameObject
void FinalizeFoe(GameObject go)
{
var mobileUnit = go.GetComponentInChildren<MobileUnit>();
if (mobileUnit)
{
// Align ground creatures on surface, raise flying creatures slightly into air
if (mobileUnit.Enemy.Behaviour != MobileBehaviour.Flying)
GameObjectHelper.AlignControllerToGround(go.GetComponent<CharacterController>());
else
go.transform.localPosition += Vector3.up * 1.5f;
}
else
{
// Just align to ground
GameObjectHelper.AlignControllerToGround(go.GetComponent<CharacterController>());
}
go.SetActive(true);
}
#endregion
#region Event Handlers
private void PlayerEnterExit_OnTransition(PlayerEnterExit.TransitionEventArgs args)
{
PlayerEnterExit_Reset();
}
private void StreamingWorld_OnInitWorld()
{
PlayerEnterExit_Reset();
}
private void PlayerEnterExit_Reset()
{
// Any foes pending placement are now invalid
seq = Seq.none;
}
#endregion
#region Serialization
[fsObject("v1")]
public struct SaveData_v1
{
public Symbol foeSymbol;
}
public override object GetSaveData()
{
SaveData_v1 data = new SaveData_v1();
data.foeSymbol = foeSymbol;
return data;
}
public override void RestoreSaveData(object dataIn)
{
if (dataIn == null)
return;
SaveData_v1 data = (SaveData_v1)dataIn;
foeSymbol = data.foeSymbol;
}
#endregion
}
}