I highly recommend looking into ViewModels. ViewModels are essentially a clone of your character, except it’s controlled entirely independently from your movement. I’ve written up a sample code for how you’d set up a basic ViewModel, note this is to support your understanding of it, I still strongly urge you to look into ViewModels. They are used in FPS games, but I see a potential for them to be used in this context as well.
Firstly, you need to setup the ViewModel by cloning the character - then you need to setup the ViewModel’s HRP, in this case, it’s called “PsuedoHRP”, it’ll act as a camera later on. We then loop through the bodyparts of the ViewModel to destroy any unnecessary items, we only want the limbs at this moment.
Note, this is all in one local script located in StarterCharacterScripts.
local viewModel,psuedoHRP,watch;
local IN_VIEW = false
local LoadedAnimations = {} --// STORE CHARACTER ANIMATIONS LOADED INTO THE VIEWMODEL
--// FUNCTIONS
function GenerateViewmodel()
char.Archivable = true -- ALLOW CLONING OF THE CHARACTER
viewModel = char:Clone()
viewModel.Parent = camera
viewModel.Name = "ViewModel"
char.Archivable = false
psuedoHRP = Instance.new("Part") -- "PSEUDOHRP" ACTS AS THE VIEWMODEL CAMERA
psuedoHRP.Name = "ViewmodelHRP"
psuedoHRP.Size = Vector3.new(0.5,0.5,2)
psuedoHRP.Anchored = true
psuedoHRP.CanCollide = false
psuedoHRP.Parent = camera
psuedoHRP.Transparency = 1
--// FOR THE SAKE OF SIMPLICITY, I'M ASSIGNING THE ATTACHMENT A POSITION WHICH SHOULD REPRESENT THE ACTUAL WATCH MODEL ON THE ARM
watch = Instance.new("Attachment")
watch.Parent = char.RightLowerArm
watch.Position = Vector3.new(0, -0.107, -0.51)
--// DELETE UNNECCESSARY BODY PARTS
for _,bodyPart in pairs(viewModel:GetDescendants()) do
if bodyPart:IsA("BasePart") then
bodyPart.CastShadow = false
bodyPart.CollisionGroup = "ViewModel"
local LowerName = bodyPart.Name:lower()
if LowerName:match("leg") or LowerName:match("foot") then
bodyPart:Destroy()
elseif not (LowerName:match("arm") or LowerName:match("hand")) then
bodyPart.Transparency = 1
end
elseif bodyPart:IsA("Decal") then
bodyPart:Destroy()
elseif bodyPart:IsA("Accessory") then
bodyPart:Destroy()
elseif bodyPart:IsA("LocalScript") then
bodyPart:Destroy()
end
end
end
Secondly, (the simplest part) you’ll need to link this all into a RenderStepped loop. We want to move the ViewModel and the ViewModel’s camera for each frame, as well as updating the animation for the ViewModel, which I’ll explain further along. It should look something like this.
RunService.RenderStepped:Connect(function()
if not (viewModel or psuedoHRP) then return end --// IF EITHER AREN'T LOADED, DON'T PROGRESS FURTHER
_UpdateAnimations()
viewModel:SetPrimaryPartCFrame(camera.CFrame)
if not IN_VIEW then
psuedoHRP.CFrame = camera.CFrame --// AS THE VIEWMODEL HRP ACTS AS A CAMERA, IT'LL NEED TO REPLICATE THE CAMERA CFRAME FOR EACH FRAME.
end
end)
Then you’ll need to either create an animation that extends the arm into the FOV of the player or manually reposition the arm using CFrames. At this point, your ViewModel should be able to replicate animations being played by the character. I’d personally recommend using an animation, it’s simpler and less messy, but to each their own. I’ve written a sample code for replicating the animations if you’re struggling with this part.
The animator in reference will be the characters animator, we want to always fire an event whenever the character is playing an animation. Moreover, you’ll want to assign an attribute to your loaded animations to filter out animations that may be unrelated to the ViewModel with those that are (focusing the watch for instance).
We’ll also need to update the animation whenever it’s updated on the Character to the Viewmodel. _UpdateAnimation()
will therefore need to run in the RenderStepped loop.
function _UpdateAnimations()
for CharAnim,Anim in pairs(LoadedAnimations) do
if CharAnim.IsPlaying ~= Anim.IsPlaying then
if CharAnim.IsPlaying then
Anim:Play()
else
Anim:Stop()
end
end
Anim.TimePosition = CharAnim.TimePosition
Anim:AdjustWeight(CharAnim.WeightCurrent, 0)
end
end
function GenerateViewmodel()
--// THIS WILL BE IN ADDITION TO WHAT WAS WRITTEN ABOVE, THIS SHOULD RUN ALONGSIDE THE SETUP CALL FOR A VIEWMODEL TO INITIATE THE ANIMATOR.
local viewModelAnimator = viewModel.Humanoid.Animator --// REPLICATE CHARACTER ANIMATIONS ONTO VIEWMODEL
viewModel.Humanoid.BreakJointsOnDeath = false
viewModel.Head.Anchored = true
viewModel.PrimaryPart = viewModel.Head
viewModel:SetPrimaryPartCFrame(CFrame.new(0, 0, 0))
end
local exampleAnim = animator:LoadAnimation(watch.Animation) --// LOAD EXAMPLE ANIMATION
exampleAnim:SetAttribute("WatchAnimation",true) --// ASSIGN EXAMPLE ANIMATION UNIQUE ATTRIBUTE TO IDENTIFY WHETHER TO PLAY ON VIEWMODEL
animator.AnimationPlayed:Connect(function(AnimationTrack)
if AnimationTrack:GetAttribute("WatchAnimation") ~= true then return end --// IF ANIMATION ISN'T LINKED TO THE VIEWMODEL, SKIP OVER IT. (FILTER)
if not LoadedAnimations[AnimationTrack] then --// INDEX THE LOADED ANIMATIONS USING THE ANIMATION TRACK
LoadedAnimations[AnimationTrack] = viewModelAnimator:LoadAnimation(AnimationTrack.Animation) --// LOAD ANIMATION FOR VIEWMODEL ANIMATOR
end
end)
Finally, you’ll need to change your camera movement to focus on the “PsuedoHRP” part. This will lock the camera to a part, allowing for slight mouse/camera movement. This is what that should look like. The camera movement will follow the mouse, and should be similar to what you’ve described.
function AdjustCamera(toggle)
local maxTilt = 10 --// CLAMP THE ANGLE IN WHICH THE CAMERA WILL TILT
local function Active()
camera.CFrame = psuedoHRP.CFrame * CFrame.Angles(
math.rad((((mouse.Y - mouse.ViewSizeY / 2) / mouse.ViewSizeY)) * -maxTilt),
math.rad((((mouse.X - mouse.ViewSizeX / 2) / mouse.ViewSizeX)) * -maxTilt),
0
)
end
if toggle then
camera.CameraType = Enum.CameraType.Scriptable
IN_VIEW = true
psuedoHRP.CFrame = CFrame.lookAt(psuedoHRP.Position, watch.Position) --// POINT "CAMERA" TO THE WATCH
RunService:BindToRenderStep("ActiveCamera",Enum.RenderPriority.Camera.Value,Active)
else
RunService:UnbindFromRenderStep("ActiveCamera")
camera.CameraType = Enum.CameraType.Custom
IN_VIEW = false
end
end
I hope this is useful, please let me know if you have any questions regarding this, I rushed this out a little so there may be some mistakes. This code provided by all means will not be able to complete what you’re looking to accomplish, I hope they provide a strong basis on how to achieve it though. They shouldn’t require too much adjustment to solve, but again, if you need additional support I’m happy to answer any other questions. I don’t want to spoon feed what you’re trying to accomplish, but I’ve tried to explain each step as much as I could.
Also, the ability to focus in on the watch with a tool being equipped can simply be done with the tool.equipped event. Moreover, you could simply use UserInputService to initiate the AdjustCamera()
function when either mouse button is clicked or held down.